@ak--47/dungeon-master 1.0.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/README.md +518 -0
- package/dungeons/array-of-object-lookup-schema.json +327 -0
- package/dungeons/array-of-object-lookup.js +220 -0
- package/dungeons/ecommerce-schema.json +462 -0
- package/dungeons/ecommerce.js +447 -0
- package/dungeons/education-schema.json +2409 -0
- package/dungeons/education.js +768 -0
- package/dungeons/fintech-schema.json +14034 -0
- package/dungeons/fintech.js +696 -0
- package/dungeons/foobar-schema.json +403 -0
- package/dungeons/foobar.js +296 -0
- package/dungeons/food-delivery-schema.json +192 -0
- package/dungeons/food-delivery.js +602 -0
- package/dungeons/food-schema.json +1152 -0
- package/dungeons/food.js +754 -0
- package/dungeons/gaming-schema.json +1270 -0
- package/dungeons/gaming.js +508 -0
- package/dungeons/insurance-application-schema.json +204 -0
- package/dungeons/insurance-application.js +605 -0
- package/dungeons/media-schema.json +906 -0
- package/dungeons/media.js +790 -0
- package/dungeons/retention-cadence-schema.json +78 -0
- package/dungeons/retention-cadence.js +244 -0
- package/dungeons/rpg-schema.json +4526 -0
- package/dungeons/rpg.js +919 -0
- package/dungeons/sanity-schema.json +255 -0
- package/dungeons/sanity.js +152 -0
- package/dungeons/sass-schema.json +1291 -0
- package/dungeons/sass.js +795 -0
- package/dungeons/scd-schema.json +919 -0
- package/dungeons/scd.js +277 -0
- package/dungeons/simple-schema.json +608 -0
- package/dungeons/simple.js +285 -0
- package/dungeons/simplest-schema.json +1418 -0
- package/dungeons/simplest.js +392 -0
- package/dungeons/social-schema.json +1118 -0
- package/dungeons/social.js +686 -0
- package/dungeons/text-generation-schema.json +3096 -0
- package/dungeons/text-generation.js +812 -0
- package/index.js +567 -0
- package/lib/core/config-validator.js +395 -0
- package/lib/core/context.js +204 -0
- package/lib/core/dungeon-loader.js +337 -0
- package/lib/core/storage.js +379 -0
- package/lib/generators/adspend.js +132 -0
- package/lib/generators/events.js +271 -0
- package/lib/generators/funnels.js +407 -0
- package/lib/generators/mirror.js +167 -0
- package/lib/generators/product-lookup.js +262 -0
- package/lib/generators/product-names.js +195 -0
- package/lib/generators/profiles.js +93 -0
- package/lib/generators/scd.js +124 -0
- package/lib/generators/text.js +1192 -0
- package/lib/orchestrators/mixpanel-sender.js +266 -0
- package/lib/orchestrators/user-loop.js +335 -0
- package/lib/templates/abbreviated.d.ts +169 -0
- package/lib/templates/defaults.js +1405 -0
- package/lib/templates/phrases.js +2526 -0
- package/lib/templates/schema.d.ts +173 -0
- package/lib/templates/soup-presets.js +188 -0
- package/lib/utils/function-registry.js +302 -0
- package/lib/utils/json-evaluator.js +172 -0
- package/lib/utils/logger.js +34 -0
- package/lib/utils/utils.js +1490 -0
- package/package.json +89 -0
- package/types.d.ts +865 -0
|
@@ -0,0 +1,768 @@
|
|
|
1
|
+
import dayjs from "dayjs";
|
|
2
|
+
import utc from "dayjs/plugin/utc.js";
|
|
3
|
+
import "dotenv/config";
|
|
4
|
+
import * as u from "../lib/utils/utils.js";
|
|
5
|
+
import * as v from "ak-tools";
|
|
6
|
+
|
|
7
|
+
const SEED = "harness-education";
|
|
8
|
+
dayjs.extend(utc);
|
|
9
|
+
const chance = u.initChance(SEED);
|
|
10
|
+
const num_users = 5_000;
|
|
11
|
+
const days = 100;
|
|
12
|
+
|
|
13
|
+
/** @typedef {import("../types.d.ts").Dungeon} Config */
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* ═══════════════════════════════════════════════════════════════════════════════
|
|
17
|
+
* DATASET OVERVIEW — LearnPath eLearning Platform
|
|
18
|
+
* ═══════════════════════════════════════════════════════════════════════════════
|
|
19
|
+
*
|
|
20
|
+
* An online learning platform modeled after Coursera, Khan Academy, and Udemy.
|
|
21
|
+
* Supports self-paced and cohort-based learning with courses, quizzes,
|
|
22
|
+
* assignments, and social study features.
|
|
23
|
+
*
|
|
24
|
+
* Scale: 5,000 users / 600K events / 100 days / 17 event types
|
|
25
|
+
*
|
|
26
|
+
* CORE LOOP:
|
|
27
|
+
* Register → browse/enroll in courses → watch lectures → practice problems →
|
|
28
|
+
* quizzes/assignments → certificate earned. Social layer (study groups,
|
|
29
|
+
* discussions) drives retention. Subscription tiers (free/monthly/annual)
|
|
30
|
+
* gate completion rates.
|
|
31
|
+
*
|
|
32
|
+
* FUNNELS:
|
|
33
|
+
* - Onboarding: account registered → course enrolled → lecture started
|
|
34
|
+
* - Learning loop: lecture started → lecture completed → practice problem solved
|
|
35
|
+
* - Assessment: quiz started → quiz completed → assignment submitted
|
|
36
|
+
* - Course completion: course enrolled → lecture completed → quiz completed → certificate earned
|
|
37
|
+
* - Social learning: discussion posted → study group joined → resource downloaded
|
|
38
|
+
* - Instructor interaction: assignment submitted → assignment graded → instructor feedback given
|
|
39
|
+
* - Support/monetization: help requested → subscription purchased → course reviewed
|
|
40
|
+
*
|
|
41
|
+
* GROUPS: course_id (150 courses), group_id (300 study groups)
|
|
42
|
+
* SUBSCRIPTIONS: free (~60%), monthly, annual
|
|
43
|
+
* ACCOUNT TYPES: ~89% students, ~11% instructors (two-sided marketplace)
|
|
44
|
+
*/
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* ═══════════════════════════════════════════════════════════════════════════════
|
|
48
|
+
* ANALYTICS HOOKS (8 architected patterns)
|
|
49
|
+
* ═══════════════════════════════════════════════════════════════════════════════
|
|
50
|
+
*
|
|
51
|
+
* 1. STUDENT VS INSTRUCTOR PROFILES
|
|
52
|
+
* Instructor profiles get teaching attributes (courses_created,
|
|
53
|
+
* teaching_experience_years, instructor_rating). Students get learning
|
|
54
|
+
* attributes (learning_goal, study_hours_per_week).
|
|
55
|
+
*
|
|
56
|
+
* Mixpanel reports:
|
|
57
|
+
* • Insights → Any event → Unique users → Breakdown: "account_type"
|
|
58
|
+
* Expected: ~89% students, ~11% instructors
|
|
59
|
+
* • Insights → "instructor feedback given" → Total per user → Breakdown: "account_type"
|
|
60
|
+
* Expected: Instructors dominate feedback; students show learning_goal instead
|
|
61
|
+
*
|
|
62
|
+
* 2. DEADLINE CRAMMING
|
|
63
|
+
* Assignments submitted on Sun/Mon are rushed: 60% late (vs ~20% baseline),
|
|
64
|
+
* quiz scores drop by 25 points. Events carry is_deadline_rush: true.
|
|
65
|
+
*
|
|
66
|
+
* Mixpanel reports:
|
|
67
|
+
* • Insights → "assignment submitted" → Total → Breakdown: "is_deadline_rush"
|
|
68
|
+
* Expected: is_deadline_rush=true shows ~60% late rate vs ~20% baseline
|
|
69
|
+
* • Insights → "quiz completed" → Avg "score_percent" → Breakdown: Day of Week
|
|
70
|
+
* Expected: Sun/Mon scores ~25 points lower (~40 vs ~65)
|
|
71
|
+
*
|
|
72
|
+
* 3. NOTES-TAKERS SUCCEED
|
|
73
|
+
* Students with 5+ notes_taken=true lectures get +20 quiz score boost
|
|
74
|
+
* (capped at 100) and 40% chance of bonus certificate. Marked diligent_student: true.
|
|
75
|
+
*
|
|
76
|
+
* Mixpanel reports:
|
|
77
|
+
* • Insights → "quiz completed" → Avg "score_percent" → Breakdown: "diligent_student"
|
|
78
|
+
* Expected: diligent_student=true ≈ 85 avg vs ~65 baseline (+20 pts)
|
|
79
|
+
* • Insights → "certificate earned" → Total per user → Breakdown: "diligent_student"
|
|
80
|
+
* Expected: diligent_student=true earn ~40% more certificates
|
|
81
|
+
*
|
|
82
|
+
* 4. STUDY GROUP RETENTION
|
|
83
|
+
* Early study group joiners (within 10 days) retain and get bonus discussions.
|
|
84
|
+
* Non-joiners with low quiz scores (<60) churn hard at day 14 (all later events removed).
|
|
85
|
+
*
|
|
86
|
+
* Mixpanel reports:
|
|
87
|
+
* • Retention → A: "account registered" → B: Any event → Segment by early study group join
|
|
88
|
+
* Expected: Early joiners ~90% D14 retention; non-joiners with low scores ~30%
|
|
89
|
+
* • Insights → "discussion posted" → Total per user → Breakdown: "study_group_member"
|
|
90
|
+
* Expected: study_group_member=true users post more
|
|
91
|
+
*
|
|
92
|
+
* 5. HINT DEPENDENCY
|
|
93
|
+
* Hint users get 60% chance of easy problems; non-hint users get 40% chance of
|
|
94
|
+
* hard problems with independent_solver: true.
|
|
95
|
+
*
|
|
96
|
+
* Mixpanel reports:
|
|
97
|
+
* • Insights → "practice problem solved" → Total → Breakdown: "difficulty" → Filter: hint_used=true
|
|
98
|
+
* Expected: ~60% easy (vs ~33% baseline)
|
|
99
|
+
* • Insights → "practice problem solved" → Total → Breakdown: "difficulty" → Filter: hint_used=false
|
|
100
|
+
* Expected: ~40% hard (vs ~33% baseline)
|
|
101
|
+
*
|
|
102
|
+
* 6. SEMESTER-END SPIKE
|
|
103
|
+
* Days 75-85: quiz_started, quiz_completed, assignment_submitted events duplicated
|
|
104
|
+
* at 80% rate. Events carry semester_end_rush: true.
|
|
105
|
+
*
|
|
106
|
+
* Mixpanel reports:
|
|
107
|
+
* • Insights (line) → "quiz started" + "quiz completed" + "assignment submitted" → Daily
|
|
108
|
+
* Expected: ~2x volume spike during days 75-85
|
|
109
|
+
* • Insights → "quiz completed" → Total → Breakdown: "semester_end_rush"
|
|
110
|
+
* Expected: semester_end_rush=true clusters in days 75-85
|
|
111
|
+
*
|
|
112
|
+
* 7. FREE VS PAID COURSES
|
|
113
|
+
* Free users get 0.5x funnel conversion rate; paid subscribers get 1.5x.
|
|
114
|
+
* Free users also lose 55% of certificates. Creates ~2.2x completion gap.
|
|
115
|
+
*
|
|
116
|
+
* Mixpanel reports:
|
|
117
|
+
* • Funnels → "course enrolled" → "lecture completed" → "quiz completed" → "certificate earned"
|
|
118
|
+
* Breakdown: "subscription_status"
|
|
119
|
+
* Expected: free ≈ 15% completion, paid ≈ 33% (~2.2x difference)
|
|
120
|
+
* • Insights → "certificate earned" → Total per user → Breakdown: "subscription_status"
|
|
121
|
+
* Expected: Paid subscribers earn significantly more certificates
|
|
122
|
+
*
|
|
123
|
+
* 8. PLAYBACK SPEED CORRELATION
|
|
124
|
+
* Speed learners (>=2.0x, 3+ lectures): compressed watch_time (0.6x),
|
|
125
|
+
* paradoxically higher quiz scores (+8 pts). Thorough learners (<=1.0x):
|
|
126
|
+
* extended watch_time (1.4x).
|
|
127
|
+
*
|
|
128
|
+
* Mixpanel reports:
|
|
129
|
+
* • Insights → "lecture completed" → Avg "watch_time_mins" → Breakdown: "speed_learner"
|
|
130
|
+
* Expected: speed_learner=true ≈ 0.6x watch time
|
|
131
|
+
* • Insights → "quiz completed" → Avg "score_percent" → Breakdown: "speed_learner_effect"
|
|
132
|
+
* Expected: speed_learner_effect=true shows +8 points (faster = better)
|
|
133
|
+
*
|
|
134
|
+
* ═══════════════════════════════════════════════════════════════════════════════
|
|
135
|
+
* ADVANCED ANALYSIS IDEAS
|
|
136
|
+
* ═══════════════════════════════════════════════════════════════════════════════
|
|
137
|
+
*
|
|
138
|
+
* CROSS-HOOK PATTERNS:
|
|
139
|
+
* - The Ideal Student: notes (H3) + study groups (H4) + no hints (H5) + paid (H7) + speed (H8)
|
|
140
|
+
* - Cramming Cascade: deadline crammers (H2) compounded with semester-end spike (H6)?
|
|
141
|
+
* - Social Safety Net: does early study group joining (H4) prevent churn for low scorers?
|
|
142
|
+
* - Hint-to-Mastery: do hint-dependent (H5) students who join groups (H4) wean off hints?
|
|
143
|
+
* - Payment + Notes: are paid subscribers (H7) more likely to take notes (H3)?
|
|
144
|
+
*
|
|
145
|
+
* COHORT ANALYSIS:
|
|
146
|
+
* - By education level: PhD vs self-taught hook patterns
|
|
147
|
+
* - By learning style: visual vs hands-on note-taking rates
|
|
148
|
+
* - By platform: mobile vs desktop playback speed preferences
|
|
149
|
+
* - By course category: CS vs Arts hint usage
|
|
150
|
+
*
|
|
151
|
+
* FUNNEL ANALYSIS:
|
|
152
|
+
* - Onboarding by account_type
|
|
153
|
+
* - Course completion by subscription, notes, study groups
|
|
154
|
+
* - Practice mastery by hint usage, speed, learning style
|
|
155
|
+
*
|
|
156
|
+
* ═══════════════════════════════════════════════════════════════════════════════
|
|
157
|
+
* EXPECTED METRICS SUMMARY
|
|
158
|
+
* ═══════════════════════════════════════════════════════════════════════════════
|
|
159
|
+
*
|
|
160
|
+
* Hook | Metric | Baseline | Hook Effect | Ratio
|
|
161
|
+
* ────────────────────────|───────────────────────|──────────|──────────────|──────
|
|
162
|
+
* Student vs Instructor | Profile attributes | generic | role-specific| N/A
|
|
163
|
+
* Deadline Cramming | Late submission rate | ~20% | ~60% | 3x
|
|
164
|
+
* Deadline Cramming | Quiz score (Sun/Mon) | ~65 | ~40 | -25pt
|
|
165
|
+
* Notes-Takers Succeed | Quiz score | ~65 | ~85 | +20pt
|
|
166
|
+
* Notes-Takers Succeed | Certificate rate | baseline | +40% | 1.4x
|
|
167
|
+
* Study Group Retention | D14 retention | ~40% | ~90% | 2.3x
|
|
168
|
+
* Study Group Retention | Post-D14 events | 100% | 30% (churn) | 0.3x
|
|
169
|
+
* Hint Dependency | Easy problem rate | ~33% | ~60% | 1.8x
|
|
170
|
+
* Hint Dependency | Hard problem rate | ~33% | ~40% (no hint)| 1.2x
|
|
171
|
+
* Semester-End Spike | Assessment volume | baseline | ~2x | 2x
|
|
172
|
+
* Free vs Paid | Course completion | 15% | 33% | 2.2x
|
|
173
|
+
* Playback Speed | Quiz score (speed) | ~65 | ~73 | +8pt
|
|
174
|
+
*/
|
|
175
|
+
|
|
176
|
+
// Generate consistent IDs for lookup tables and event properties
|
|
177
|
+
const courseIds = v.range(1, 151).map(n => `course_${v.uid(6)}`);
|
|
178
|
+
const quizIds = v.range(1, 401).map(n => `quiz_${v.uid(6)}`);
|
|
179
|
+
const groupIds = v.range(1, 301).map(n => `group_${v.uid(6)}`);
|
|
180
|
+
const lectureIds = v.range(1, 501).map(n => `lecture_${v.uid(6)}`);
|
|
181
|
+
const assignmentIds = v.range(1, 201).map(n => `assignment_${v.uid(6)}`);
|
|
182
|
+
const problemIds = v.range(1, 601).map(n => `problem_${v.uid(6)}`);
|
|
183
|
+
|
|
184
|
+
/** @type {Config} */
|
|
185
|
+
const config = {
|
|
186
|
+
token: "",
|
|
187
|
+
seed: SEED,
|
|
188
|
+
numDays: days,
|
|
189
|
+
numEvents: num_users * 120,
|
|
190
|
+
numUsers: num_users,
|
|
191
|
+
hasAnonIds: false,
|
|
192
|
+
hasSessionIds: true,
|
|
193
|
+
format: "json",
|
|
194
|
+
gzip: true,
|
|
195
|
+
alsoInferFunnels: false,
|
|
196
|
+
hasLocation: true,
|
|
197
|
+
hasAndroidDevices: true,
|
|
198
|
+
hasIOSDevices: true,
|
|
199
|
+
hasDesktopDevices: true,
|
|
200
|
+
hasBrowser: false,
|
|
201
|
+
hasCampaigns: false,
|
|
202
|
+
isAnonymous: false,
|
|
203
|
+
hasAdSpend: false,
|
|
204
|
+
percentUsersBornInDataset: 50,
|
|
205
|
+
hasAvatar: true,
|
|
206
|
+
batchSize: 2_500_000,
|
|
207
|
+
concurrency: 1,
|
|
208
|
+
writeToDisk: false,
|
|
209
|
+
|
|
210
|
+
funnels: [
|
|
211
|
+
{
|
|
212
|
+
sequence: ["account registered", "course enrolled", "lecture started"],
|
|
213
|
+
isFirstFunnel: true,
|
|
214
|
+
conversionRate: 75,
|
|
215
|
+
timeToConvert: 1,
|
|
216
|
+
},
|
|
217
|
+
{
|
|
218
|
+
// Core learning loop: students watch lectures and do practice problems constantly
|
|
219
|
+
sequence: ["lecture started", "lecture completed", "practice problem solved"],
|
|
220
|
+
conversionRate: 70,
|
|
221
|
+
timeToConvert: 4,
|
|
222
|
+
weight: 5,
|
|
223
|
+
},
|
|
224
|
+
{
|
|
225
|
+
// Assessment flow: quizzes and assignments after studying
|
|
226
|
+
sequence: ["quiz started", "quiz completed", "assignment submitted"],
|
|
227
|
+
conversionRate: 55,
|
|
228
|
+
timeToConvert: 8,
|
|
229
|
+
weight: 3,
|
|
230
|
+
},
|
|
231
|
+
{
|
|
232
|
+
// Course completion journey: enroll → complete → earn certificate
|
|
233
|
+
sequence: ["course enrolled", "lecture completed", "quiz completed", "certificate earned"],
|
|
234
|
+
conversionRate: 30,
|
|
235
|
+
timeToConvert: 48,
|
|
236
|
+
weight: 2,
|
|
237
|
+
},
|
|
238
|
+
{
|
|
239
|
+
// Social learning: discussions and study groups
|
|
240
|
+
sequence: ["discussion posted", "study group joined", "resource downloaded"],
|
|
241
|
+
conversionRate: 50,
|
|
242
|
+
timeToConvert: 12,
|
|
243
|
+
weight: 2,
|
|
244
|
+
},
|
|
245
|
+
{
|
|
246
|
+
// Instructor interaction loop
|
|
247
|
+
sequence: ["assignment submitted", "assignment graded", "instructor feedback given"],
|
|
248
|
+
conversionRate: 45,
|
|
249
|
+
timeToConvert: 24,
|
|
250
|
+
weight: 2,
|
|
251
|
+
},
|
|
252
|
+
{
|
|
253
|
+
// Support and monetization
|
|
254
|
+
sequence: ["help requested", "subscription purchased", "course reviewed"],
|
|
255
|
+
conversionRate: 35,
|
|
256
|
+
timeToConvert: 24,
|
|
257
|
+
weight: 1,
|
|
258
|
+
},
|
|
259
|
+
],
|
|
260
|
+
|
|
261
|
+
events: [
|
|
262
|
+
{
|
|
263
|
+
event: "account registered",
|
|
264
|
+
weight: 1,
|
|
265
|
+
isFirstEvent: true,
|
|
266
|
+
properties: {
|
|
267
|
+
"account_type": u.pickAWinner(["student", "instructor"], 0.15),
|
|
268
|
+
"signup_source": ["organic", "referral", "school_partnership", "social_ad"],
|
|
269
|
+
}
|
|
270
|
+
},
|
|
271
|
+
{
|
|
272
|
+
event: "course enrolled",
|
|
273
|
+
weight: 8,
|
|
274
|
+
properties: {
|
|
275
|
+
"course_id": u.pickAWinner(courseIds),
|
|
276
|
+
"course_category": ["CS", "Math", "Science", "Business", "Arts", "Languages"],
|
|
277
|
+
"difficulty": ["beginner", "intermediate", "advanced"],
|
|
278
|
+
"is_free": u.pickAWinner([true, false], 0.4),
|
|
279
|
+
}
|
|
280
|
+
},
|
|
281
|
+
{
|
|
282
|
+
event: "lecture started",
|
|
283
|
+
weight: 18,
|
|
284
|
+
properties: {
|
|
285
|
+
"course_id": u.pickAWinner(courseIds),
|
|
286
|
+
"lecture_id": u.pickAWinner(lectureIds),
|
|
287
|
+
"lecture_duration_mins": u.weighNumRange(5, 60, 0.8, 20),
|
|
288
|
+
"module_number": u.weighNumRange(1, 12),
|
|
289
|
+
}
|
|
290
|
+
},
|
|
291
|
+
{
|
|
292
|
+
event: "lecture completed",
|
|
293
|
+
weight: 14,
|
|
294
|
+
properties: {
|
|
295
|
+
"course_id": u.pickAWinner(courseIds),
|
|
296
|
+
"lecture_id": u.pickAWinner(lectureIds),
|
|
297
|
+
"watch_time_mins": u.weighNumRange(3, 60, 0.8, 20),
|
|
298
|
+
"playback_speed": u.pickAWinner([0.75, 1.0, 1.0, 1.0, 1.25, 1.5, 2.0]),
|
|
299
|
+
"notes_taken": u.pickAWinner([true, false], 0.35),
|
|
300
|
+
}
|
|
301
|
+
},
|
|
302
|
+
{
|
|
303
|
+
event: "quiz started",
|
|
304
|
+
weight: 10,
|
|
305
|
+
properties: {
|
|
306
|
+
"course_id": u.pickAWinner(courseIds),
|
|
307
|
+
"quiz_id": u.pickAWinner(quizIds),
|
|
308
|
+
"quiz_type": ["practice", "graded", "final_exam"],
|
|
309
|
+
"question_count": u.weighNumRange(5, 50, 0.7, 15),
|
|
310
|
+
}
|
|
311
|
+
},
|
|
312
|
+
{
|
|
313
|
+
event: "quiz completed",
|
|
314
|
+
weight: 8,
|
|
315
|
+
properties: {
|
|
316
|
+
"course_id": u.pickAWinner(courseIds),
|
|
317
|
+
"quiz_id": u.pickAWinner(quizIds),
|
|
318
|
+
"score_percent": u.weighNumRange(0, 100, 1.2, 50),
|
|
319
|
+
"time_spent_mins": u.weighNumRange(3, 120, 0.6, 25),
|
|
320
|
+
"attempts": u.weighNumRange(1, 5, 0.5, 3),
|
|
321
|
+
}
|
|
322
|
+
},
|
|
323
|
+
{
|
|
324
|
+
event: "assignment submitted",
|
|
325
|
+
weight: 6,
|
|
326
|
+
properties: {
|
|
327
|
+
"course_id": u.pickAWinner(courseIds),
|
|
328
|
+
"assignment_id": u.pickAWinner(assignmentIds),
|
|
329
|
+
"submission_type": ["text", "code", "file", "project"],
|
|
330
|
+
"word_count": u.weighNumRange(100, 5000, 0.6, 500),
|
|
331
|
+
"is_late": u.pickAWinner([true, false], 0.2),
|
|
332
|
+
}
|
|
333
|
+
},
|
|
334
|
+
{
|
|
335
|
+
event: "assignment graded",
|
|
336
|
+
weight: 5,
|
|
337
|
+
properties: {
|
|
338
|
+
"course_id": u.pickAWinner(courseIds),
|
|
339
|
+
"assignment_id": u.pickAWinner(assignmentIds),
|
|
340
|
+
"grade": ["A", "B", "C", "D", "F"],
|
|
341
|
+
"feedback_length": u.weighNumRange(0, 500, 0.5, 100),
|
|
342
|
+
"grader": ["instructor", "peer", "auto"],
|
|
343
|
+
}
|
|
344
|
+
},
|
|
345
|
+
{
|
|
346
|
+
event: "discussion posted",
|
|
347
|
+
weight: 7,
|
|
348
|
+
properties: {
|
|
349
|
+
"course_id": u.pickAWinner(courseIds),
|
|
350
|
+
"post_type": ["question", "answer", "comment"],
|
|
351
|
+
"word_count": u.weighNumRange(10, 500, 0.6, 80),
|
|
352
|
+
}
|
|
353
|
+
},
|
|
354
|
+
{
|
|
355
|
+
event: "certificate earned",
|
|
356
|
+
weight: 2,
|
|
357
|
+
properties: {
|
|
358
|
+
"course_id": u.pickAWinner(courseIds),
|
|
359
|
+
"completion_time_days": u.weighNumRange(7, 180, 0.5, 45),
|
|
360
|
+
"final_grade": u.weighNumRange(60, 100, 1.2, 30),
|
|
361
|
+
}
|
|
362
|
+
},
|
|
363
|
+
{
|
|
364
|
+
event: "study group joined",
|
|
365
|
+
weight: 4,
|
|
366
|
+
properties: {
|
|
367
|
+
"group_id": u.pickAWinner(groupIds),
|
|
368
|
+
"group_size": u.weighNumRange(3, 20, 0.7, 8),
|
|
369
|
+
"group_type": ["study_circle", "project_team", "tutoring"],
|
|
370
|
+
}
|
|
371
|
+
},
|
|
372
|
+
{
|
|
373
|
+
event: "resource downloaded",
|
|
374
|
+
weight: 9,
|
|
375
|
+
properties: {
|
|
376
|
+
"resource_type": ["pdf", "slides", "code_sample", "dataset", "cheat_sheet"],
|
|
377
|
+
"course_id": u.pickAWinner(courseIds),
|
|
378
|
+
}
|
|
379
|
+
},
|
|
380
|
+
{
|
|
381
|
+
event: "instructor feedback given",
|
|
382
|
+
weight: 3,
|
|
383
|
+
properties: {
|
|
384
|
+
"course_id": u.pickAWinner(courseIds),
|
|
385
|
+
"feedback_type": ["written", "video", "rubric"],
|
|
386
|
+
"response_time_hours": u.weighNumRange(1, 72, 0.5, 15),
|
|
387
|
+
}
|
|
388
|
+
},
|
|
389
|
+
{
|
|
390
|
+
event: "course reviewed",
|
|
391
|
+
weight: 3,
|
|
392
|
+
properties: {
|
|
393
|
+
"course_id": u.pickAWinner(courseIds),
|
|
394
|
+
"rating": u.weighNumRange(1, 5, 1.5, 3),
|
|
395
|
+
"review_length": u.weighNumRange(10, 1000, 0.5, 100),
|
|
396
|
+
"would_recommend": u.pickAWinner([true, false], 0.7),
|
|
397
|
+
}
|
|
398
|
+
},
|
|
399
|
+
{
|
|
400
|
+
event: "subscription purchased",
|
|
401
|
+
weight: 2,
|
|
402
|
+
properties: {
|
|
403
|
+
"plan": ["monthly", "annual", "lifetime"],
|
|
404
|
+
"price": u.pickAWinner([19.99, 149.99, 499.99]),
|
|
405
|
+
}
|
|
406
|
+
},
|
|
407
|
+
{
|
|
408
|
+
event: "help requested",
|
|
409
|
+
weight: 4,
|
|
410
|
+
properties: {
|
|
411
|
+
"topic": ["technical", "content", "billing", "accessibility"],
|
|
412
|
+
"channel": ["chat", "email", "forum"],
|
|
413
|
+
}
|
|
414
|
+
},
|
|
415
|
+
{
|
|
416
|
+
event: "practice problem solved",
|
|
417
|
+
weight: 12,
|
|
418
|
+
properties: {
|
|
419
|
+
"course_id": u.pickAWinner(courseIds),
|
|
420
|
+
"problem_id": u.pickAWinner(problemIds),
|
|
421
|
+
"difficulty": ["easy", "medium", "hard"],
|
|
422
|
+
"time_to_solve_sec": u.weighNumRange(10, 3600, 0.5, 300),
|
|
423
|
+
"hint_used": u.pickAWinner([true, false], 0.35),
|
|
424
|
+
}
|
|
425
|
+
},
|
|
426
|
+
],
|
|
427
|
+
|
|
428
|
+
superProps: {
|
|
429
|
+
platform: ["Web", "iOS", "Android", "iPad"],
|
|
430
|
+
},
|
|
431
|
+
|
|
432
|
+
scdProps: {},
|
|
433
|
+
|
|
434
|
+
userProps: {
|
|
435
|
+
"account_type": u.pickAWinner(["student", "student", "student", "student", "student", "student", "student", "student", "instructor"]),
|
|
436
|
+
"subscription_status": u.pickAWinner(["free", "free", "free", "monthly", "annual"]),
|
|
437
|
+
"learning_style": ["visual", "reading", "hands_on", "auditory"],
|
|
438
|
+
"education_level": ["high_school", "bachelors", "masters", "phd", "self_taught"],
|
|
439
|
+
"timezone": ["US_Eastern", "US_Pacific", "US_Central", "Europe", "Asia"],
|
|
440
|
+
},
|
|
441
|
+
|
|
442
|
+
groupKeys: [
|
|
443
|
+
["course_id", 150, ["course enrolled", "lecture started", "lecture completed", "quiz completed", "certificate earned"]],
|
|
444
|
+
["group_id", 300, ["study group joined", "discussion posted"]],
|
|
445
|
+
],
|
|
446
|
+
|
|
447
|
+
groupProps: {
|
|
448
|
+
course_id: {
|
|
449
|
+
"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"])}`,
|
|
450
|
+
"instructor_count": u.weighNumRange(1, 5, 0.5, 2),
|
|
451
|
+
"total_enrolled": u.weighNumRange(50, 5000, 0.6, 500),
|
|
452
|
+
"avg_rating": u.weighNumRange(3, 5, 1.5, 1),
|
|
453
|
+
},
|
|
454
|
+
group_id: {
|
|
455
|
+
"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 })}`,
|
|
456
|
+
"member_count": u.weighNumRange(3, 20, 0.7, 8),
|
|
457
|
+
"focus_area": ["CS", "Math", "Science", "Business", "Arts", "Languages"],
|
|
458
|
+
}
|
|
459
|
+
},
|
|
460
|
+
|
|
461
|
+
lookupTables: [],
|
|
462
|
+
|
|
463
|
+
/**
|
|
464
|
+
* ARCHITECTED ANALYTICS HOOKS
|
|
465
|
+
*
|
|
466
|
+
* This hook function creates 8 deliberate patterns in the data:
|
|
467
|
+
*
|
|
468
|
+
* 1. STUDENT VS INSTRUCTOR PROFILES: Instructor profiles get teaching attributes; students get learning attributes
|
|
469
|
+
* 2. DEADLINE CRAMMING: Assignments submitted on Sun/Mon are rushed and lower quality
|
|
470
|
+
* 3. NOTES-TAKERS SUCCEED: Students who take notes during lectures score higher on quizzes
|
|
471
|
+
* 4. STUDY GROUP RETENTION: Early study group joiners retain; non-joiners with low scores churn
|
|
472
|
+
* 5. HINT DEPENDENCY: Hint users get locked into easy problems; non-hint users tackle harder ones
|
|
473
|
+
* 6. SEMESTER-END SPIKE: Days 75-85 see doubled assessment activity (cramming period)
|
|
474
|
+
* 7. FREE VS PAID COURSES: Paid subscribers convert through Course Completion funnel at ~2.2x rate
|
|
475
|
+
* 8. PLAYBACK SPEED CORRELATION: Speed learners paradoxically score higher; thorough learners get extended time
|
|
476
|
+
*/
|
|
477
|
+
hook: function (record, type, meta) {
|
|
478
|
+
const NOW = dayjs();
|
|
479
|
+
const DATASET_START = NOW.subtract(days, 'days');
|
|
480
|
+
|
|
481
|
+
// ═══════════════════════════════════════════════════════════════════
|
|
482
|
+
// Hook #1: STUDENT VS INSTRUCTOR PROFILES
|
|
483
|
+
// ═══════════════════════════════════════════════════════════════════
|
|
484
|
+
if (type === "user") {
|
|
485
|
+
if (record.account_type === "instructor") {
|
|
486
|
+
record.courses_created = chance.integer({ min: 1, max: 15 });
|
|
487
|
+
record.teaching_experience_years = chance.integer({ min: 1, max: 20 });
|
|
488
|
+
record.instructor_rating = Math.round((chance.floating({ min: 3.0, max: 5.0 }) + Number.EPSILON) * 100) / 100;
|
|
489
|
+
} else {
|
|
490
|
+
record.learning_goal = chance.pickone(["career_change", "skill_upgrade", "hobby", "degree_requirement"]);
|
|
491
|
+
record.study_hours_per_week = chance.integer({ min: 2, max: 30 });
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
// ═══════════════════════════════════════════════════════════════════
|
|
496
|
+
// Hook #2: DEADLINE CRAMMING
|
|
497
|
+
// ═══════════════════════════════════════════════════════════════════
|
|
498
|
+
if (type === "event") {
|
|
499
|
+
if (record.event === "assignment submitted" && record.time) {
|
|
500
|
+
const eventDay = dayjs(record.time).day(); // 0 = Sunday, 1 = Monday
|
|
501
|
+
if (eventDay === 0 || eventDay === 1) {
|
|
502
|
+
record.is_deadline_rush = true;
|
|
503
|
+
// 60% chance of being late (only 40% on time)
|
|
504
|
+
record.is_late = !chance.bool({ likelihood: 40 });
|
|
505
|
+
} else {
|
|
506
|
+
record.is_deadline_rush = false;
|
|
507
|
+
record.is_late = !chance.bool({ likelihood: 80 });
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
// Quiz score penalty moved to everything hook (after churn removal)
|
|
512
|
+
// to avoid selection bias — the penalty was causing more Sun/Mon
|
|
513
|
+
// quiz-takers to trigger hasLowQuizScore churn, inflating their avg
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
// ═══════════════════════════════════════════════════════════════════
|
|
517
|
+
// Hook #5: HINT DEPENDENCY
|
|
518
|
+
// ═══════════════════════════════════════════════════════════════════
|
|
519
|
+
if (type === "event") {
|
|
520
|
+
if (record.event === "practice problem solved") {
|
|
521
|
+
if (record.hint_used === true) {
|
|
522
|
+
// Hint users gravitate toward easy problems
|
|
523
|
+
if (chance.bool({ likelihood: 60 })) {
|
|
524
|
+
record.difficulty = "easy";
|
|
525
|
+
}
|
|
526
|
+
record.independent_solver = false;
|
|
527
|
+
} else if (record.hint_used === false) {
|
|
528
|
+
// Independent solvers tackle harder problems
|
|
529
|
+
if (chance.bool({ likelihood: 40 })) {
|
|
530
|
+
record.difficulty = "hard";
|
|
531
|
+
record.independent_solver = true;
|
|
532
|
+
} else {
|
|
533
|
+
record.independent_solver = false;
|
|
534
|
+
}
|
|
535
|
+
} else {
|
|
536
|
+
record.independent_solver = false;
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
// ═══════════════════════════════════════════════════════════════════
|
|
542
|
+
// Hook #6: SEMESTER-END SPIKE (tag in event hook, duplicate in everything hook)
|
|
543
|
+
// ═══════════════════════════════════════════════════════════════════
|
|
544
|
+
if (type === "event") {
|
|
545
|
+
if (record.time) {
|
|
546
|
+
const eventTime = dayjs(record.time);
|
|
547
|
+
const dayInDataset = eventTime.diff(DATASET_START, 'days', true);
|
|
548
|
+
|
|
549
|
+
const spikableEvents = ["quiz started", "quiz completed", "assignment submitted"];
|
|
550
|
+
if (spikableEvents.includes(record.event)) {
|
|
551
|
+
if (dayInDataset >= 75 && dayInDataset <= 85) {
|
|
552
|
+
record.semester_end_rush = true;
|
|
553
|
+
} else {
|
|
554
|
+
record.semester_end_rush = false;
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
// ═══════════════════════════════════════════════════════════════════
|
|
561
|
+
// Hook #8: PLAYBACK SPEED CORRELATION
|
|
562
|
+
// ═══════════════════════════════════════════════════════════════════
|
|
563
|
+
if (type === "event") {
|
|
564
|
+
if (record.event === "lecture completed") {
|
|
565
|
+
const speed = record.playback_speed;
|
|
566
|
+
|
|
567
|
+
if (speed >= 2.0) {
|
|
568
|
+
record.speed_learner = true;
|
|
569
|
+
record.thorough_learner = false;
|
|
570
|
+
// Compress watch time for speed learners
|
|
571
|
+
if (record.watch_time_mins !== undefined) {
|
|
572
|
+
record.watch_time_mins = Math.max(3, Math.floor(record.watch_time_mins * 0.6));
|
|
573
|
+
}
|
|
574
|
+
} else if (speed !== undefined && speed <= 1.0) {
|
|
575
|
+
record.speed_learner = false;
|
|
576
|
+
record.thorough_learner = true;
|
|
577
|
+
// Extend watch time for thorough learners
|
|
578
|
+
if (record.watch_time_mins !== undefined) {
|
|
579
|
+
record.watch_time_mins = Math.min(90, Math.floor(record.watch_time_mins * 1.4));
|
|
580
|
+
}
|
|
581
|
+
} else {
|
|
582
|
+
record.speed_learner = false;
|
|
583
|
+
record.thorough_learner = false;
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
// ═══════════════════════════════════════════════════════════════════
|
|
589
|
+
// Hook #3: NOTES-TAKERS SUCCEED
|
|
590
|
+
// Hook #4: STUDY GROUP RETENTION
|
|
591
|
+
// Hook #7: FREE VS PAID (funnel-pre handled below)
|
|
592
|
+
// ═══════════════════════════════════════════════════════════════════
|
|
593
|
+
if (type === "everything") {
|
|
594
|
+
const userEvents = record;
|
|
595
|
+
const firstEventTime = userEvents.length > 0 ? dayjs(userEvents[0].time) : null;
|
|
596
|
+
|
|
597
|
+
// ---------------------------------------------------------------
|
|
598
|
+
// First pass: identify user patterns
|
|
599
|
+
// ---------------------------------------------------------------
|
|
600
|
+
let notesTakenCount = 0;
|
|
601
|
+
let joinedStudyGroupEarly = false;
|
|
602
|
+
let hasLowQuizScore = false;
|
|
603
|
+
|
|
604
|
+
userEvents.forEach((event) => {
|
|
605
|
+
const eventTime = dayjs(event.time);
|
|
606
|
+
const daysSinceStart = firstEventTime ? eventTime.diff(firstEventTime, 'days', true) : 0;
|
|
607
|
+
|
|
608
|
+
// Hook #3: Count lecture_completed events where notes_taken === true
|
|
609
|
+
if (event.event === "lecture completed" && event.notes_taken === true) {
|
|
610
|
+
notesTakenCount++;
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
// Hook #4: Check if user joined a study group within the first 10 days
|
|
614
|
+
if (event.event === "study group joined" && daysSinceStart <= 10) {
|
|
615
|
+
joinedStudyGroupEarly = true;
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
// Hook #4: Check for any quiz_completed with score < 60
|
|
619
|
+
if (event.event === "quiz completed" && event.score_percent < 60) {
|
|
620
|
+
hasLowQuizScore = true;
|
|
621
|
+
}
|
|
622
|
+
});
|
|
623
|
+
|
|
624
|
+
// ---------------------------------------------------------------
|
|
625
|
+
// Second pass: modify events based on patterns
|
|
626
|
+
// ---------------------------------------------------------------
|
|
627
|
+
|
|
628
|
+
// Hook #3: NOTES-TAKERS SUCCEED
|
|
629
|
+
if (notesTakenCount >= 5) {
|
|
630
|
+
userEvents.forEach((event, idx) => {
|
|
631
|
+
// Boost quiz scores for diligent note-takers
|
|
632
|
+
if (event.event === "quiz completed") {
|
|
633
|
+
if (event.score_percent !== undefined) {
|
|
634
|
+
event.score_percent = Math.min(100, event.score_percent + 20);
|
|
635
|
+
}
|
|
636
|
+
event.diligent_student = true;
|
|
637
|
+
}
|
|
638
|
+
});
|
|
639
|
+
|
|
640
|
+
// 40% chance to splice in an extra certificate_earned event
|
|
641
|
+
if (chance.bool({ likelihood: 40 })) {
|
|
642
|
+
const lastEvent = userEvents[userEvents.length - 1];
|
|
643
|
+
if (lastEvent) {
|
|
644
|
+
const certEvent = {
|
|
645
|
+
event: "certificate earned",
|
|
646
|
+
time: dayjs(lastEvent.time).add(chance.integer({ min: 1, max: 5 }), 'days').toISOString(),
|
|
647
|
+
user_id: lastEvent.user_id,
|
|
648
|
+
course_id: chance.pickone(courseIds),
|
|
649
|
+
completion_time_days: chance.integer({ min: 14, max: 90 }),
|
|
650
|
+
final_grade: chance.integer({ min: 80, max: 100 }),
|
|
651
|
+
diligent_student: true,
|
|
652
|
+
};
|
|
653
|
+
userEvents.push(certEvent);
|
|
654
|
+
}
|
|
655
|
+
}
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
// Hook #8 (everything pass): Speed learners (3+ lectures at 2.0x) get higher quiz scores
|
|
659
|
+
let speedLectureCount = 0;
|
|
660
|
+
userEvents.forEach((event) => {
|
|
661
|
+
if (event.event === "lecture completed" && event.speed_learner === true) {
|
|
662
|
+
speedLectureCount++;
|
|
663
|
+
}
|
|
664
|
+
});
|
|
665
|
+
const isSpeedLearner = speedLectureCount >= 3;
|
|
666
|
+
|
|
667
|
+
if (isSpeedLearner) {
|
|
668
|
+
userEvents.forEach((event) => {
|
|
669
|
+
if (event.event === "quiz completed") {
|
|
670
|
+
if (event.score_percent !== undefined) {
|
|
671
|
+
event.score_percent = Math.min(100, event.score_percent + 8);
|
|
672
|
+
event.speed_learner_effect = true;
|
|
673
|
+
}
|
|
674
|
+
}
|
|
675
|
+
});
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
// Hook #6: SEMESTER-END SPIKE - duplicate assessment events in the spike window
|
|
679
|
+
const duplicates = [];
|
|
680
|
+
userEvents.forEach((event) => {
|
|
681
|
+
if (event.semester_end_rush === true && chance.bool({ likelihood: 80 })) {
|
|
682
|
+
const dup = JSON.parse(JSON.stringify(event));
|
|
683
|
+
dup.time = dayjs(event.time).add(chance.integer({ min: 5, max: 120 }), 'minutes').toISOString();
|
|
684
|
+
dup.semester_end_rush = true;
|
|
685
|
+
duplicates.push(dup);
|
|
686
|
+
}
|
|
687
|
+
});
|
|
688
|
+
if (duplicates.length > 0) {
|
|
689
|
+
userEvents.push(...duplicates);
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
// Hook #7: FREE VS PAID - reinforce the subscription effect on certificates
|
|
693
|
+
const subStatus = meta && meta.profile ? meta.profile.subscription_status : "free";
|
|
694
|
+
if (subStatus === "free") {
|
|
695
|
+
// Free users lose 55% of their certificates (simulating lower completion)
|
|
696
|
+
for (let i = userEvents.length - 1; i >= 0; i--) {
|
|
697
|
+
if (userEvents[i].event === "certificate earned" && chance.bool({ likelihood: 55 })) {
|
|
698
|
+
userEvents.splice(i, 1);
|
|
699
|
+
}
|
|
700
|
+
}
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
// Hook #4: STUDY GROUP RETENTION (runs LAST to ensure churn removal isn't undone by later hooks)
|
|
704
|
+
if (!joinedStudyGroupEarly && hasLowQuizScore) {
|
|
705
|
+
// Non-joiners with low scores: remove ALL events after day 14 from their first event (hard churn)
|
|
706
|
+
const churnCutoff = firstEventTime ? firstEventTime.add(14, 'days') : null;
|
|
707
|
+
for (let i = userEvents.length - 1; i >= 0; i--) {
|
|
708
|
+
const evt = userEvents[i];
|
|
709
|
+
if (churnCutoff && dayjs(evt.time).isAfter(churnCutoff)) {
|
|
710
|
+
userEvents.splice(i, 1);
|
|
711
|
+
}
|
|
712
|
+
}
|
|
713
|
+
} else if (joinedStudyGroupEarly) {
|
|
714
|
+
// Study group joiners keep all events and get bonus discussion_posted events
|
|
715
|
+
const lastEvent = userEvents[userEvents.length - 1];
|
|
716
|
+
if (lastEvent && chance.bool({ likelihood: 60 })) {
|
|
717
|
+
const bonusDiscussion = {
|
|
718
|
+
event: "discussion posted",
|
|
719
|
+
time: dayjs(lastEvent.time).add(chance.integer({ min: 1, max: 3 }), 'days').toISOString(),
|
|
720
|
+
user_id: lastEvent.user_id,
|
|
721
|
+
course_id: chance.pickone(courseIds),
|
|
722
|
+
post_type: chance.pickone(["question", "answer", "comment"]),
|
|
723
|
+
word_count: chance.integer({ min: 20, max: 400 }),
|
|
724
|
+
study_group_member: true,
|
|
725
|
+
};
|
|
726
|
+
userEvents.push(bonusDiscussion);
|
|
727
|
+
}
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
// Hook #2b: DEADLINE CRAMMING (quiz score penalty)
|
|
731
|
+
// Applied LAST to avoid selection bias — if applied before churn,
|
|
732
|
+
// the penalty pushes Sun/Mon quiz-takers below the hasLowQuizScore
|
|
733
|
+
// threshold, selectively churning them and inflating the avg.
|
|
734
|
+
userEvents.forEach((event) => {
|
|
735
|
+
if (event.event === "quiz completed" && event.time) {
|
|
736
|
+
const eventDay = dayjs(event.time).day();
|
|
737
|
+
if (eventDay === 0 || eventDay === 1) {
|
|
738
|
+
if (event.score_percent !== undefined) {
|
|
739
|
+
event.score_percent = Math.max(0, event.score_percent - 25);
|
|
740
|
+
}
|
|
741
|
+
}
|
|
742
|
+
}
|
|
743
|
+
});
|
|
744
|
+
}
|
|
745
|
+
|
|
746
|
+
// ═══════════════════════════════════════════════════════════════════
|
|
747
|
+
// Hook #7: FREE VS PAID COURSES (funnel-pre)
|
|
748
|
+
// ═══════════════════════════════════════════════════════════════════
|
|
749
|
+
if (type === "funnel-pre") {
|
|
750
|
+
// Target funnels containing course completion events
|
|
751
|
+
if (meta && meta.profile && meta.funnel) {
|
|
752
|
+
const subscriptionStatus = meta.profile.subscription_status;
|
|
753
|
+
|
|
754
|
+
if (subscriptionStatus === "free") {
|
|
755
|
+
// Free users convert at 0.5x rate
|
|
756
|
+
record.conversionRate = (record.conversionRate || 0.25) * 0.5;
|
|
757
|
+
} else if (subscriptionStatus === "monthly" || subscriptionStatus === "annual") {
|
|
758
|
+
// Paid subscribers convert at 1.5x rate
|
|
759
|
+
record.conversionRate = (record.conversionRate || 0.25) * 1.5;
|
|
760
|
+
}
|
|
761
|
+
}
|
|
762
|
+
}
|
|
763
|
+
|
|
764
|
+
return record;
|
|
765
|
+
}
|
|
766
|
+
};
|
|
767
|
+
|
|
768
|
+
export default config;
|