@ak--47/dungeon-master 1.2.2 → 1.3.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.
Files changed (52) hide show
  1. package/CHANGELOG.md +42 -0
  2. package/README.md +51 -13
  3. package/dungeons/technical/ad-spend.js +2 -2
  4. package/dungeons/technical/anonymous-users.js +2 -2
  5. package/dungeons/technical/array-of-object-lookup.js +2 -2
  6. package/dungeons/technical/experiments.js +2 -2
  7. package/dungeons/technical/foobar.js +2 -2
  8. package/dungeons/technical/group-analytics.js +2 -2
  9. package/dungeons/technical/mirror-strategies.js +2 -2
  10. package/dungeons/technical/nested-objects.js +2 -2
  11. package/dungeons/technical/retention-cadence.js +2 -3
  12. package/dungeons/technical/sanity.js +2 -2
  13. package/dungeons/technical/scale-test.js +2 -2
  14. package/dungeons/technical/scd.js +2 -2
  15. package/dungeons/technical/simple.js +2 -2
  16. package/dungeons/technical/simplest.js +2 -2
  17. package/dungeons/technical/text-generation.js +2 -2
  18. package/dungeons/vertical/ai-platform-schema.json +617 -0
  19. package/dungeons/vertical/ai-platform.js +799 -0
  20. package/dungeons/vertical/community.js +40 -26
  21. package/dungeons/vertical/crypto-schema.json +546 -0
  22. package/dungeons/vertical/crypto.js +721 -0
  23. package/dungeons/vertical/dating-schema.json +401 -0
  24. package/dungeons/vertical/dating.js +798 -0
  25. package/dungeons/vertical/devtools.js +13 -9
  26. package/dungeons/vertical/ecommerce.js +2 -3
  27. package/dungeons/vertical/education.js +4 -5
  28. package/dungeons/vertical/fintech.js +32 -29
  29. package/dungeons/vertical/fitness.js +2 -3
  30. package/dungeons/vertical/food-delivery.js +37 -43
  31. package/dungeons/vertical/gaming-schema.json +2495 -230
  32. package/dungeons/vertical/gaming.js +771 -388
  33. package/dungeons/vertical/healthcare.js +2 -3
  34. package/dungeons/vertical/insurance-application.js +2 -3
  35. package/dungeons/vertical/logistics.js +20 -14
  36. package/dungeons/vertical/marketplace.js +22 -14
  37. package/dungeons/vertical/media.js +39 -30
  38. package/dungeons/vertical/real-estate-schema.json +527 -0
  39. package/dungeons/vertical/real-estate.js +774 -0
  40. package/dungeons/vertical/sass.js +2 -3
  41. package/dungeons/vertical/social.js +15 -13
  42. package/dungeons/vertical/travel.js +59 -26
  43. package/lib/core/config-validator.js +71 -15
  44. package/lib/core/storage.js +14 -4
  45. package/lib/orchestrators/user-loop.js +39 -5
  46. package/lib/templates/macro-presets.js +111 -0
  47. package/lib/templates/soup-presets.js +19 -36
  48. package/package.json +8 -2
  49. package/types.d.ts +219 -42
  50. package/dungeons/user/.gitkeep +0 -0
  51. package/dungeons/vertical/rpg-schema.json +0 -2491
  52. package/dungeons/vertical/rpg.js +0 -976
