@ak--47/dungeon-master 1.5.4 → 1.6.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/.claude/skills/analyze-soup/SKILL.md +21 -12
- package/.claude/skills/create-dungeon/SKILL.md +65 -5
- package/.claude/skills/create-project/SKILL.md +1 -1
- package/.claude/skills/create-project/provision.mjs +57 -10
- package/.claude/skills/verify-dungeon/SKILL.md +56 -25
- package/.claude/skills/verify-dungeon/references/counting-semantics.md +26 -6
- package/.claude/skills/verify-dungeon/references/report-format.md +14 -8
- package/.claude/skills/write-hooks/SKILL.md +156 -18
- package/CHANGELOG.md +183 -0
- package/HOOKS.md +566 -112
- package/README.md +28 -9
- package/docs/guides/1.3.0-upgrade-guide.md +262 -0
- package/docs/guides/1.3.1-upgrade-guide.md +328 -0
- package/docs/guides/1.4.0-upgrade-guide.md +130 -0
- package/docs/guides/1.4.1-upgrade-guide.md +125 -0
- package/docs/guides/1.4.3-upgrade-guide.md +200 -0
- package/docs/guides/1.4.4-upgrade-guide.md +95 -0
- package/docs/guides/1.5.0-upgrade-guide.md +426 -0
- package/docs/guides/1.5.1-upgrade-guide.md +178 -0
- package/docs/guides/1.5.3-upgrade-guide.md +117 -0
- package/docs/guides/1.6.0-upgrade-guide.md +204 -0
- package/dungeons/technical/stories-verify.js +152 -0
- package/dungeons/vertical/README.md +154 -0
- package/dungeons/vertical/ai-platform/ai-platform.js +1424 -0
- package/dungeons/vertical/ai-platform/ai-platform.sql +184 -0
- package/dungeons/vertical/ai-platform/ai-platform.verify.mjs +71 -0
- package/dungeons/vertical/community/community.js +1589 -0
- package/dungeons/vertical/community/community.sql +216 -0
- package/dungeons/vertical/community/community.verify.mjs +72 -0
- package/dungeons/vertical/crypto/crypto.js +1483 -0
- package/dungeons/vertical/crypto/crypto.sql +194 -0
- package/dungeons/vertical/crypto/crypto.verify.mjs +78 -0
- package/dungeons/vertical/dating/dating.js +1540 -0
- package/dungeons/vertical/dating/dating.sql +288 -0
- package/dungeons/vertical/dating/dating.verify.mjs +72 -0
- package/dungeons/vertical/devtools/devtools.js +1524 -0
- package/dungeons/vertical/devtools/devtools.sql +161 -0
- package/dungeons/vertical/devtools/devtools.verify.mjs +73 -0
- package/dungeons/vertical/ecommerce/ecommerce.js +1480 -0
- package/dungeons/vertical/ecommerce/ecommerce.sql +218 -0
- package/dungeons/vertical/ecommerce/ecommerce.verify.mjs +70 -0
- package/dungeons/vertical/education/education.js +1763 -0
- package/dungeons/vertical/education/education.sql +243 -0
- package/dungeons/vertical/education/education.verify.mjs +75 -0
- package/dungeons/vertical/fintech/fintech.js +1410 -0
- package/dungeons/vertical/fintech/fintech.sql +175 -0
- package/dungeons/vertical/fintech/fintech.verify.mjs +70 -0
- package/dungeons/vertical/fitness/fitness.js +1495 -0
- package/dungeons/vertical/fitness/fitness.sql +206 -0
- package/dungeons/vertical/fitness/fitness.verify.mjs +71 -0
- package/dungeons/vertical/food-delivery/food-delivery.js +1641 -0
- package/dungeons/vertical/food-delivery/food-delivery.sql +183 -0
- package/dungeons/vertical/food-delivery/food-delivery.verify.mjs +75 -0
- package/dungeons/vertical/gaming/gaming.js +1741 -0
- package/dungeons/vertical/gaming/gaming.sql +249 -0
- package/dungeons/vertical/gaming/gaming.verify.mjs +70 -0
- package/dungeons/vertical/healthcare/healthcare.js +1488 -0
- package/dungeons/vertical/healthcare/healthcare.sql +193 -0
- package/dungeons/vertical/healthcare/healthcare.verify.mjs +72 -0
- package/dungeons/vertical/insurance-application/insurance-application.js +1852 -0
- package/dungeons/vertical/insurance-application/insurance-application.sql +174 -0
- package/dungeons/vertical/insurance-application/insurance-application.verify.mjs +74 -0
- package/dungeons/vertical/logistics/logistics.js +1541 -0
- package/dungeons/vertical/logistics/logistics.sql +205 -0
- package/dungeons/vertical/logistics/logistics.verify.mjs +73 -0
- package/dungeons/vertical/marketplace/marketplace.js +1438 -0
- package/dungeons/vertical/marketplace/marketplace.sql +148 -0
- package/dungeons/vertical/marketplace/marketplace.verify.mjs +74 -0
- package/dungeons/vertical/{media.js → media/media.js} +557 -58
- package/dungeons/vertical/media/media.sql +173 -0
- package/dungeons/vertical/media/media.verify.mjs +74 -0
- package/dungeons/vertical/real-estate/real-estate.js +1629 -0
- package/dungeons/vertical/real-estate/real-estate.sql +167 -0
- package/dungeons/vertical/real-estate/real-estate.verify.mjs +75 -0
- package/dungeons/vertical/sass/sass.js +1733 -0
- package/dungeons/vertical/sass/sass.sql +173 -0
- package/dungeons/vertical/sass/sass.verify.mjs +74 -0
- package/dungeons/vertical/social/social.js +1456 -0
- package/dungeons/vertical/social/social.sql +160 -0
- package/dungeons/vertical/social/social.verify.mjs +76 -0
- package/dungeons/vertical/streaming/streaming.js +677 -0
- package/dungeons/vertical/streaming/streaming.sql +101 -0
- package/dungeons/vertical/streaming/streaming.verify.mjs +75 -0
- package/dungeons/vertical/support-desk/support-desk.js +741 -0
- package/dungeons/vertical/support-desk/support-desk.sql +137 -0
- package/dungeons/vertical/support-desk/support-desk.verify.mjs +75 -0
- package/dungeons/vertical/travel/travel.js +1603 -0
- package/dungeons/vertical/travel/travel.sql +236 -0
- package/dungeons/vertical/travel/travel.verify.mjs +72 -0
- package/index.js +2 -2
- package/lib/core/config-validator.js +37 -2
- package/lib/core/context.js +4 -2
- package/lib/core/dungeon-loader.js +20 -15
- package/lib/core/dungeon-to-json.js +22 -4
- package/lib/core/storage.js +3 -3
- package/lib/generators/funnels.js +36 -9
- package/lib/hook-helpers/cohort.js +53 -0
- package/lib/hook-helpers/index.js +8 -0
- package/lib/hook-helpers/mutate.js +1 -1
- package/lib/hook-helpers/shape.js +319 -0
- package/lib/hook-patterns/aggregate-per-user-by-bin.js +13 -5
- package/lib/hook-patterns/attributed-by-source.js +60 -58
- package/lib/hook-patterns/frequency-by-frequency.js +14 -4
- package/lib/hook-patterns/funnel-frequency-breakdown.js +24 -12
- package/lib/hook-patterns/index.js +1 -1
- package/lib/hook-patterns/time-to-convert-by-segment.js +75 -13
- package/lib/orchestrators/user-loop.js +41 -0
- package/lib/templates/story-spec.schema.json +175 -0
- package/lib/utils/json-evaluator.js +1 -1
- package/lib/utils/utils.js +5 -4
- package/lib/verify/coerce.js +186 -0
- package/lib/verify/counting.js +270 -32
- package/lib/verify/emulate-breakdown.js +1285 -203
- package/lib/verify/first-time.js +89 -0
- package/lib/verify/flows.js +679 -0
- package/lib/verify/formula.js +259 -0
- package/lib/verify/funnel-engine.js +697 -113
- package/lib/verify/index.js +32 -4
- package/lib/verify/sessionize.js +275 -0
- package/lib/verify/story-runner.js +483 -0
- package/lib/verify/verify-dungeon.js +59 -37
- package/package.json +2 -1
- package/scripts/extract-dungeon-schema.mjs +2 -2
- package/scripts/run-many.mjs +2 -2
- package/scripts/verify-runner.mjs +2 -2
- package/scripts/verify-stories.mjs +244 -0
- package/types.d.ts +296 -30
- package/dungeons/vertical/ai-platform.js +0 -818
- package/dungeons/vertical/community.js +0 -966
- package/dungeons/vertical/crypto.js +0 -895
- package/dungeons/vertical/dating.js +0 -830
- package/dungeons/vertical/devtools.js +0 -1050
- package/dungeons/vertical/ecommerce.js +0 -811
- package/dungeons/vertical/education.js +0 -964
- package/dungeons/vertical/fintech.js +0 -946
- package/dungeons/vertical/fitness.js +0 -935
- package/dungeons/vertical/food-delivery.js +0 -934
- package/dungeons/vertical/gaming.js +0 -1120
- package/dungeons/vertical/healthcare.js +0 -945
- package/dungeons/vertical/insurance-application.js +0 -1039
- package/dungeons/vertical/logistics.js +0 -986
- package/dungeons/vertical/marketplace.js +0 -1019
- package/dungeons/vertical/real-estate.js +0 -872
- package/dungeons/vertical/sass.js +0 -995
- package/dungeons/vertical/social.js +0 -904
- package/dungeons/vertical/travel.js +0 -930
|
@@ -1,964 +0,0 @@
|
|
|
1
|
-
// ── IMPORTS ──
|
|
2
|
-
import dayjs from "dayjs";
|
|
3
|
-
import utc from "dayjs/plugin/utc.js";
|
|
4
|
-
dayjs.extend(utc);
|
|
5
|
-
import "dotenv/config";
|
|
6
|
-
import * as u from "../../lib/utils/utils.js";
|
|
7
|
-
import * as v from "ak-tools";
|
|
8
|
-
/** @typedef {import("../../types").Dungeon} Config */
|
|
9
|
-
|
|
10
|
-
// ── OVERVIEW ──
|
|
11
|
-
/*
|
|
12
|
-
* NAME: LearnPath
|
|
13
|
-
* APP: Online learning platform modeled after Coursera, Khan Academy,
|
|
14
|
-
* and Udemy. Self-paced and cohort-based courses with quizzes,
|
|
15
|
-
* assignments, certificates, and a social study layer. Two-sided
|
|
16
|
-
* marketplace: ~89% students, ~11% instructors.
|
|
17
|
-
* SCALE: 10,000 users, ~1.4M events, 121 days (2026-01-01 → 2026-05-01)
|
|
18
|
-
* CORE LOOP: account registered → course enrolled → lecture started/completed → quiz → certificate
|
|
19
|
-
*
|
|
20
|
-
* EVENTS (17):
|
|
21
|
-
* lecture started (18) > lecture completed (14) > practice problem solved (12)
|
|
22
|
-
* > quiz started (10) > resource downloaded (9) > course enrolled (8)
|
|
23
|
-
* > quiz completed (8) > discussion posted (7) > assignment submitted (6)
|
|
24
|
-
* > assignment graded (5) > help requested (4) > study group joined (4)
|
|
25
|
-
* > instructor feedback given (3) > course reviewed (3) > certificate earned (2)
|
|
26
|
-
* > subscription purchased (2) > account registered (1)
|
|
27
|
-
*
|
|
28
|
-
* FUNNELS (7):
|
|
29
|
-
* - Onboarding: account registered → course enrolled → lecture started (75%)
|
|
30
|
-
* - Learning Loop: lecture started → lecture completed → practice problem solved (70%, reentry)
|
|
31
|
-
* - Assessment: quiz started → quiz completed → assignment submitted (55%, reentry)
|
|
32
|
-
* - Course Completion: course enrolled → lecture completed → quiz completed → certificate earned (30%)
|
|
33
|
-
* - Social Learning: discussion posted → study group joined → resource downloaded (50%, AI Study Buddy A/B)
|
|
34
|
-
* - Instructor Interaction: assignment submitted → assignment graded → instructor feedback given (45%)
|
|
35
|
-
* - Support/Monetization: help requested → subscription purchased → course reviewed (35%)
|
|
36
|
-
*
|
|
37
|
-
* USER PROPS: account_type, subscription_status, learning_style, education_level, timezone, courses_created, teaching_experience_years, instructor_rating, learning_goal, study_hours_per_week, Platform
|
|
38
|
-
* SUPER PROPS: Platform
|
|
39
|
-
* SCD PROPS: enrollment_status (enrolled/active/completed/dropped, monthly fuzzy, max 6), course_status (draft/published/archived/deprecated, monthly fixed, max 6, type: course_id)
|
|
40
|
-
* GROUPS: course_id (150 courses), group_id (300 study groups)
|
|
41
|
-
*/
|
|
42
|
-
|
|
43
|
-
// ── HOOK STORIES ──
|
|
44
|
-
/*
|
|
45
|
-
* ---------------------------------------------------------------
|
|
46
|
-
* 1. STUDENT VS INSTRUCTOR PROFILES (user)
|
|
47
|
-
* ---------------------------------------------------------------
|
|
48
|
-
*
|
|
49
|
-
* PATTERN: Instructor profiles get teaching attributes
|
|
50
|
-
* (courses_created, teaching_experience_years, instructor_rating).
|
|
51
|
-
* Students get learning attributes (learning_goal,
|
|
52
|
-
* study_hours_per_week). Two-sided marketplace at ~89% students,
|
|
53
|
-
* ~11% instructors.
|
|
54
|
-
*
|
|
55
|
-
* HOW TO FIND IT IN MIXPANEL:
|
|
56
|
-
*
|
|
57
|
-
* Report 1: Account Mix
|
|
58
|
-
* - Report type: Insights
|
|
59
|
-
* - Event: any event
|
|
60
|
-
* - Measure: Unique users
|
|
61
|
-
* - Breakdown: "account_type"
|
|
62
|
-
* - Expected: ~89% students, ~11% instructors
|
|
63
|
-
*
|
|
64
|
-
* Report 2: Instructor-Driven Feedback
|
|
65
|
-
* - Report type: Insights
|
|
66
|
-
* - Event: "instructor feedback given"
|
|
67
|
-
* - Measure: Total per user (average)
|
|
68
|
-
* - Breakdown: "account_type"
|
|
69
|
-
* - Expected: instructors dominate feedback volume; students rarely emit
|
|
70
|
-
*
|
|
71
|
-
* REAL-WORLD ANALOGUE: Two-sided learning marketplaces have
|
|
72
|
-
* fundamentally different role personas — teachers create supply,
|
|
73
|
-
* learners consume it.
|
|
74
|
-
*
|
|
75
|
-
* ---------------------------------------------------------------
|
|
76
|
-
* 2. DEADLINE CRAMMING (everything)
|
|
77
|
-
* ---------------------------------------------------------------
|
|
78
|
-
*
|
|
79
|
-
* PATTERN: Assignments submitted on Sun/Mon are rushed — 60% are late
|
|
80
|
-
* (raw is_late prop set true at 60% likelihood) vs ~20% baseline. Quiz
|
|
81
|
-
* scores on Sun/Mon drop by 25 points. No flag — discover via Day of Week
|
|
82
|
-
* breakdown.
|
|
83
|
-
*
|
|
84
|
-
* HOW TO FIND IT IN MIXPANEL:
|
|
85
|
-
*
|
|
86
|
-
* Report 1: Late Submission Rate by Day of Week
|
|
87
|
-
* - Report type: Insights
|
|
88
|
-
* - Event: "assignment submitted"
|
|
89
|
-
* - Measure: count where is_late=true / total
|
|
90
|
-
* - Breakdown: Day of Week
|
|
91
|
-
* - Expected: Sun/Mon ~ 60% late vs other days ~ 20%
|
|
92
|
-
*
|
|
93
|
-
* Report 2: Quiz Score by Day of Week
|
|
94
|
-
* - Report type: Insights
|
|
95
|
-
* - Event: "quiz completed"
|
|
96
|
-
* - Measure: Average of "score_percent"
|
|
97
|
-
* - Breakdown: Day of Week
|
|
98
|
-
* - Expected: Sun/Mon ~ 25, other days ~ 49 (-25 pts)
|
|
99
|
-
*
|
|
100
|
-
* REAL-WORLD ANALOGUE: Procrastination clusters submissions at the
|
|
101
|
-
* deadline weekend and hammers performance.
|
|
102
|
-
*
|
|
103
|
-
* ---------------------------------------------------------------
|
|
104
|
-
* 3. NOTES MAGIC NUMBER (everything, in-funnel)
|
|
105
|
-
* ---------------------------------------------------------------
|
|
106
|
-
*
|
|
107
|
-
* PATTERN: Sweet 5-8 lectures with notes_taken=true → +30% quiz
|
|
108
|
-
* score_percent (cap 100) and 40% chance of bonus cloned certificate.
|
|
109
|
-
* Over 9+ → 35% of certificate-earned events drop (over-noted but
|
|
110
|
-
* stuck in study mode). No flag.
|
|
111
|
-
*
|
|
112
|
-
* HOW TO FIND IT IN MIXPANEL:
|
|
113
|
-
*
|
|
114
|
-
* Report 1: Quiz Score by Notes-Taken Bucket
|
|
115
|
-
* - Cohort A: users with 5-8 "lecture completed" where notes_taken=true
|
|
116
|
-
* - Cohort B: users with 0-4
|
|
117
|
-
* - Event: "quiz completed"
|
|
118
|
-
* - Measure: Average of "score_percent"
|
|
119
|
-
* - Expected: A ~ 1.3x B
|
|
120
|
-
*
|
|
121
|
-
* Report 2: Certificates per User on Heavy Note-Takers
|
|
122
|
-
* - Cohort C: users with >= 9 notes-taken lectures
|
|
123
|
-
* - Cohort A: users with 5-8
|
|
124
|
-
* - Event: "certificate earned"
|
|
125
|
-
* - Measure: Total per user
|
|
126
|
-
* - Expected: C ~ 35% fewer certificates per user vs A
|
|
127
|
-
*
|
|
128
|
-
* REAL-WORLD ANALOGUE: Active note-taking lifts quiz performance, but
|
|
129
|
-
* obsessive note-taking signals "stuck in study mode" without finishing.
|
|
130
|
-
*
|
|
131
|
-
* ---------------------------------------------------------------
|
|
132
|
-
* 4. STUDY GROUP RETENTION (everything)
|
|
133
|
-
* ---------------------------------------------------------------
|
|
134
|
-
*
|
|
135
|
-
* PATTERN: Users who join a study group within 10 days get bonus
|
|
136
|
-
* discussion events. Non-joiners with low quiz scores (<60) churn
|
|
137
|
-
* hard at day 14 — all later events are removed.
|
|
138
|
-
*
|
|
139
|
-
* HOW TO FIND IT IN MIXPANEL:
|
|
140
|
-
*
|
|
141
|
-
* Report 1: D14 Retention by Early Group Join Cohort
|
|
142
|
-
* - Cohort A: users who fired "study group joined" within first 10 days
|
|
143
|
-
* - Cohort B: users with no early study group joined
|
|
144
|
-
* - Compare D14 retention (any event past d14) per cohort
|
|
145
|
-
* - Expected: A ~ 90%+ vs B (with low quiz scores) ~ 30%
|
|
146
|
-
*
|
|
147
|
-
* Report 2: Discussion Volume by Group Cohort
|
|
148
|
-
* - Cohort A vs B (as above)
|
|
149
|
-
* - Event: "discussion posted"
|
|
150
|
-
* - Measure: Total per user
|
|
151
|
-
* - Expected: A posts substantially more
|
|
152
|
-
*
|
|
153
|
-
* REAL-WORLD ANALOGUE: Social learning ties create accountability
|
|
154
|
-
* and dramatically reduce drop-off in cohort-based courses.
|
|
155
|
-
*
|
|
156
|
-
* ---------------------------------------------------------------
|
|
157
|
-
* 5. HINT DEPENDENCY (event)
|
|
158
|
-
* ---------------------------------------------------------------
|
|
159
|
-
*
|
|
160
|
-
* PATTERN: On "practice problem solved", hint_used=true gets difficulty
|
|
161
|
-
* forced to "easy" 60% of the time. hint_used=false gets difficulty forced
|
|
162
|
-
* to "hard" 40% of the time. No flag — discover via difficulty breakdown
|
|
163
|
-
* filtered by hint_used.
|
|
164
|
-
*
|
|
165
|
-
* HOW TO FIND IT IN MIXPANEL:
|
|
166
|
-
*
|
|
167
|
-
* Report 1: Easy Problem Mix for Hint Users
|
|
168
|
-
* - Report type: Insights
|
|
169
|
-
* - Event: "practice problem solved"
|
|
170
|
-
* - Measure: Total
|
|
171
|
-
* - Filter: "hint_used" = true
|
|
172
|
-
* - Breakdown: "difficulty"
|
|
173
|
-
* - Expected: ~60% easy (vs ~33% baseline)
|
|
174
|
-
*
|
|
175
|
-
* Report 2: Hard Problem Mix for Independent Solvers
|
|
176
|
-
* - Report type: Insights
|
|
177
|
-
* - Event: "practice problem solved"
|
|
178
|
-
* - Measure: Total
|
|
179
|
-
* - Filter: "hint_used" = false
|
|
180
|
-
* - Breakdown: "difficulty"
|
|
181
|
-
* - Expected: ~40% hard (vs ~33% baseline)
|
|
182
|
-
*
|
|
183
|
-
* REAL-WORLD ANALOGUE: Learners who lean on hints get nudged toward
|
|
184
|
-
* easier work, while those who push through unaided self-select
|
|
185
|
-
* into harder material.
|
|
186
|
-
*
|
|
187
|
-
* ---------------------------------------------------------------
|
|
188
|
-
* 6. SEMESTER-END SPIKE (everything)
|
|
189
|
-
* ---------------------------------------------------------------
|
|
190
|
-
*
|
|
191
|
-
* PATTERN: Days 75-85 simulate semester crunch. quiz_started, quiz_completed,
|
|
192
|
-
* and assignment_submitted events are duplicated at an 80% rate. No flag —
|
|
193
|
-
* discover via line chart of those event volumes by day.
|
|
194
|
-
*
|
|
195
|
-
* HOW TO FIND IT IN MIXPANEL:
|
|
196
|
-
*
|
|
197
|
-
* Report 1: Assessment Volume Over Time
|
|
198
|
-
* - Report type: Insights
|
|
199
|
-
* - Events: "quiz started" + "quiz completed" + "assignment submitted"
|
|
200
|
-
* - Measure: Total
|
|
201
|
-
* - Line chart by day
|
|
202
|
-
* - Expected: ~2x volume spike on days 75-85
|
|
203
|
-
*
|
|
204
|
-
* REAL-WORLD ANALOGUE: Semester-end deadlines reliably produce a
|
|
205
|
-
* massive last-minute surge in student activity.
|
|
206
|
-
*
|
|
207
|
-
* ---------------------------------------------------------------
|
|
208
|
-
* 7. FREE VS PAID COURSES (funnel-pre + everything)
|
|
209
|
-
* ---------------------------------------------------------------
|
|
210
|
-
*
|
|
211
|
-
* PATTERN: Free users get 0.5x funnel conversion rate on the cert funnel only; paid
|
|
212
|
-
* subscribers get 1.5x. Free users also lose 55% of their
|
|
213
|
-
* certificates, producing a ~2.2x completion gap.
|
|
214
|
-
*
|
|
215
|
-
* HOW TO FIND IT IN MIXPANEL:
|
|
216
|
-
*
|
|
217
|
-
* Report 1: Course Completion Funnel by Subscription
|
|
218
|
-
* - Report type: Funnels
|
|
219
|
-
* - Steps: "course enrolled" -> "lecture completed" -> "quiz completed" -> "certificate earned"
|
|
220
|
-
* - Breakdown: "subscription_status"
|
|
221
|
-
* - Expected: free ~ 15% completion, paid ~ 33% (~2.2x gap)
|
|
222
|
-
*
|
|
223
|
-
* Report 2: Certificates Earned per User
|
|
224
|
-
* - Report type: Insights
|
|
225
|
-
* - Event: "certificate earned"
|
|
226
|
-
* - Measure: Total per user (average)
|
|
227
|
-
* - Breakdown: "subscription_status"
|
|
228
|
-
* - Expected: paid subscribers earn substantially more certificates
|
|
229
|
-
*
|
|
230
|
-
* REAL-WORLD ANALOGUE: Paid commitment correlates strongly with
|
|
231
|
-
* follow-through; free learners drop off long before completion.
|
|
232
|
-
*
|
|
233
|
-
* ---------------------------------------------------------------
|
|
234
|
-
* 8. PLAYBACK SPEED CORRELATION (event + everything)
|
|
235
|
-
* ---------------------------------------------------------------
|
|
236
|
-
*
|
|
237
|
-
* PATTERN: Speed learners (>=2.0x speed on 3+ lectures) get 0.6x
|
|
238
|
-
* watch_time and a paradoxical +8 quiz score boost. Thorough
|
|
239
|
-
* learners (<=1.0x) get 1.4x watch_time.
|
|
240
|
-
*
|
|
241
|
-
* HOW TO FIND IT IN MIXPANEL:
|
|
242
|
-
*
|
|
243
|
-
* Report 1: Watch Time by Playback Speed
|
|
244
|
-
* - Report type: Insights
|
|
245
|
-
* - Event: "lecture completed"
|
|
246
|
-
* - Measure: Average of "watch_time_mins"
|
|
247
|
-
* - Breakdown: "playback_speed"
|
|
248
|
-
* - Expected: speed >= 2.0 ~ 0.6x baseline; speed <= 1.0 ~ 1.4x baseline
|
|
249
|
-
*
|
|
250
|
-
* Report 2: Quiz Score by Speed Learner Cohort
|
|
251
|
-
* - Cohort A: users with 3+ "lecture completed" events at playback_speed >= 2.0
|
|
252
|
-
* - Cohort B: rest
|
|
253
|
-
* - Event: "quiz completed"
|
|
254
|
-
* - Measure: Average of "score_percent"
|
|
255
|
-
* - Expected: A ~ +8 pts vs B
|
|
256
|
-
*
|
|
257
|
-
* REAL-WORLD ANALOGUE: Power users who watch lectures at 2x speed
|
|
258
|
-
* tend to be domain-confident and outperform on assessments
|
|
259
|
-
* despite spending less time.
|
|
260
|
-
*
|
|
261
|
-
* ---------------------------------------------------------------
|
|
262
|
-
* 9. COURSE COMPLETION TIME-TO-CONVERT (everything)
|
|
263
|
-
* ---------------------------------------------------------------
|
|
264
|
-
*
|
|
265
|
-
* PATTERN: Annual subscribers complete the course-completion funnel
|
|
266
|
-
* 2x faster (factor 0.5); Free users 1.8x slower (factor 1.8).
|
|
267
|
-
* Applied in the everything hook by scaling the enrolled-to-cert
|
|
268
|
-
* gap on the raw events. Stronger factors compensate for the
|
|
269
|
-
* composition effect from H7's conversion-rate gating.
|
|
270
|
-
*
|
|
271
|
-
* HOW TO FIND IT IN MIXPANEL:
|
|
272
|
-
*
|
|
273
|
-
* Report 1: Course Completion Median Time-to-Convert by Subscription
|
|
274
|
-
* - Funnels > "course enrolled" -> "lecture completed" -> "quiz completed" -> "certificate earned"
|
|
275
|
-
* - Measure: Median time to convert
|
|
276
|
-
* - Breakdown: subscription_status
|
|
277
|
-
* - Expected: annual < monthly < free (direction)
|
|
278
|
-
*
|
|
279
|
-
* Also visible via cross-event SQL: MIN("course enrolled" time) to
|
|
280
|
-
* MIN("certificate earned" time) per user, broken down by
|
|
281
|
-
* subscription_status. Annual < monthly < free.
|
|
282
|
-
*
|
|
283
|
-
* REAL-WORLD ANALOGUE: Paid commitment accelerates throughput.
|
|
284
|
-
*
|
|
285
|
-
* ---------------------------------------------------------------
|
|
286
|
-
* 10. SOCIAL LEARNING EXPERIMENT (funnel experiment)
|
|
287
|
-
* ---------------------------------------------------------------
|
|
288
|
-
*
|
|
289
|
-
* PATTERN: A/B experiment on the Social Learning funnel (discussion
|
|
290
|
-
* posted → study group joined → resource downloaded). "AI Study
|
|
291
|
-
* Buddy" variant boosts conversion 1.4x and speeds TTC to 0.85x.
|
|
292
|
-
* Activates 30 days before dataset end.
|
|
293
|
-
*
|
|
294
|
-
* HOW TO FIND IT IN MIXPANEL:
|
|
295
|
-
*
|
|
296
|
-
* Report 1: $experiment_started by Variant
|
|
297
|
-
* - Report type: Insights
|
|
298
|
-
* - Event: "$experiment_started"
|
|
299
|
-
* - Measure: Total
|
|
300
|
-
* - Breakdown: "$experiment_name" and "Variant"
|
|
301
|
-
* - Expected: ~50% Control, ~50% AI Study Buddy
|
|
302
|
-
*
|
|
303
|
-
* Report 2: Social Learning Funnel by Variant
|
|
304
|
-
* - Report type: Funnels
|
|
305
|
-
* - Steps: "discussion posted" → "study group joined" → "resource downloaded"
|
|
306
|
-
* - Breakdown: Variant
|
|
307
|
-
* - Expected: AI Study Buddy ~ 1.4x conversion vs Control
|
|
308
|
-
*
|
|
309
|
-
* REAL-WORLD ANALOGUE: AI-powered study companions boost social
|
|
310
|
-
* engagement and resource discovery in cohort-based courses.
|
|
311
|
-
*
|
|
312
|
-
* ===============================================================
|
|
313
|
-
* EXPECTED METRICS SUMMARY
|
|
314
|
-
* ===============================================================
|
|
315
|
-
*
|
|
316
|
-
* Hook | Metric | Baseline | Hook Effect | Ratio
|
|
317
|
-
* ------------------------|-----------------------|----------|--------------|------
|
|
318
|
-
* Student vs Instructor | Profile attributes | generic | role-specific| n/a
|
|
319
|
-
* Deadline Cramming | Sun/Mon quiz score | 1x | -25 pt | -38%
|
|
320
|
-
* Notes Magic Number | sweet quiz score | 1x | 1.3x | 1.3x
|
|
321
|
-
* Notes Magic Number | over certificates/user| 1x | 0.65x | -35%
|
|
322
|
-
* Study Group Retention | D14 retention | ~ 40% | ~ 90% | 2.3x
|
|
323
|
-
* Hint Dependency | easy problem rate (hint) | 33% | ~ 60% | 1.8x
|
|
324
|
-
* Semester-End Spike | Assessment volume | 1x | ~ 2x | 2x
|
|
325
|
-
* Free vs Paid | Course completion | 15% | 33% | 2.2x
|
|
326
|
-
* Playback Speed | Quiz score (speed) | ~ 65 | ~ 73 | +8 pt
|
|
327
|
-
* Course Completion T2C | median min by tier | 1x | 0.5x/1.8x | ~ 3.6x range
|
|
328
|
-
* Social Learning Exp | AI variant conversion | 1x | 1.4x | 1.4x
|
|
329
|
-
* Social Learning Exp | AI variant TTC | 1x | 0.85x | -15%
|
|
330
|
-
*/
|
|
331
|
-
|
|
332
|
-
// ── SCALE ──
|
|
333
|
-
const SEED = "harness-education";
|
|
334
|
-
const NUM_USERS = 10_000;
|
|
335
|
-
const DATASET_START = "2026-01-01T00:00:00Z";
|
|
336
|
-
const DATASET_END = "2026-05-01T23:59:59Z";
|
|
337
|
-
const EVENTS_PER_DAY = 1.2;
|
|
338
|
-
const token = process.env.MP_TOKEN || "your-mixpanel-token";
|
|
339
|
-
|
|
340
|
-
const chance = u.initChance(SEED);
|
|
341
|
-
|
|
342
|
-
// ── KNOBS (tweak these to reshape stories) ──
|
|
343
|
-
const HINT_EASY_LIKELIHOOD = 60;
|
|
344
|
-
const HINT_HARD_LIKELIHOOD = 40;
|
|
345
|
-
|
|
346
|
-
const SPEED_FAST_THRESHOLD = 2.0;
|
|
347
|
-
const SPEED_FAST_WATCH_FACTOR = 0.6;
|
|
348
|
-
const SPEED_FAST_WATCH_MIN = 3;
|
|
349
|
-
const SPEED_SLOW_THRESHOLD = 1.0;
|
|
350
|
-
const SPEED_SLOW_WATCH_FACTOR = 1.4;
|
|
351
|
-
const SPEED_SLOW_WATCH_MAX = 90;
|
|
352
|
-
const SPEED_LECTURE_COUNT_THRESHOLD = 3;
|
|
353
|
-
const SPEED_QUIZ_BOOST_POINTS = 8;
|
|
354
|
-
|
|
355
|
-
const FREE_FUNNEL_CONV_FACTOR = 0.5;
|
|
356
|
-
const PAID_FUNNEL_CONV_FACTOR = 1.5;
|
|
357
|
-
const FREE_CERT_DROP_LIKELIHOOD = 55;
|
|
358
|
-
|
|
359
|
-
const NOTES_SWEET_MIN = 5;
|
|
360
|
-
const NOTES_SWEET_MAX = 8;
|
|
361
|
-
const NOTES_OVER_THRESHOLD = 9;
|
|
362
|
-
const NOTES_QUIZ_BOOST = 1.3;
|
|
363
|
-
const NOTES_BONUS_CERT_LIKELIHOOD = 40;
|
|
364
|
-
const NOTES_OVER_CERT_DROP_LIKELIHOOD = 35;
|
|
365
|
-
|
|
366
|
-
const SEMESTER_SPIKE_START_DAY = 75;
|
|
367
|
-
const SEMESTER_SPIKE_END_DAY = 85;
|
|
368
|
-
const SEMESTER_SPIKE_LIKELIHOOD = 80;
|
|
369
|
-
|
|
370
|
-
const TTC_ANNUAL_FACTOR = 0.5;
|
|
371
|
-
const TTC_FREE_FACTOR = 1.8;
|
|
372
|
-
|
|
373
|
-
const STUDY_GROUP_EARLY_DAYS = 10;
|
|
374
|
-
const STUDY_GROUP_LOW_QUIZ_THRESHOLD = 60;
|
|
375
|
-
const STUDY_GROUP_CHURN_CUTOFF_DAYS = 14;
|
|
376
|
-
const STUDY_GROUP_DISCUSSION_CLONE_LIKELIHOOD = 60;
|
|
377
|
-
|
|
378
|
-
const DEADLINE_LATE_LIKELIHOOD = 60;
|
|
379
|
-
const DEADLINE_QUIZ_PENALTY = 25;
|
|
380
|
-
|
|
381
|
-
// ── DATA ARRAYS ──
|
|
382
|
-
// Generate consistent IDs for lookup tables and event properties
|
|
383
|
-
const courseIds = v.range(1, 151).map(n => `course_${v.uid(6)}`);
|
|
384
|
-
const quizIds = v.range(1, 401).map(n => `quiz_${v.uid(6)}`);
|
|
385
|
-
const groupIds = v.range(1, 301).map(n => `group_${v.uid(6)}`);
|
|
386
|
-
const lectureIds = v.range(1, 501).map(n => `lecture_${v.uid(6)}`);
|
|
387
|
-
const assignmentIds = v.range(1, 201).map(n => `assignment_${v.uid(6)}`);
|
|
388
|
-
const problemIds = v.range(1, 601).map(n => `problem_${v.uid(6)}`);
|
|
389
|
-
|
|
390
|
-
// ── HELPER FUNCTIONS ──
|
|
391
|
-
function handleUserHooks(record) {
|
|
392
|
-
// H1: STUDENT VS INSTRUCTOR PROFILES — role-based attributes.
|
|
393
|
-
if (record.account_type === "instructor") {
|
|
394
|
-
record.courses_created = chance.integer({ min: 1, max: 15 });
|
|
395
|
-
record.teaching_experience_years = chance.integer({ min: 1, max: 20 });
|
|
396
|
-
record.instructor_rating = Math.round((chance.floating({ min: 3.0, max: 5.0 }) + Number.EPSILON) * 100) / 100;
|
|
397
|
-
} else {
|
|
398
|
-
record.learning_goal = chance.pickone(["career_change", "skill_upgrade", "hobby", "degree_requirement"]);
|
|
399
|
-
record.study_hours_per_week = chance.integer({ min: 2, max: 30 });
|
|
400
|
-
}
|
|
401
|
-
return record;
|
|
402
|
-
}
|
|
403
|
-
|
|
404
|
-
function handleEventHooks(record) {
|
|
405
|
-
// H5: HINT DEPENDENCY — hint users get 60% easy problems; non-hint
|
|
406
|
-
// users get 40% hard problems. Mutates difficulty (raw).
|
|
407
|
-
if (record.event === "practice problem solved") {
|
|
408
|
-
if (record.hint_used === true && chance.bool({ likelihood: HINT_EASY_LIKELIHOOD })) {
|
|
409
|
-
record.difficulty = "easy";
|
|
410
|
-
} else if (record.hint_used === false && chance.bool({ likelihood: HINT_HARD_LIKELIHOOD })) {
|
|
411
|
-
record.difficulty = "hard";
|
|
412
|
-
}
|
|
413
|
-
}
|
|
414
|
-
// H8 (event): PLAYBACK SPEED — speed learners (>= 2.0x) get
|
|
415
|
-
// watch_time_mins compressed 0.6x; thorough learners (<= 1.0x) get 1.4x.
|
|
416
|
-
if (record.event === "lecture completed") {
|
|
417
|
-
const speed = record.playback_speed;
|
|
418
|
-
if (speed >= SPEED_FAST_THRESHOLD && record.watch_time_mins !== undefined) {
|
|
419
|
-
record.watch_time_mins = Math.max(SPEED_FAST_WATCH_MIN, Math.floor(record.watch_time_mins * SPEED_FAST_WATCH_FACTOR));
|
|
420
|
-
} else if (speed !== undefined && speed <= SPEED_SLOW_THRESHOLD && record.watch_time_mins !== undefined) {
|
|
421
|
-
record.watch_time_mins = Math.min(SPEED_SLOW_WATCH_MAX, Math.floor(record.watch_time_mins * SPEED_SLOW_WATCH_FACTOR));
|
|
422
|
-
}
|
|
423
|
-
}
|
|
424
|
-
return record;
|
|
425
|
-
}
|
|
426
|
-
|
|
427
|
-
function handleFunnelPreHooks(record, meta) {
|
|
428
|
-
// H7: FREE VS PAID — free users get 0.5x conversion rate; paid
|
|
429
|
-
// subscribers get 1.5x. Scoped to the course-completion funnel ONLY
|
|
430
|
-
// (sequence ending in "certificate earned") to avoid displacing standalone
|
|
431
|
-
// events for paid users and triggering unintended churn in H4.
|
|
432
|
-
const isCertFunnel = Array.isArray(meta?.funnel?.sequence) &&
|
|
433
|
-
meta.funnel.sequence.includes("certificate earned");
|
|
434
|
-
if (isCertFunnel) {
|
|
435
|
-
const subStatus = meta?.profile?.subscription_status;
|
|
436
|
-
if (subStatus === "free") {
|
|
437
|
-
record.conversionRate = Math.round(record.conversionRate * FREE_FUNNEL_CONV_FACTOR);
|
|
438
|
-
} else if (subStatus === "monthly" || subStatus === "annual") {
|
|
439
|
-
record.conversionRate = Math.min(100, Math.round(record.conversionRate * PAID_FUNNEL_CONV_FACTOR));
|
|
440
|
-
}
|
|
441
|
-
}
|
|
442
|
-
return record;
|
|
443
|
-
}
|
|
444
|
-
|
|
445
|
-
function handleEverythingHooks(record, meta) {
|
|
446
|
-
const datasetStart = dayjs.unix(meta.datasetStart);
|
|
447
|
-
const userEvents = record;
|
|
448
|
-
const profile = meta.profile;
|
|
449
|
-
const firstEventTime = userEvents.length > 0 ? dayjs(userEvents[0].time) : null;
|
|
450
|
-
|
|
451
|
-
if (profile) {
|
|
452
|
-
userEvents.forEach((event) => {
|
|
453
|
-
if (profile.Platform !== undefined) event.Platform = profile.Platform;
|
|
454
|
-
});
|
|
455
|
-
}
|
|
456
|
-
|
|
457
|
-
let notesTakenCount = 0;
|
|
458
|
-
let joinedStudyGroupEarly = false;
|
|
459
|
-
let hasLowQuizScore = false;
|
|
460
|
-
let speedLectureCount = 0;
|
|
461
|
-
|
|
462
|
-
userEvents.forEach((event) => {
|
|
463
|
-
const eventTime = dayjs(event.time);
|
|
464
|
-
const daysSinceStart = firstEventTime ? eventTime.diff(firstEventTime, 'days', true) : 0;
|
|
465
|
-
if (event.event === "lecture completed" && event.notes_taken === true) notesTakenCount++;
|
|
466
|
-
if (event.event === "study group joined" && daysSinceStart <= STUDY_GROUP_EARLY_DAYS) joinedStudyGroupEarly = true;
|
|
467
|
-
if (event.event === "quiz completed" && event.score_percent < STUDY_GROUP_LOW_QUIZ_THRESHOLD) hasLowQuizScore = true;
|
|
468
|
-
if (event.event === "lecture completed" && event.playback_speed >= SPEED_FAST_THRESHOLD) speedLectureCount++;
|
|
469
|
-
});
|
|
470
|
-
|
|
471
|
-
// H3 + H10: NOTES MAGIC NUMBER (in-funnel, no flags)
|
|
472
|
-
// Sweet 5-8 notes-taken lectures → +30% quiz score_percent (cap 100).
|
|
473
|
-
// Over 9+ → drop 35% of certificate-earned events (over-noted but
|
|
474
|
-
// can't synthesize; gets stuck in "study mode").
|
|
475
|
-
if (notesTakenCount >= NOTES_SWEET_MIN && notesTakenCount <= NOTES_SWEET_MAX) {
|
|
476
|
-
userEvents.forEach((event) => {
|
|
477
|
-
if (event.event === "quiz completed" && event.score_percent !== undefined) {
|
|
478
|
-
event.score_percent = Math.min(100, Math.round(event.score_percent * NOTES_QUIZ_BOOST));
|
|
479
|
-
}
|
|
480
|
-
});
|
|
481
|
-
if (chance.bool({ likelihood: NOTES_BONUS_CERT_LIKELIHOOD })) {
|
|
482
|
-
const lastEvent = userEvents[userEvents.length - 1];
|
|
483
|
-
const certTemplate = userEvents.find(e => e.event === "certificate earned");
|
|
484
|
-
if (lastEvent && certTemplate) {
|
|
485
|
-
userEvents.push({
|
|
486
|
-
...certTemplate,
|
|
487
|
-
time: dayjs(lastEvent.time).add(chance.integer({ min: 1, max: 5 }), 'days').toISOString(),
|
|
488
|
-
user_id: lastEvent.user_id,
|
|
489
|
-
course_id: chance.pickone(courseIds),
|
|
490
|
-
completion_time_days: chance.integer({ min: 14, max: 90 }),
|
|
491
|
-
final_grade: chance.integer({ min: 80, max: 100 }),
|
|
492
|
-
});
|
|
493
|
-
}
|
|
494
|
-
}
|
|
495
|
-
} else if (notesTakenCount >= NOTES_OVER_THRESHOLD) {
|
|
496
|
-
// Over-noters: drop 35% of certificates (stuck in study mode)
|
|
497
|
-
for (let i = userEvents.length - 1; i >= 0; i--) {
|
|
498
|
-
if (userEvents[i].event === "certificate earned" && chance.bool({ likelihood: NOTES_OVER_CERT_DROP_LIKELIHOOD })) {
|
|
499
|
-
userEvents.splice(i, 1);
|
|
500
|
-
}
|
|
501
|
-
}
|
|
502
|
-
}
|
|
503
|
-
|
|
504
|
-
// H8 (cont): Speed learners (3+ lectures at 2.0x) score +8 on quizzes.
|
|
505
|
-
if (speedLectureCount >= SPEED_LECTURE_COUNT_THRESHOLD) {
|
|
506
|
-
userEvents.forEach((event) => {
|
|
507
|
-
if (event.event === "quiz completed" && event.score_percent !== undefined) {
|
|
508
|
-
event.score_percent = Math.min(100, event.score_percent + SPEED_QUIZ_BOOST_POINTS);
|
|
509
|
-
}
|
|
510
|
-
});
|
|
511
|
-
}
|
|
512
|
-
|
|
513
|
-
// H6: SEMESTER-END SPIKE — duplicate quiz/assignment events
|
|
514
|
-
// in days 75-85 window. No flag — discover via line chart.
|
|
515
|
-
const duplicates = [];
|
|
516
|
-
const spikableEvents = ["quiz started", "quiz completed", "assignment submitted"];
|
|
517
|
-
userEvents.forEach((event) => {
|
|
518
|
-
if (spikableEvents.includes(event.event) && event.time) {
|
|
519
|
-
const dayInDataset = dayjs.utc(event.time).diff(datasetStart, 'days', true);
|
|
520
|
-
if (dayInDataset >= SEMESTER_SPIKE_START_DAY && dayInDataset <= SEMESTER_SPIKE_END_DAY && chance.bool({ likelihood: SEMESTER_SPIKE_LIKELIHOOD })) {
|
|
521
|
-
const dup = JSON.parse(JSON.stringify(event));
|
|
522
|
-
dup.time = dayjs(event.time).add(chance.integer({ min: 5, max: 120 }), 'minutes').toISOString();
|
|
523
|
-
duplicates.push(dup);
|
|
524
|
-
}
|
|
525
|
-
}
|
|
526
|
-
});
|
|
527
|
-
if (duplicates.length > 0) userEvents.push(...duplicates);
|
|
528
|
-
|
|
529
|
-
const subStatus = profile ? profile.subscription_status : "free";
|
|
530
|
-
|
|
531
|
-
// H9 (T2C): COURSE COMPLETION TIME-TO-CONVERT (everything)
|
|
532
|
-
// Annual subscribers complete the cert funnel 2x faster (factor 0.5);
|
|
533
|
-
// Free users 1.8x slower (factor 1.8). For each "certificate earned"
|
|
534
|
-
// event, find the nearest preceding "course enrolled" and scale the gap.
|
|
535
|
-
// Runs BEFORE cert-dropping (H7) so TTC adjustments aren't masked by
|
|
536
|
-
// survivorship bias from the 55% free cert removal.
|
|
537
|
-
{
|
|
538
|
-
const ttcFactor = (
|
|
539
|
-
subStatus === "annual" ? TTC_ANNUAL_FACTOR :
|
|
540
|
-
subStatus === "free" ? TTC_FREE_FACTOR :
|
|
541
|
-
1.0
|
|
542
|
-
);
|
|
543
|
-
if (ttcFactor !== 1.0) {
|
|
544
|
-
// Collect all "course enrolled" times (sorted) for binary lookup
|
|
545
|
-
const enrolledTimes = userEvents
|
|
546
|
-
.filter(e => e.event === "course enrolled")
|
|
547
|
-
.map(e => dayjs(e.time))
|
|
548
|
-
.sort((a, b) => a.valueOf() - b.valueOf());
|
|
549
|
-
|
|
550
|
-
if (enrolledTimes.length > 0) {
|
|
551
|
-
for (const event of userEvents) {
|
|
552
|
-
if (event.event === "certificate earned") {
|
|
553
|
-
const certTime = dayjs(event.time);
|
|
554
|
-
// Find the latest enrolled time before this cert
|
|
555
|
-
let anchor = null;
|
|
556
|
-
for (let k = enrolledTimes.length - 1; k >= 0; k--) {
|
|
557
|
-
if (enrolledTimes[k].isBefore(certTime)) {
|
|
558
|
-
anchor = enrolledTimes[k];
|
|
559
|
-
break;
|
|
560
|
-
}
|
|
561
|
-
}
|
|
562
|
-
if (anchor) {
|
|
563
|
-
const gap = certTime.diff(anchor);
|
|
564
|
-
const newGap = Math.round(gap * ttcFactor);
|
|
565
|
-
event.time = anchor.add(newGap, "milliseconds").toISOString();
|
|
566
|
-
}
|
|
567
|
-
}
|
|
568
|
-
}
|
|
569
|
-
}
|
|
570
|
-
}
|
|
571
|
-
}
|
|
572
|
-
|
|
573
|
-
// H7: FREE VS PAID — free users lose 55% of certificates.
|
|
574
|
-
if (subStatus === "free") {
|
|
575
|
-
for (let i = userEvents.length - 1; i >= 0; i--) {
|
|
576
|
-
if (userEvents[i].event === "certificate earned" && chance.bool({ likelihood: FREE_CERT_DROP_LIKELIHOOD })) {
|
|
577
|
-
userEvents.splice(i, 1);
|
|
578
|
-
}
|
|
579
|
-
}
|
|
580
|
-
}
|
|
581
|
-
|
|
582
|
-
// H4: STUDY GROUP RETENTION — non-joiners with low scores lose
|
|
583
|
-
// all post-day-14 events. Joiners get extra cloned discussion events.
|
|
584
|
-
if (!joinedStudyGroupEarly && hasLowQuizScore) {
|
|
585
|
-
const churnCutoff = firstEventTime ? firstEventTime.add(STUDY_GROUP_CHURN_CUTOFF_DAYS, 'days') : null;
|
|
586
|
-
for (let i = userEvents.length - 1; i >= 0; i--) {
|
|
587
|
-
if (churnCutoff && dayjs(userEvents[i].time).isAfter(churnCutoff)) {
|
|
588
|
-
userEvents.splice(i, 1);
|
|
589
|
-
}
|
|
590
|
-
}
|
|
591
|
-
} else if (joinedStudyGroupEarly) {
|
|
592
|
-
const lastEvent = userEvents[userEvents.length - 1];
|
|
593
|
-
const discussionTemplate = userEvents.find(e => e.event === "discussion posted");
|
|
594
|
-
if (lastEvent && discussionTemplate && chance.bool({ likelihood: STUDY_GROUP_DISCUSSION_CLONE_LIKELIHOOD })) {
|
|
595
|
-
userEvents.push({
|
|
596
|
-
...discussionTemplate,
|
|
597
|
-
time: dayjs(lastEvent.time).add(chance.integer({ min: 1, max: 3 }), 'days').toISOString(),
|
|
598
|
-
user_id: lastEvent.user_id,
|
|
599
|
-
course_id: chance.pickone(courseIds),
|
|
600
|
-
post_type: chance.pickone(["question", "answer", "comment"]),
|
|
601
|
-
word_count: chance.integer({ min: 20, max: 400 }),
|
|
602
|
-
});
|
|
603
|
-
}
|
|
604
|
-
}
|
|
605
|
-
|
|
606
|
-
// H2: DEADLINE CRAMMING — Sun/Mon assignment_submitted events
|
|
607
|
-
// flip is_late to true 60% of the time and quiz_completed score_percent
|
|
608
|
-
// drops 25 points. Mutates raw is_late + score_percent.
|
|
609
|
-
for (const event of userEvents) {
|
|
610
|
-
if (event.event === "assignment submitted" && event.time) {
|
|
611
|
-
const dow = new Date(event.time).getUTCDay();
|
|
612
|
-
if (dow === 0 || dow === 1) {
|
|
613
|
-
event.is_late = chance.bool({ likelihood: DEADLINE_LATE_LIKELIHOOD });
|
|
614
|
-
}
|
|
615
|
-
}
|
|
616
|
-
}
|
|
617
|
-
userEvents.forEach((event) => {
|
|
618
|
-
if (event.event === "quiz completed" && event.time) {
|
|
619
|
-
const dow = new Date(event.time).getUTCDay();
|
|
620
|
-
if ((dow === 0 || dow === 1) && event.score_percent !== undefined) {
|
|
621
|
-
event.score_percent = Math.max(0, event.score_percent - DEADLINE_QUIZ_PENALTY);
|
|
622
|
-
}
|
|
623
|
-
}
|
|
624
|
-
});
|
|
625
|
-
|
|
626
|
-
return record;
|
|
627
|
-
}
|
|
628
|
-
|
|
629
|
-
// ── CONFIG ──
|
|
630
|
-
/** @type {Config} */
|
|
631
|
-
const config = {
|
|
632
|
-
version: 2,
|
|
633
|
-
seed: SEED,
|
|
634
|
-
datasetStart: DATASET_START,
|
|
635
|
-
datasetEnd: DATASET_END,
|
|
636
|
-
avgEventsPerUserPerDay: EVENTS_PER_DAY,
|
|
637
|
-
numUsers: NUM_USERS,
|
|
638
|
-
format: "json",
|
|
639
|
-
gzip: true,
|
|
640
|
-
credentials: {
|
|
641
|
-
token,
|
|
642
|
-
},
|
|
643
|
-
switches: {
|
|
644
|
-
hasSessionIds: true,
|
|
645
|
-
alsoInferFunnels: false,
|
|
646
|
-
hasLocation: true,
|
|
647
|
-
hasAndroidDevices: true,
|
|
648
|
-
hasIOSDevices: true,
|
|
649
|
-
hasDesktopDevices: true,
|
|
650
|
-
hasBrowser: false,
|
|
651
|
-
hasCampaigns: false,
|
|
652
|
-
isAnonymous: false,
|
|
653
|
-
hasAdSpend: false,
|
|
654
|
-
hasAvatar: true,
|
|
655
|
-
},
|
|
656
|
-
identity: {
|
|
657
|
-
avgDevicePerUser: 2,
|
|
658
|
-
},
|
|
659
|
-
concurrency: 1,
|
|
660
|
-
writeToDisk: false,
|
|
661
|
-
|
|
662
|
-
funnels: [
|
|
663
|
-
{
|
|
664
|
-
sequence: ["account registered", "course enrolled", "lecture started"],
|
|
665
|
-
isFirstFunnel: true,
|
|
666
|
-
conversionRate: 75,
|
|
667
|
-
timeToConvert: 1,
|
|
668
|
-
},
|
|
669
|
-
{
|
|
670
|
-
// Core learning loop: students watch lectures and do practice problems constantly
|
|
671
|
-
sequence: ["lecture started", "lecture completed", "practice problem solved"],
|
|
672
|
-
conversionRate: 70,
|
|
673
|
-
timeToConvert: 4,
|
|
674
|
-
weight: 5,
|
|
675
|
-
reentry: true,
|
|
676
|
-
},
|
|
677
|
-
{
|
|
678
|
-
// Assessment flow: quizzes and assignments after studying
|
|
679
|
-
sequence: ["quiz started", "quiz completed", "assignment submitted"],
|
|
680
|
-
conversionRate: 55,
|
|
681
|
-
timeToConvert: 8,
|
|
682
|
-
weight: 3,
|
|
683
|
-
reentry: true,
|
|
684
|
-
},
|
|
685
|
-
{
|
|
686
|
-
// Course completion journey: enroll → complete → earn certificate
|
|
687
|
-
sequence: ["course enrolled", "lecture completed", "quiz completed", "certificate earned"],
|
|
688
|
-
conversionRate: 30,
|
|
689
|
-
timeToConvert: 48,
|
|
690
|
-
weight: 2,
|
|
691
|
-
},
|
|
692
|
-
{
|
|
693
|
-
// Social learning: discussions and study groups
|
|
694
|
-
sequence: ["discussion posted", "study group joined", "resource downloaded"],
|
|
695
|
-
conversionRate: 50,
|
|
696
|
-
timeToConvert: 12,
|
|
697
|
-
weight: 2,
|
|
698
|
-
experiment: {
|
|
699
|
-
name: "AI Study Buddy",
|
|
700
|
-
variants: [
|
|
701
|
-
{ name: "Control" },
|
|
702
|
-
{ name: "AI Study Buddy", conversionMultiplier: 1.4, ttcMultiplier: 0.85 },
|
|
703
|
-
],
|
|
704
|
-
startDaysBeforeEnd: 30,
|
|
705
|
-
},
|
|
706
|
-
},
|
|
707
|
-
{
|
|
708
|
-
// Instructor interaction loop
|
|
709
|
-
sequence: ["assignment submitted", "assignment graded", "instructor feedback given"],
|
|
710
|
-
conversionRate: 45,
|
|
711
|
-
timeToConvert: 24,
|
|
712
|
-
weight: 2,
|
|
713
|
-
},
|
|
714
|
-
{
|
|
715
|
-
// Support and monetization
|
|
716
|
-
sequence: ["help requested", "subscription purchased", "course reviewed"],
|
|
717
|
-
conversionRate: 35,
|
|
718
|
-
timeToConvert: 24,
|
|
719
|
-
weight: 1,
|
|
720
|
-
},
|
|
721
|
-
],
|
|
722
|
-
|
|
723
|
-
events: [
|
|
724
|
-
{
|
|
725
|
-
event: "account registered",
|
|
726
|
-
weight: 1,
|
|
727
|
-
isFirstEvent: true,
|
|
728
|
-
isAuthEvent: true,
|
|
729
|
-
properties: {
|
|
730
|
-
"account_type": ["instructor", "instructor", "instructor", "instructor", "instructor", "instructor", "student"],
|
|
731
|
-
"signup_source": ["organic", "referral", "school_partnership", "social_ad"],
|
|
732
|
-
}
|
|
733
|
-
},
|
|
734
|
-
{
|
|
735
|
-
event: "course enrolled",
|
|
736
|
-
weight: 8,
|
|
737
|
-
isStrictEvent: false,
|
|
738
|
-
properties: {
|
|
739
|
-
"course_id": courseIds,
|
|
740
|
-
"course_category": ["CS", "Math", "Science", "Business", "Arts", "Languages"],
|
|
741
|
-
"difficulty": ["beginner", "intermediate", "advanced"],
|
|
742
|
-
"is_free": [false, false, false, true, true],
|
|
743
|
-
}
|
|
744
|
-
},
|
|
745
|
-
{
|
|
746
|
-
event: "lecture started",
|
|
747
|
-
weight: 18,
|
|
748
|
-
properties: {
|
|
749
|
-
"course_id": courseIds,
|
|
750
|
-
"lecture_id": lectureIds,
|
|
751
|
-
"lecture_duration_mins": u.weighNumRange(5, 60, 0.8, 20),
|
|
752
|
-
"module_number": u.weighNumRange(1, 12),
|
|
753
|
-
}
|
|
754
|
-
},
|
|
755
|
-
{
|
|
756
|
-
event: "lecture completed",
|
|
757
|
-
weight: 14,
|
|
758
|
-
isStrictEvent: false,
|
|
759
|
-
properties: {
|
|
760
|
-
"course_id": courseIds,
|
|
761
|
-
"lecture_id": lectureIds,
|
|
762
|
-
"watch_time_mins": u.weighNumRange(3, 60, 0.8, 20),
|
|
763
|
-
"playback_speed": [0.75, 1.0, 1.0, 1.0, 1.25, 1.5, 2.0],
|
|
764
|
-
"notes_taken": [false, false, true],
|
|
765
|
-
}
|
|
766
|
-
},
|
|
767
|
-
{
|
|
768
|
-
event: "quiz started",
|
|
769
|
-
weight: 10,
|
|
770
|
-
isStrictEvent: false,
|
|
771
|
-
properties: {
|
|
772
|
-
"course_id": courseIds,
|
|
773
|
-
"quiz_id": quizIds,
|
|
774
|
-
"quiz_type": ["practice", "graded", "final_exam"],
|
|
775
|
-
"question_count": u.weighNumRange(5, 50, 0.7, 15),
|
|
776
|
-
}
|
|
777
|
-
},
|
|
778
|
-
{
|
|
779
|
-
event: "quiz completed",
|
|
780
|
-
weight: 8,
|
|
781
|
-
isStrictEvent: false,
|
|
782
|
-
properties: {
|
|
783
|
-
"course_id": courseIds,
|
|
784
|
-
"quiz_id": quizIds,
|
|
785
|
-
"score_percent": u.weighNumRange(0, 100, 1.2, 50),
|
|
786
|
-
"time_spent_mins": u.weighNumRange(3, 120, 0.6, 25),
|
|
787
|
-
"attempts": u.weighNumRange(1, 5, 0.5, 3),
|
|
788
|
-
}
|
|
789
|
-
},
|
|
790
|
-
{
|
|
791
|
-
event: "assignment submitted",
|
|
792
|
-
weight: 6,
|
|
793
|
-
isStrictEvent: false,
|
|
794
|
-
properties: {
|
|
795
|
-
"course_id": courseIds,
|
|
796
|
-
"assignment_id": assignmentIds,
|
|
797
|
-
"submission_type": ["text", "code", "file", "project"],
|
|
798
|
-
"word_count": u.weighNumRange(100, 5000, 0.6, 500),
|
|
799
|
-
"is_late": [false, false, false, false, true],
|
|
800
|
-
}
|
|
801
|
-
},
|
|
802
|
-
{
|
|
803
|
-
event: "assignment graded",
|
|
804
|
-
weight: 5,
|
|
805
|
-
properties: {
|
|
806
|
-
"course_id": courseIds,
|
|
807
|
-
"assignment_id": assignmentIds,
|
|
808
|
-
"grade": ["A", "B", "C", "D", "F"],
|
|
809
|
-
"feedback_length": u.weighNumRange(0, 500, 0.5, 100),
|
|
810
|
-
"grader": ["instructor", "peer", "auto"],
|
|
811
|
-
}
|
|
812
|
-
},
|
|
813
|
-
{
|
|
814
|
-
event: "discussion posted",
|
|
815
|
-
weight: 7,
|
|
816
|
-
isStrictEvent: false,
|
|
817
|
-
properties: {
|
|
818
|
-
"course_id": courseIds,
|
|
819
|
-
"post_type": ["question", "answer", "comment"],
|
|
820
|
-
"word_count": u.weighNumRange(10, 500, 0.6, 80),
|
|
821
|
-
}
|
|
822
|
-
},
|
|
823
|
-
{
|
|
824
|
-
event: "certificate earned",
|
|
825
|
-
weight: 2,
|
|
826
|
-
isStrictEvent: false,
|
|
827
|
-
properties: {
|
|
828
|
-
"course_id": courseIds,
|
|
829
|
-
"completion_time_days": u.weighNumRange(7, 180, 0.5, 45),
|
|
830
|
-
"final_grade": u.weighNumRange(60, 100, 1.2, 30),
|
|
831
|
-
}
|
|
832
|
-
},
|
|
833
|
-
{
|
|
834
|
-
event: "study group joined",
|
|
835
|
-
weight: 4,
|
|
836
|
-
isStrictEvent: false,
|
|
837
|
-
properties: {
|
|
838
|
-
"group_id": groupIds,
|
|
839
|
-
"group_size": u.weighNumRange(3, 20, 0.7, 8),
|
|
840
|
-
"group_type": ["study_circle", "project_team", "tutoring"],
|
|
841
|
-
}
|
|
842
|
-
},
|
|
843
|
-
{
|
|
844
|
-
event: "resource downloaded",
|
|
845
|
-
weight: 9,
|
|
846
|
-
properties: {
|
|
847
|
-
"resource_type": ["pdf", "slides", "code_sample", "dataset", "cheat_sheet"],
|
|
848
|
-
"course_id": courseIds,
|
|
849
|
-
}
|
|
850
|
-
},
|
|
851
|
-
{
|
|
852
|
-
event: "instructor feedback given",
|
|
853
|
-
weight: 3,
|
|
854
|
-
properties: {
|
|
855
|
-
"course_id": courseIds,
|
|
856
|
-
"feedback_type": ["written", "video", "rubric"],
|
|
857
|
-
"response_time_hours": u.weighNumRange(1, 72, 0.5, 15),
|
|
858
|
-
}
|
|
859
|
-
},
|
|
860
|
-
{
|
|
861
|
-
event: "course reviewed",
|
|
862
|
-
weight: 3,
|
|
863
|
-
properties: {
|
|
864
|
-
"course_id": courseIds,
|
|
865
|
-
"rating": u.weighNumRange(1, 5, 1.5, 3),
|
|
866
|
-
"review_length": u.weighNumRange(10, 1000, 0.5, 100),
|
|
867
|
-
"would_recommend": [false, false, false, true, true, true, true, true, true, true],
|
|
868
|
-
}
|
|
869
|
-
},
|
|
870
|
-
{
|
|
871
|
-
event: "subscription purchased",
|
|
872
|
-
weight: 2,
|
|
873
|
-
properties: {
|
|
874
|
-
"plan": ["monthly", "annual", "lifetime"],
|
|
875
|
-
"price": [19.99, 149.99, 499.99],
|
|
876
|
-
}
|
|
877
|
-
},
|
|
878
|
-
{
|
|
879
|
-
event: "help requested",
|
|
880
|
-
weight: 4,
|
|
881
|
-
properties: {
|
|
882
|
-
"topic": ["technical", "content", "billing", "accessibility"],
|
|
883
|
-
"channel": ["chat", "email", "forum"],
|
|
884
|
-
}
|
|
885
|
-
},
|
|
886
|
-
{
|
|
887
|
-
event: "practice problem solved",
|
|
888
|
-
weight: 12,
|
|
889
|
-
isStrictEvent: false,
|
|
890
|
-
properties: {
|
|
891
|
-
"course_id": courseIds,
|
|
892
|
-
"problem_id": problemIds,
|
|
893
|
-
"difficulty": ["easy", "medium", "hard"],
|
|
894
|
-
"time_to_solve_sec": u.weighNumRange(10, 3600, 0.5, 300),
|
|
895
|
-
"hint_used": [false, false, true],
|
|
896
|
-
}
|
|
897
|
-
},
|
|
898
|
-
],
|
|
899
|
-
|
|
900
|
-
superProps: {
|
|
901
|
-
Platform: ["Web", "iOS", "Android", "iPad"],
|
|
902
|
-
},
|
|
903
|
-
|
|
904
|
-
scdProps: {
|
|
905
|
-
enrollment_status: {
|
|
906
|
-
values: ["enrolled", "active", "completed", "dropped"],
|
|
907
|
-
frequency: "month",
|
|
908
|
-
timing: "fuzzy",
|
|
909
|
-
max: 6
|
|
910
|
-
},
|
|
911
|
-
course_status: {
|
|
912
|
-
values: ["draft", "published", "archived", "deprecated"],
|
|
913
|
-
frequency: "month",
|
|
914
|
-
timing: "fixed",
|
|
915
|
-
max: 6,
|
|
916
|
-
type: "course_id"
|
|
917
|
-
}
|
|
918
|
-
},
|
|
919
|
-
|
|
920
|
-
userProps: {
|
|
921
|
-
"account_type": ["student", "student", "student", "student", "student", "student", "student", "student", "instructor"],
|
|
922
|
-
"subscription_status": ["free", "free", "free", "monthly", "annual"],
|
|
923
|
-
"learning_style": ["visual", "reading", "hands_on", "auditory"],
|
|
924
|
-
"education_level": ["high_school", "bachelors", "masters", "phd", "self_taught"],
|
|
925
|
-
"timezone": ["US_Eastern", "US_Pacific", "US_Central", "Europe", "Asia"],
|
|
926
|
-
"courses_created": [0],
|
|
927
|
-
"teaching_experience_years": [0],
|
|
928
|
-
"instructor_rating": [0],
|
|
929
|
-
"learning_goal": ["none"],
|
|
930
|
-
"study_hours_per_week": [0],
|
|
931
|
-
"Platform": ["Web", "iOS", "Android", "iPad"],
|
|
932
|
-
},
|
|
933
|
-
|
|
934
|
-
groupKeys: [
|
|
935
|
-
["course_id", 150, ["course enrolled", "lecture started", "lecture completed", "quiz completed", "certificate earned"]],
|
|
936
|
-
["group_id", 300, ["study group joined", "discussion posted"]],
|
|
937
|
-
],
|
|
938
|
-
|
|
939
|
-
groupProps: {
|
|
940
|
-
course_id: {
|
|
941
|
-
"title": () => `${chance.pickone(["Introduction to", "Advanced", "Mastering", "Fundamentals of", "Applied"])} ${chance.pickone(["Algorithms", "Data Science", "Machine Learning", "Statistics", "Web Development", "Calculus", "Biology", "Economics", "Design Thinking", "Creative Writing"])}`,
|
|
942
|
-
"instructor_count": u.weighNumRange(1, 5, 0.5, 2),
|
|
943
|
-
"total_enrolled": u.weighNumRange(50, 5000, 0.6, 500),
|
|
944
|
-
"avg_rating": u.weighNumRange(3, 5, 1.5, 1),
|
|
945
|
-
},
|
|
946
|
-
group_id: {
|
|
947
|
-
"name": () => `${chance.pickone(["Study", "Learning", "Focus", "Peer", "Cohort"])} ${chance.pickone(["Circle", "Squad", "Team", "Hub", "Group"])} ${chance.character({ alpha: true, casing: "upper" })}${chance.integer({ min: 1, max: 99 })}`,
|
|
948
|
-
"member_count": u.weighNumRange(3, 20, 0.7, 8),
|
|
949
|
-
"focus_area": ["CS", "Math", "Science", "Business", "Arts", "Languages"],
|
|
950
|
-
}
|
|
951
|
-
},
|
|
952
|
-
|
|
953
|
-
lookupTables: [],
|
|
954
|
-
|
|
955
|
-
hook(record, type, meta) {
|
|
956
|
-
if (type === "user") return handleUserHooks(record);
|
|
957
|
-
if (type === "event") return handleEventHooks(record);
|
|
958
|
-
if (type === "funnel-pre") return handleFunnelPreHooks(record, meta);
|
|
959
|
-
if (type === "everything") return handleEverythingHooks(record, meta);
|
|
960
|
-
return record;
|
|
961
|
-
}
|
|
962
|
-
};
|
|
963
|
-
|
|
964
|
-
export default config;
|