@@ -0,0 +1,799 @@
1
+ // ── TWEAK THESE ──
2
+ const SEED = "promptforge";
3
+ const num_days = 120;
4
+ const num_users = 8_000;
5
+ const avg_events_per_user_per_day = 0.83;
6
+ let token = "your-mixpanel-token";
7
+
8
+ // ── env overrides ──
9
+ if (process.env.MP_TOKEN) token = process.env.MP_TOKEN;
10
+
11
+ import dayjs from "dayjs";
12
+ import utc from "dayjs/plugin/utc.js";
13
+ import "dotenv/config";
14
+ import * as u from "../../lib/utils/utils.js";
15
+ import * as v from "ak-tools";
16
+
17
+ dayjs.extend(utc);
18
+ const chance = u.initChance(SEED);
19
+ const NOW = dayjs();
20
+ const DATASET_START = NOW.subtract(num_days, "days");
21
+
22
+ /** @typedef {import("../../types").Dungeon} Config */
23
+
24
+ /**
25
+ * ===============================================================
26
+ * DATASET OVERVIEW
27
+ * ===============================================================
28
+ *
29
+ * PromptForge -- an LLM API platform (like Anthropic/OpenAI).
30
+ * Customers (developers and companies) send API requests for chat
31
+ * completions, embeddings, evaluations, and tool use. Billing is
32
+ * per input/output token. Key features: prompt caching, tool use,
33
+ * multi-turn conversations, batch API, model selection, and
34
+ * evaluation pipelines.
35
+ *
36
+ * - 8,000 users over 120 days, ~800K events
37
+ * - Three API tiers: Free, Build, Enterprise
38
+ * - Core loop: org created -> api key created -> api call -> iterate
39
+ * - Revenue: token-based billing with tier-based pricing
40
+ *
41
+ * Key entities:
42
+ * - model: LLM model version (sonnet-4, haiku-4, opus-4-6, opus-4-7)
43
+ * - api_tier: Free / Build / Enterprise (determines context window, rate limits)
44
+ * - tokens_used: total tokens consumed per API call (input + output)
45
+ * - cost_usd: dollar cost of a single API call
46
+ * - cache_enabled: prompt caching flag that reduces cost 70%
47
+ * - multi_turn: whether the call is part of a conversation
48
+ *
49
+ * ===============================================================
50
+ * ANALYTICS HOOKS (8 hooks)
51
+ * ===============================================================
52
+ *
53
+ * ---------------------------------------------------------------
54
+ * 1. PROMPT CACHING ADOPTION (CONVERSION — event + everything)
55
+ * ---------------------------------------------------------------
56
+ *
57
+ * PATTERN: Customers who enable prompt caching see 70% lower
58
+ * cost_per_call. Once any api call has cache_enabled=true, all
59
+ * subsequent calls for that user get cost_usd reduced by 70%.
60
+ *
61
+ * HOW TO FIND IT IN MIXPANEL:
62
+ *
63
+ * Report 1: Cost Per Call by Cache Status
64
+ * - Report type: Insights
65
+ * - Event: "api call"
66
+ * - Measure: Average of "cost_usd"
67
+ * - Breakdown: "cache_enabled"
68
+ * - Expected: cache_enabled=true ~ $0.003, false ~ $0.01 (70% cheaper)
69
+ *
70
+ * Report 2: Cache Adoption Over Time
71
+ * - Report type: Insights
72
+ * - Event: "api call"
73
+ * - Measure: Total
74
+ * - Filter: cache_enabled = true
75
+ * - Line chart by week
76
+ * - Expected: steady growth in cached calls over the dataset
77
+ *
78
+ * REAL-WORLD ANALOGUE: Prompt caching avoids re-processing long
79
+ * system prompts on every call, dramatically reducing cost and latency.
80
+ *
81
+ * ---------------------------------------------------------------
82
+ * 2. MODEL MIGRATION WAVE (TIMED RELEASE — event)
83
+ * ---------------------------------------------------------------
84
+ *
85
+ * PATTERN: At day 60, new model "opus-4-7" releases. After day 60,
86
+ * 35% of api calls from Build/Enterprise users switch model to
87
+ * "opus-4-7". These calls use 1.5x tokens (smarter model, longer
88
+ * responses).
89
+ *
90
+ * HOW TO FIND IT IN MIXPANEL:
91
+ *
92
+ * Report 1: Model Distribution Over Time
93
+ * - Report type: Insights
94
+ * - Event: "api call"
95
+ * - Measure: Total
96
+ * - Breakdown: "model"
97
+ * - Line chart by week
98
+ * - Expected: opus-4-7 appears at day 60, ramps to ~35% of paid calls
99
+ *
100
+ * Report 2: Tokens Per Model
101
+ * - Report type: Insights
102
+ * - Event: "api call"
103
+ * - Measure: Average of "tokens_used"
104
+ * - Breakdown: "model"
105
+ * - Expected: opus-4-7 ~ 1.5x tokens vs other models
106
+ *
107
+ * REAL-WORLD ANALOGUE: New flagship model launches cause migration
108
+ * waves among power users who want improved capabilities.
109
+ *
110
+ * ---------------------------------------------------------------
111
+ * 3. AGENTIC LOOP POWER USERS (BEHAVIORS TOGETHER — everything)
112
+ * ---------------------------------------------------------------
113
+ *
114
+ * PATTERN: Users who use both "tool use call" AND have multi_turn=true
115
+ * on any api call are agentic loop users. They get 8x tokens_used on
116
+ * all api calls and 3x extra api call events injected.
117
+ *
118
+ * HOW TO FIND IT IN MIXPANEL:
119
+ *
120
+ * Report 1: Token Usage — Agentic vs Standard
121
+ * - Report type: Insights
122
+ * - Event: "api call"
123
+ * - Measure: Average of "tokens_used"
124
+ * - Breakdown: "is_agentic_user"
125
+ * - Expected: is_agentic_user=true ~ 8x tokens (agentic ~ 40K, standard ~ 5K)
126
+ *
127
+ * Report 2: API Call Volume — Agentic vs Standard
128
+ * - Report type: Insights
129
+ * - Event: "api call"
130
+ * - Measure: Total per user (average)
131
+ * - Breakdown: "is_agentic_user"
132
+ * - Expected: agentic users ~ 3x more api calls
133
+ *
134
+ * REAL-WORLD ANALOGUE: Agentic workloads (coding agents, research
135
+ * assistants) consume dramatically more tokens via extended tool-use
136
+ * loops and multi-turn conversations.
137
+ *
138
+ * ---------------------------------------------------------------
139
+ * 4. RATE LIMIT CHURN (CHURN — everything)
140
+ * ---------------------------------------------------------------
141
+ *
142
+ * PATTERN: Users hitting "rate limit error" >= 5 times in first
143
+ * 7 days lose 60% of events after week 1. Rate-limited users
144
+ * churn from frustration.
145
+ *
146
+ * HOW TO FIND IT IN MIXPANEL:
147
+ *
148
+ * Report 1: Retention by Early Rate Limiting
149
+ * - Report type: Retention
150
+ * - Event A: any event
151
+ * - Event B: any event
152
+ * - Breakdown: "hit_rate_limit_early" (user property)
153
+ * - Expected: hit_rate_limit_early=true ~ 40% D30 retention
154
+ * vs ~80% for others
155
+ *
156
+ * Report 2: Event Volume Post Rate-Limit
157
+ * - Report type: Insights
158
+ * - Event: any event
159
+ * - Measure: Total per user (average)
160
+ * - Breakdown: "hit_rate_limit_early"
161
+ * - Expected: rate-limited users ~ 40% of normal volume
162
+ *
163
+ * REAL-WORLD ANALOGUE: Developers who get rate-limited early in
164
+ * their evaluation often switch to a competitor platform.
165
+ *
166
+ * ---------------------------------------------------------------
167
+ * 5. TIER-BASED CONTEXT WINDOW (SUBSCRIPTION TIER — everything)
168
+ * ---------------------------------------------------------------
169
+ *
170
+ * PATTERN: Free users have context_window=200000, Build=1000000,
171
+ * Enterprise=2000000. Enterprise users send 4x larger input_tokens.
172
+ * Context window and input tokens are scaled by tier.
173
+ *
174
+ * HOW TO FIND IT IN MIXPANEL:
175
+ *
176
+ * Report 1: Input Tokens by Tier
177
+ * - Report type: Insights
178
+ * - Event: "api call"
179
+ * - Measure: Average of "input_tokens"
180
+ * - Breakdown: "api_tier" (superProp)
181
+ * - Expected: Enterprise ~ 4x Free (Enterprise ~ 8K, Free ~ 2K)
182
+ *
183
+ * Report 2: Context Window by Tier
184
+ * - Report type: Insights
185
+ * - Event: "api call"
186
+ * - Measure: Average of "context_window"
187
+ * - Breakdown: "api_tier"
188
+ * - Expected: Free=200K, Build=1M, Enterprise=2M
189
+ *
190
+ * REAL-WORLD ANALOGUE: Enterprise customers pay for larger context
191
+ * windows and use them for long-document analysis and code review.
192
+ *
193
+ * ---------------------------------------------------------------
194
+ * 6. OUTAGE DAY (TIME-BASED — event)
195
+ * ---------------------------------------------------------------
196
+ *
197
+ * PATTERN: Days 40-41, is_error is set to true on 40% of api call
198
+ * events. error_type is set to service errors. Simulates a major
199
+ * platform outage.
200
+ *
201
+ * HOW TO FIND IT IN MIXPANEL:
202
+ *
203
+ * Report 1: Error Rate Over Time
204
+ * - Report type: Insights
205
+ * - Event: "api call"
206
+ * - Measure: Total
207
+ * - Filter: is_error = true
208
+ * - Line chart by day
209
+ * - Expected: massive spike on days 40-41 (8x baseline error rate)
210
+ *
211
+ * Report 2: Error Types During Outage
212
+ * - Report type: Insights
213
+ * - Event: "api call"
214
+ * - Filter: is_error = true
215
+ * - Breakdown: "error_type"
216
+ * - Date range: days 40-41
217
+ * - Expected: service_overloaded and internal_server_error dominate
218
+ *
219
+ * REAL-WORLD ANALOGUE: API platforms experience periodic outages
220
+ * that spike error rates across all customers.
221
+ *
222
+ * ---------------------------------------------------------------
223
+ * 7. BATCH API DISCOUNT (PURCHASE VALUE — everything)
224
+ * ---------------------------------------------------------------
225
+ *
226
+ * PATTERN: Users who submit batch jobs get 50% lower cost_per_token
227
+ * on api calls but use 2x tokens_used. Batch processing is cheaper
228
+ * per token but encourages higher volume.
229
+ *
230
+ * HOW TO FIND IT IN MIXPANEL:
231
+ *
232
+ * Report 1: Cost Per Token — Batch vs Interactive
233
+ * - Report type: Insights
234
+ * - Event: "api call"
235
+ * - Measure: Average of "cost_per_token"
236
+ * - Breakdown: "is_batch_user"
237
+ * - Expected: is_batch_user=true ~ 50% lower cost per token
238
+ *
239
+ * Report 2: Token Volume — Batch Users
240
+ * - Report type: Insights
241
+ * - Event: "api call"
242
+ * - Measure: Average of "tokens_used"
243
+ * - Breakdown: "is_batch_user"
244
+ * - Expected: batch users ~ 2x token volume
245
+ *
246
+ * REAL-WORLD ANALOGUE: Batch API pricing incentivizes high-volume
247
+ * workloads with discounted per-token rates.
248
+ *
249
+ * ---------------------------------------------------------------
250
+ * 8. EVAL-DRIVEN RETENTION (RETENTION — everything)
251
+ * ---------------------------------------------------------------
252
+ *
253
+ * PATTERN: Users who run "eval job" in the first 7 days have 75%
254
+ * D30 retention vs 25% for non-eval users. Early eval adoption
255
+ * indicates serious platform investment.
256
+ *
257
+ * HOW TO FIND IT IN MIXPANEL:
258
+ *
259
+ * Report 1: Retention by Early Eval Usage
260
+ * - Report type: Retention
261
+ * - Event A: any event
262
+ * - Event B: any event
263
+ * - Breakdown: "has_early_eval" (user property)
264
+ * - Expected: has_early_eval=true ~ 75% D30 vs 25% for false
265
+ *
266
+ * Report 2: Event Volume Over Time
267
+ * - Report type: Insights
268
+ * - Event: any event
269
+ * - Measure: Total per user (average)
270
+ * - Breakdown: "has_early_eval"
271
+ * - Line chart by week
272
+ * - Expected: early eval users sustain volume; non-eval users decay
273
+ *
274
+ * REAL-WORLD ANALOGUE: Teams that set up evaluation pipelines early
275
+ * are deeply invested in prompt quality and stick with the platform.
276
+ *
277
+ * ===============================================================
278
+ * EXPECTED METRICS SUMMARY
279
+ * ===============================================================
280
+ *
281
+ * Hook | Metric | Baseline | Effect | Ratio
282
+ * ----------------------------|----------------------|------------|--------------|------
283
+ * Prompt Caching Adoption | cost_usd | $0.01 | $0.003 | 0.3x
284
+ * Model Migration Wave | opus-4-7 share | 0% | ~35% (paid) | new
285
+ * Agentic Loop Power Users | tokens_used | 5K | 40K | 8x
286
+ * Rate Limit Churn | D30 retention | 80% | 40% | 0.5x
287
+ * Tier-Based Context Window | input_tokens | 2K (Free) | 8K (Ent) | 4x
288
+ * Outage Day | error rate | 5% | 40% | 8x
289
+ * Batch API Discount | cost_per_token | $0.00001 | $0.000005 | 0.5x
290
+ * Eval-Driven Retention | D30 retention | 25% | 75% | 3x
291
+ */
292
+
293
+ /** @type {Config} */
294
+ const config = {
295
+ token,
296
+ seed: SEED,
297
+ numDays: num_days,
298
+ avgEventsPerUserPerDay: avg_events_per_user_per_day,
299
+ numUsers: num_users,
300
+ hasAnonIds: false,
301
+ hasSessionIds: false,
302
+ format: "json",
303
+ gzip: true,
304
+ alsoInferFunnels: false,
305
+ hasLocation: true,
306
+ hasAndroidDevices: false,
307
+ hasIOSDevices: false,
308
+ hasDesktopDevices: true,
309
+ hasBrowser: true,
310
+ hasCampaigns: false,
311
+ isAnonymous: false,
312
+ hasAdSpend: false,
313
+ hasAvatar: true,
314
+ concurrency: 1,
315
+ writeToDisk: false,
316
+
317
+ soup: "growth",
318
+
319
+ scdProps: {
320
+ monthly_api_usage: {
321
+ values: u.weighNumRange(0, 1000000, 0.3, 50),
322
+ frequency: "week",
323
+ timing: "fuzzy",
324
+ max: 20,
325
+ },
326
+ api_tier_history: {
327
+ values: ["Free", "Build", "Enterprise"],
328
+ frequency: "month",
329
+ timing: "fixed",
330
+ max: 6,
331
+ },
332
+ },
333
+
334
+ // -- Events (18) ------------------------------------------
335
+ events: [
336
+ {
337
+ event: "organization created",
338
+ weight: 1,
339
+ isFirstEvent: true,
340
+ properties: {
341
+ org_size: ["solo", "startup", "growth", "enterprise"],
342
+ referral_source: ["docs", "blog", "github", "word_of_mouth", "search", "conference"],
343
+ },
344
+ },
345
+ {
346
+ event: "api key created",
347
+ weight: 2,
348
+ properties: {
349
+ key_type: ["development", "production", "staging"],
350
+ key_scope: ["full_access", "read_only", "completions_only"],
351
+ },
352
+ },
353
+ {
354
+ event: "api key rotated",
355
+ weight: 1,
356
+ properties: {
357
+ rotation_reason: ["scheduled", "compromised", "policy", "manual"],
358
+ },
359
+ },
360
+ {
361
+ event: "api call",
362
+ weight: 10,
363
+ properties: {
364
+ model: ["sonnet-4", "sonnet-4", "sonnet-4", "haiku-4", "haiku-4", "opus-4-6"],
365
+ input_tokens: u.weighNumRange(50, 8000, 0.4, 2000),
366
+ output_tokens: u.weighNumRange(10, 4000, 0.4, 500),
367
+ tokens_used: u.weighNumRange(100, 12000, 0.4, 2500),
368
+ cost_usd: [0.001, 0.002, 0.003, 0.003, 0.005, 0.005, 0.005, 0.008, 0.008, 0.01, 0.01, 0.01, 0.01, 0.015, 0.015, 0.02, 0.025, 0.03, 0.04, 0.05],
369
+ cost_per_token: [0.000002, 0.000003, 0.000005, 0.000005, 0.000008, 0.000008, 0.00001, 0.00001, 0.00001, 0.000012, 0.000015, 0.00002, 0.000025, 0.00003],
370
+ latency_ms: u.weighNumRange(100, 15000, 0.4, 1500),
371
+ cache_enabled: [false],
372
+ is_error: [false],
373
+ error_type: ["none"],
374
+ multi_turn: [false, false, false, true],
375
+ context_window: [200000],
376
+ is_agentic_user: [false],
377
+ is_batch_user: [false],
378
+ stream: [true, true, true, false],
379
+ stop_reason: ["end_turn", "end_turn", "end_turn", "max_tokens", "tool_use"],
380
+ },
381
+ },
382
+ {
383
+ event: "tool use call",
384
+ weight: 4,
385
+ properties: {
386
+ tool_name: ["web_search", "code_interpreter", "file_reader", "calculator", "database_query", "api_connector"],
387
+ execution_time_ms: u.weighNumRange(50, 10000, 0.4, 800),
388
+ success: [true, true, true, true, false],
389
+ tool_input_tokens: u.weighNumRange(50, 2000, 0.4, 300),
390
+ tool_output_tokens: u.weighNumRange(20, 5000, 0.4, 500),
391
+ },
392
+ },
393
+ {
394
+ event: "batch job submitted",
395
+ weight: 2,
396
+ properties: {
397
+ batch_size: u.weighNumRange(10, 10000, 0.3, 500),
398
+ model: ["sonnet-4", "haiku-4", "opus-4-6"],
399
+ estimated_tokens: u.weighNumRange(10000, 5000000, 0.3, 500000),
400
+ priority: ["standard", "standard", "standard", "express"],
401
+ },
402
+ },
403
+ {
404
+ event: "batch job completed",
405
+ weight: 2,
406
+ properties: {
407
+ batch_size: u.weighNumRange(10, 10000, 0.3, 500),
408
+ processing_time_sec: u.weighNumRange(60, 7200, 0.4, 900),
409
+ total_tokens: u.weighNumRange(10000, 5000000, 0.3, 500000),
410
+ success_rate: u.weighNumRange(90, 100, 0.8, 98),
411
+ },
412
+ },
413
+ {
414
+ event: "eval job",
415
+ weight: 3,
416
+ properties: {
417
+ eval_type: ["accuracy", "relevance", "safety", "latency", "cost", "custom"],
418
+ num_test_cases: u.weighNumRange(10, 1000, 0.3, 100),
419
+ model: ["sonnet-4", "haiku-4", "opus-4-6"],
420
+ dataset_name: ["prod_prompts", "safety_suite", "regression_set", "benchmark_v2", "custom_eval"],
421
+ },
422
+ },
423
+ {
424
+ event: "eval result",
425
+ weight: 3,
426
+ properties: {
427
+ eval_type: ["accuracy", "relevance", "safety", "latency", "cost", "custom"],
428
+ score: u.weighNumRange(0, 100, 0.6, 75),
429
+ pass_rate: u.weighNumRange(50, 100, 0.7, 85),
430
+ model: ["sonnet-4", "haiku-4", "opus-4-6"],
431
+ regression_detected: [false, false, false, false, true],
432
+ },
433
+ },
434
+ {
435
+ event: "rate limit error",
436
+ weight: 3,
437
+ properties: {
438
+ error_code: [429],
439
+ retry_after_ms: u.weighNumRange(1000, 60000, 0.3, 5000),
440
+ requests_per_minute: u.weighNumRange(50, 2000, 0.4, 500),
441
+ tier_limit: ["Free", "Build", "Enterprise"],
442
+ },
443
+ },
444
+ {
445
+ event: "billing payment",
446
+ weight: 2,
447
+ properties: {
448
+ amount_usd: u.weighNumRange(5, 50000, 0.2, 500),
449
+ payment_method: ["credit_card", "credit_card", "credit_card", "invoice", "wire_transfer"],
450
+ billing_period: ["monthly", "monthly", "annual"],
451
+ tokens_consumed: u.weighNumRange(100000, 50000000, 0.3, 5000000),
452
+ },
453
+ },
454
+ {
455
+ event: "model selected",
456
+ weight: 3,
457
+ properties: {
458
+ model: ["sonnet-4", "sonnet-4", "haiku-4", "opus-4-6"],
459
+ is_default: [true, true, false],
460
+ selection_context: ["playground", "api_config", "eval_setup", "batch_config"],
461
+ },
462
+ },
463
+ {
464
+ event: "dashboard viewed",
465
+ weight: 5,
466
+ properties: {
467
+ dashboard_section: ["usage", "billing", "api_keys", "models", "evals", "logs"],
468
+ time_range: ["1h", "24h", "7d", "30d"],
469
+ },
470
+ },
471
+ {
472
+ event: "docs searched",
473
+ weight: 4,
474
+ properties: {
475
+ search_query_category: ["api_reference", "quickstart", "pricing", "models", "tool_use", "batch_api", "caching", "errors"],
476
+ results_found: u.weighNumRange(0, 50, 0.5, 8),
477
+ clicked_result: [true, true, true, false],
478
+ },
479
+ },
480
+ {
481
+ event: "member invited",
482
+ weight: 2,
483
+ properties: {
484
+ invite_role: ["admin", "developer", "developer", "billing", "viewer"],
485
+ invite_method: ["email", "email", "sso", "link"],
486
+ },
487
+ },
488
+ {
489
+ event: "webhook configured",
490
+ weight: 1,
491
+ properties: {
492
+ webhook_event: ["usage_alert", "rate_limit", "batch_complete", "eval_complete", "billing_threshold"],
493
+ delivery_method: ["https", "https", "slack", "email"],
494
+ },
495
+ },
496
+ {
497
+ event: "playground session",
498
+ weight: 4,
499
+ properties: {
500
+ model: ["sonnet-4", "sonnet-4", "haiku-4", "opus-4-6"],
501
+ turns: u.weighNumRange(1, 30, 0.4, 5),
502
+ shared: [false, false, false, true],
503
+ tokens_used: u.weighNumRange(100, 20000, 0.3, 3000),
504
+ },
505
+ },
506
+ {
507
+ event: "account deactivated",
508
+ weight: 1,
509
+ isChurnEvent: true,
510
+ returnLikelihood: 0.1,
511
+ isStrictEvent: true,
512
+ properties: {
513
+ reason: ["cost", "switched_provider", "project_ended", "rate_limits", "no_longer_needed", "performance"],
514
+ },
515
+ },
516
+ ],
517
+
518
+ // -- Funnels (3) ------------------------------------------
519
+ funnels: [
520
+ {
521
+ name: "Onboarding",
522
+ sequence: ["organization created", "api key created", "api call"],
523
+ conversionRate: 70,
524
+ order: "sequential",
525
+ isFirstFunnel: true,
526
+ timeToConvert: 48,
527
+ weight: 3,
528
+ },
529
+ {
530
+ name: "API to Eval Pipeline",
531
+ sequence: ["api call", "tool use call", "eval job"],
532
+ conversionRate: 45,
533
+ order: "sequential",
534
+ timeToConvert: 168,
535
+ weight: 5,
536
+ },
537
+ {
538
+ name: "Usage to Billing",
539
+ sequence: ["api call", "billing payment"],
540
+ conversionRate: 30,
541
+ order: "sequential",
542
+ timeToConvert: 336,
543
+ weight: 2,
544
+ },
545
+ ],
546
+
547
+ // -- SuperProps --------------------------------------------
548
+ superProps: {
549
+ api_tier: ["Free", "Free", "Build", "Build", "Enterprise"],
550
+ primary_use_case: ["chatbot", "code_generation", "data_extraction", "content_creation", "agents"],
551
+ sdk_language: ["python", "typescript", "java", "go", "curl"],
552
+ },
553
+
554
+ // -- UserProps ---------------------------------------------
555
+ userProps: {
556
+ api_tier: ["Free", "Free", "Build", "Build", "Enterprise"],
557
+ primary_use_case: ["chatbot", "code_generation", "data_extraction", "content_creation", "agents"],
558
+ sdk_language: ["python", "typescript", "java", "go", "curl"],
559
+ monthly_spend: u.weighNumRange(0, 50000, 0.2, 200),
560
+ total_api_calls: u.weighNumRange(0, 500000, 0.2, 10000),
561
+ preferred_model: ["sonnet-4", "sonnet-4", "haiku-4", "opus-4-6"],
562
+ has_eval_pipeline: [false],
563
+ hit_rate_limit_early: [false],
564
+ has_early_eval: [false],
565
+ },
566
+
567
+ // -- Hook Function ----------------------------------------
568
+ hook: function (record, type, meta) {
569
+ // ─────────────────────────────────────────────────────────
570
+ // Hook #6: OUTAGE DAY (event)
571
+ // Days 40-41: 40% of api calls get is_error=true with
572
+ // service error types
573
+ // ─────────────────────────────────────────────────────────
574
+ if (type === "event") {
575
+ if (record.event === "api call") {
576
+ const eventTime = dayjs(record.time);
577
+ const dayInDataset = eventTime.diff(DATASET_START, "days", true);
578
+
579
+ // Hook #6: Outage day errors
580
+ if (dayInDataset >= 40 && dayInDataset < 42) {
581
+ if (chance.bool({ likelihood: 40 })) {
582
+ record.is_error = true;
583
+ record.error_type = chance.pickone([
584
+ "service_overloaded",
585
+ "internal_server_error",
586
+ "gateway_timeout",
587
+ ]);
588
+ record.latency_ms = Math.floor((record.latency_ms || 1500) * 3);
589
+ }
590
+ }
591
+
592
+ // Hook #2: Model migration wave (event portion)
593
+ // After day 60, 35% of Build/Enterprise users switch to opus-4-7
594
+ if (dayInDataset >= 60) {
595
+ if (
596
+ (record.api_tier === "Build" || record.api_tier === "Enterprise") &&
597
+ chance.bool({ likelihood: 35 })
598
+ ) {
599
+ record.model = "opus-4-7";
600
+ }
601
+ }
602
+ }
603
+
604
+ return record;
605
+ }
606
+
607
+ // ─────────────────────────────────────────────────────────
608
+ // Hook: USER PROFILE ENRICHMENT (user)
609
+ // Tag user profiles for discoverability
610
+ // ─────────────────────────────────────────────────────────
611
+ if (type === "user") {
612
+ // Defaults for hook-driven user properties
613
+ record.hit_rate_limit_early = false;
614
+ record.has_early_eval = false;
615
+ record.has_eval_pipeline = false;
616
+ }
617
+
618
+ // ─────────────────────────────────────────────────────────
619
+ // EVERYTHING HOOKS
620
+ // ─────────────────────────────────────────────────────────
621
+ if (type === "everything") {
622
+ let events = record;
623
+ if (!events.length) return record;
624
+ const profile = meta && meta.profile ? meta.profile : {};
625
+
626
+ // Stamp superProps from profile for consistency
627
+ events.forEach(e => {
628
+ if (profile.api_tier) e.api_tier = profile.api_tier;
629
+ if (profile.primary_use_case) e.primary_use_case = profile.primary_use_case;
630
+ if (profile.sdk_language) e.sdk_language = profile.sdk_language;
631
+ });
632
+
633
+ // Determine first event time for relative day calculations
634
+ const sortedByTime = [...events].sort((a, b) => dayjs(a.time).valueOf() - dayjs(b.time).valueOf());
635
+ const firstEventTime = sortedByTime.length > 0 ? dayjs(sortedByTime[0].time) : DATASET_START;
636
+
637
+ // ─────────────────────────────────────────────────────
638
+ // Hook #5: TIER-BASED CONTEXT WINDOW (SUBSCRIPTION TIER)
639
+ // Scale context_window and input_tokens by tier
640
+ // ─────────────────────────────────────────────────────
641
+ const tier = profile.api_tier || "Free";
642
+ const contextWindow = tier === "Enterprise" ? 2000000 : tier === "Build" ? 1000000 : 200000;
643
+ const inputMultiplier = tier === "Enterprise" ? 4 : tier === "Build" ? 2 : 1;
644
+
645
+ events.forEach(e => {
646
+ if (e.event === "api call") {
647
+ e.context_window = contextWindow;
648
+ e.input_tokens = Math.floor((e.input_tokens || 2000) * inputMultiplier);
649
+ }
650
+ });
651
+
652
+ // ─────────────────────────────────────────────────────
653
+ // Hook #1: PROMPT CACHING ADOPTION (CONVERSION)
654
+ // Users with any cache_enabled=true get 70% cost reduction
655
+ // on all subsequent api calls
656
+ // ─────────────────────────────────────────────────────
657
+ // ~25% of users have caching enabled on at least one event
658
+ const userId = events[0] && events[0].user_id;
659
+ const idHash = String(userId || "").split("").reduce((acc, c) => acc + c.charCodeAt(0), 0);
660
+ const isCacheUser = (idHash % 4) === 0;
661
+
662
+ if (isCacheUser) {
663
+ let cacheActivated = false;
664
+ // Activate caching on events after the first 20% of user events
665
+ const activationPoint = Math.floor(events.length * 0.2);
666
+ events.forEach((e, idx) => {
667
+ if (e.event === "api call") {
668
+ if (idx >= activationPoint) {
669
+ cacheActivated = true;
670
+ }
671
+ if (cacheActivated) {
672
+ e.cache_enabled = true;
673
+ e.cost_usd = Math.round((e.cost_usd || 0.01) * 0.3 * 10000) / 10000;
674
+ }
675
+ }
676
+ });
677
+ }
678
+
679
+ // ─────────────────────────────────────────────────────
680
+ // Hook #2: MODEL MIGRATION WAVE (everything portion)
681
+ // opus-4-7 users get 1.5x tokens_used
682
+ // (model assignment done in event hook above)
683
+ // ─────────────────────────────────────────────────────
684
+ events.forEach(e => {
685
+ if (e.event === "api call" && e.model === "opus-4-7") {
686
+ e.tokens_used = Math.floor((e.tokens_used || 2500) * 1.5);
687
+ }
688
+ });
689
+
690
+ // ─────────────────────────────────────────────────────
691
+ // Hook #3: AGENTIC LOOP POWER USERS (BEHAVIORS TOGETHER)
692
+ // Users with tool use + multi_turn get 8x tokens, 3x events
693
+ // ─────────────────────────────────────────────────────
694
+ const hasToolUse = events.some(e => e.event === "tool use call");
695
+ const hasMultiTurn = events.some(e => e.event === "api call" && e.multi_turn === true);
696
+ const isAgenticUser = hasToolUse && hasMultiTurn;
697
+
698
+ if (isAgenticUser) {
699
+ // Mark all api calls as agentic and boost tokens
700
+ events.forEach(e => {
701
+ if (e.event === "api call") {
702
+ e.is_agentic_user = true;
703
+ e.tokens_used = Math.floor((e.tokens_used || 2500) * 8);
704
+ }
705
+ });
706
+
707
+ // Inject 3x extra api call events by cloning existing ones
708
+ const apiCalls = events.filter(e => e.event === "api call");
709
+ const extraCount = apiCalls.length * 2; // 2 extra per existing = 3x total
710
+ for (let i = 0; i < extraCount; i++) {
711
+ const template = apiCalls[i % apiCalls.length];
712
+ if (template) {
713
+ events.push({
714
+ ...template,
715
+ time: dayjs(template.time).add(chance.integer({ min: 1, max: 120 }), "minutes").toISOString(),
716
+ user_id: template.user_id,
717
+ is_agentic_user: true,
718
+ multi_turn: true,
719
+ });
720
+ }
721
+ }
722
+ }
723
+
724
+ // ─────────────────────────────────────────────────────
725
+ // Hook #4: RATE LIMIT CHURN
726
+ // >=5 rate limit errors in first 7 days -> remove 60% of
727
+ // events after week 1
728
+ // ─────────────────────────────────────────────────────
729
+ const firstWeekEnd = firstEventTime.add(7, "days");
730
+ const earlyRateLimits = events.filter(e =>
731
+ e.event === "rate limit error" &&
732
+ dayjs(e.time).isBefore(firstWeekEnd)
733
+ ).length;
734
+
735
+ if (earlyRateLimits >= 5) {
736
+ // Tag the user profile
737
+ if (profile) profile.hit_rate_limit_early = true;
738
+
739
+ // Remove 60% of events after week 1
740
+ events = events.filter(e => {
741
+ if (dayjs(e.time).isAfter(firstWeekEnd)) {
742
+ return chance.bool({ likelihood: 40 }); // keep 40% = remove 60%
743
+ }
744
+ return true;
745
+ });
746
+ }
747
+
748
+ // ─────────────────────────────────────────────────────
749
+ // Hook #7: BATCH API DISCOUNT (PURCHASE VALUE)
750
+ // Batch users get 50% lower cost_per_token, 2x tokens_used
751
+ // ─────────────────────────────────────────────────────
752
+ const isBatchUser = events.some(e => e.event === "batch job submitted");
753
+
754
+ if (isBatchUser) {
755
+ events.forEach(e => {
756
+ if (e.event === "api call") {
757
+ e.is_batch_user = true;
758
+ e.cost_per_token = Math.round((e.cost_per_token || 0.00001) * 0.5 * 10000000) / 10000000;
759
+ e.tokens_used = Math.floor((e.tokens_used || 2500) * 2);
760
+ }
761
+ });
762
+ }
763
+
764
+ // ─────────────────────────────────────────────────────
765
+ // Hook #8: EVAL-DRIVEN RETENTION
766
+ // Early eval users (first 7 days) get 75% D30 retention
767
+ // Non-eval users get only 25% D30 retention (remove events)
768
+ // ─────────────────────────────────────────────────────
769
+ const hasEarlyEval = events.some(e =>
770
+ e.event === "eval job" &&
771
+ dayjs(e.time).isBefore(firstWeekEnd)
772
+ );
773
+
774
+ if (hasEarlyEval) {
775
+ // Tag user profile
776
+ if (profile) {
777
+ profile.has_early_eval = true;
778
+ profile.has_eval_pipeline = true;
779
+ }
780
+ // Early eval users keep all their events (high retention)
781
+ } else {
782
+ // Non-eval users: remove 75% of events after day 30
783
+ const day30 = firstEventTime.add(30, "days");
784
+ events = events.filter(e => {
785
+ if (dayjs(e.time).isAfter(day30)) {
786
+ return chance.bool({ likelihood: 25 }); // keep 25%
787
+ }
788
+ return true;
789
+ });
790
+ }
791
+
792
+ return events;
793
+ }
794
+
795
+ return record;
796
+ },
797
+ };
798
+
799
+ export default config;