@ak--47/dungeon-master 1.5.0 → 1.5.2
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/create-dungeon/SKILL.md +139 -46
- package/.claude/skills/verify-dungeon/references/counting-semantics.md +31 -6
- package/.claude/skills/verify-dungeon/references/sql-recipes.md +44 -25
- package/.claude/skills/write-hooks/SKILL.md +31 -3
- package/CHANGELOG.md +85 -0
- package/HOOKS.md +13 -0
- package/dungeons/technical/ad-spend.js +41 -49
- package/dungeons/technical/anonymous-users.js +38 -36
- package/dungeons/technical/array-of-object-lookup.js +136 -153
- package/dungeons/technical/datagen-v15-verify.js +24 -11
- package/dungeons/technical/experiments.js +42 -40
- package/dungeons/technical/foobar.js +114 -118
- package/dungeons/technical/group-analytics.js +42 -40
- package/dungeons/technical/hook-helpers-verify.js +69 -50
- package/dungeons/technical/identity-model-verify.js +22 -12
- package/dungeons/technical/mirror-strategies.js +37 -39
- package/dungeons/technical/nested-objects.js +119 -118
- package/dungeons/technical/pattern-aggregate-by-bin.js +21 -8
- package/dungeons/technical/pattern-attributed-by-source.js +23 -9
- package/dungeons/technical/pattern-frequency-by-frequency.js +21 -8
- package/dungeons/technical/pattern-funnel-frequency.js +30 -15
- package/dungeons/technical/pattern-ttc-by-segment.js +21 -8
- package/dungeons/technical/retention-cadence.js +115 -112
- package/dungeons/technical/sanity.js +86 -80
- package/dungeons/technical/scale-test.js +34 -38
- package/dungeons/technical/scd.js +111 -128
- package/dungeons/technical/simple.js +134 -141
- package/dungeons/technical/simplest.js +54 -62
- package/dungeons/technical/text-generation.js +110 -146
- package/dungeons/vertical/ai-platform.js +296 -333
- package/dungeons/vertical/community.js +284 -255
- package/dungeons/vertical/crypto.js +395 -391
- package/dungeons/vertical/dating.js +411 -378
- package/dungeons/vertical/devtools.js +336 -298
- package/dungeons/vertical/ecommerce.js +316 -394
- package/dungeons/vertical/education.js +369 -325
- package/dungeons/vertical/fintech.js +358 -325
- package/dungeons/vertical/fitness.js +335 -291
- package/dungeons/vertical/food-delivery.js +343 -307
- package/dungeons/vertical/gaming.js +480 -444
- package/dungeons/vertical/healthcare.js +306 -262
- package/dungeons/vertical/insurance-application.js +427 -409
- package/dungeons/vertical/logistics.js +271 -252
- package/dungeons/vertical/marketplace.js +333 -323
- package/dungeons/vertical/media.js +382 -335
- package/dungeons/vertical/real-estate.js +395 -346
- package/dungeons/vertical/sass.js +319 -333
- package/dungeons/vertical/social.js +368 -316
- package/dungeons/vertical/travel.js +297 -295
- package/index.js +46 -4
- package/lib/core/config-validator.js +126 -28
- package/lib/generators/funnels.js +4 -1
- package/lib/orchestrators/mixpanel-sender.js +7 -0
- package/lib/orchestrators/user-loop.js +132 -31
- package/lib/templates/defaults.js +59 -59
- package/lib/templates/macro-presets.js +14 -2
- package/lib/utils/dataset-context.js +103 -0
- package/lib/utils/retention-curve.js +140 -0
- package/lib/utils/utils.js +149 -38
- package/lib/verify/counting.js +40 -0
- package/lib/verify/emulate-breakdown.js +20 -1
- package/lib/verify/index.js +1 -0
- package/lib/verify/schema-validator.js +3 -1
- package/package.json +11 -2
- package/scripts/run-dungeon.mjs +12 -1
- package/types.d.ts +117 -1
|
@@ -1,66 +1,44 @@
|
|
|
1
|
-
// ──
|
|
2
|
-
const SEED = "dm4-logistics";
|
|
3
|
-
const num_days = 120;
|
|
4
|
-
const num_users = 10_000;
|
|
5
|
-
const avg_events_per_user_per_day = 1.2;
|
|
6
|
-
let token = "your-mixpanel-token";
|
|
7
|
-
|
|
8
|
-
// ── env overrides ──
|
|
9
|
-
if (process.env.MP_TOKEN) token = process.env.MP_TOKEN;
|
|
10
|
-
|
|
1
|
+
// ── IMPORTS ──
|
|
11
2
|
import dayjs from "dayjs";
|
|
12
3
|
import utc from "dayjs/plugin/utc.js";
|
|
4
|
+
dayjs.extend(utc);
|
|
13
5
|
import "dotenv/config";
|
|
14
6
|
import * as u from "../../lib/utils/utils.js";
|
|
15
7
|
import * as v from "ak-tools";
|
|
16
|
-
|
|
17
|
-
dayjs.extend(utc);
|
|
18
|
-
const chance = u.initChance(SEED);
|
|
19
8
|
/** @typedef {import("../../types").Dungeon} Config */
|
|
20
9
|
|
|
21
|
-
//
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
*
|
|
31
|
-
*
|
|
32
|
-
*
|
|
33
|
-
*
|
|
34
|
-
*
|
|
35
|
-
*
|
|
36
|
-
*
|
|
37
|
-
*
|
|
38
|
-
*
|
|
39
|
-
*
|
|
40
|
-
*
|
|
41
|
-
*
|
|
42
|
-
*
|
|
43
|
-
*
|
|
44
|
-
*
|
|
45
|
-
*
|
|
46
|
-
*
|
|
47
|
-
*
|
|
48
|
-
*
|
|
49
|
-
* - supplier_id: vendor supplying goods
|
|
50
|
-
* - sku_category: product category being managed
|
|
51
|
-
* - priority: urgency level for orders and alerts
|
|
10
|
+
// ── OVERVIEW ──
|
|
11
|
+
/*
|
|
12
|
+
* NAME: SupplyStack
|
|
13
|
+
* APP: B2B warehouse/inventory management SaaS for businesses to track
|
|
14
|
+
* stock levels, purchase orders, suppliers, shipments, and quality
|
|
15
|
+
* inspections across multiple warehouses. Multi-tier system with
|
|
16
|
+
* enterprise, mid-market, small business, and trial customers.
|
|
17
|
+
* SCALE: 10,000 users, ~1.4M events, 121 days (2026-01-01 → 2026-05-01)
|
|
18
|
+
* CORE LOOP: account created → inventory checked → purchase order → order received → shipment tracked
|
|
19
|
+
*
|
|
20
|
+
* EVENTS (17):
|
|
21
|
+
* inventory checked (8) > app session (7) > shipment tracked (6) > notification received (6)
|
|
22
|
+
* > stockout alert (5) > purchase order created (5) > order received (4) > report generated (4)
|
|
23
|
+
* > supplier contacted (3) > warehouse transfer (3) > invoice processed (3)
|
|
24
|
+
* > integration connected (2) > alert configured (2) > quality inspection (2)
|
|
25
|
+
* > account created (1) > support ticket (1) > account deactivated (1)
|
|
26
|
+
*
|
|
27
|
+
* FUNNELS (5):
|
|
28
|
+
* - Onboarding: account created → inventory checked → integration connected → report generated (40%)
|
|
29
|
+
* - Order Fulfillment: inventory checked → purchase order created → order received → shipment tracked (35%, reentry)
|
|
30
|
+
* - Supplier Management: supplier contacted → purchase order created → invoice processed (45%)
|
|
31
|
+
* - Integration Setup: integration connected → report generated → alert configured (30%)
|
|
32
|
+
* - Alert Response: stockout alert → purchase order created → order received (50%)
|
|
33
|
+
*
|
|
34
|
+
* USER PROPS: company_tier, warehouse_count, employee_count, industry, integration_count, Platform, subscription_plan
|
|
35
|
+
* SUPER PROPS: Platform, subscription_plan
|
|
36
|
+
* SCD PROPS: company_tier (trial/starter/professional/enterprise, monthly fuzzy, max 6)
|
|
37
|
+
* GROUPS: none
|
|
52
38
|
*/
|
|
53
39
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
* ANALYTICS HOOKS (10 hooks)
|
|
57
|
-
*
|
|
58
|
-
* Adds 10. ONBOARDING TIME-TO-CONVERT: enterprise 0.71x faster, small_business
|
|
59
|
-
* 1.3x slower (funnel-post). Discover via Onboarding funnel median TTC by company_tier.
|
|
60
|
-
* NOTE (funnel-post measurement): visible only via Mixpanel funnel median TTC.
|
|
61
|
-
* Cross-event MIN→MIN SQL queries on raw events do NOT show this.
|
|
62
|
-
* ===================================================================
|
|
63
|
-
*
|
|
40
|
+
// ── HOOK STORIES ──
|
|
41
|
+
/*
|
|
64
42
|
* -------------------------------------------------------------------
|
|
65
43
|
* 1. MONTH-END REPORTING SURGE (event hook)
|
|
66
44
|
* -------------------------------------------------------------------
|
|
@@ -339,31 +317,246 @@ const supplierIds = v.range(1, 150).map(() => `SUP_${v.uid(6)}`);
|
|
|
339
317
|
* Onboarding TTC | funnel median TTC | 1x | 0.71x | 1.4x faster (enterprise)
|
|
340
318
|
*/
|
|
341
319
|
|
|
320
|
+
// ── SCALE ──
|
|
321
|
+
const SEED = "dm4-logistics";
|
|
322
|
+
const NUM_USERS = 10_000;
|
|
323
|
+
const DATASET_START = "2026-01-01T00:00:00Z";
|
|
324
|
+
const DATASET_END = "2026-05-01T23:59:59Z";
|
|
325
|
+
const EVENTS_PER_DAY = 1.2;
|
|
326
|
+
const token = process.env.MP_TOKEN || "your-mixpanel-token";
|
|
327
|
+
|
|
328
|
+
const chance = u.initChance(SEED);
|
|
329
|
+
|
|
330
|
+
// ── KNOBS (tweak these to reshape stories) ──
|
|
331
|
+
const MONTH_END_DAY_THRESHOLD = 28;
|
|
332
|
+
const MONTH_END_PAGES_MULT = 2.5;
|
|
333
|
+
|
|
334
|
+
const RUSH_ORDER_COST_MULT = 1.5;
|
|
335
|
+
|
|
336
|
+
const ENTERPRISE_STOCKOUT_DROP_LIKELIHOOD = 10;
|
|
337
|
+
|
|
338
|
+
const INTEGRATION_THRESHOLD = 3;
|
|
339
|
+
const INTEGRATION_REPORT_CLONE_LIKELIHOOD = 65;
|
|
340
|
+
|
|
341
|
+
const ALERT_FATIGUE_THRESHOLD = 30;
|
|
342
|
+
const ALERT_FATIGUE_START_IDX = 20;
|
|
343
|
+
const ALERT_FATIGUE_BASE_MULT = 1.5;
|
|
344
|
+
const ALERT_FATIGUE_RAMP_MULT = 1.5;
|
|
345
|
+
|
|
346
|
+
const TRIAL_CHURN_EVENT_THRESHOLD = 10;
|
|
347
|
+
const TRIAL_CHURN_CUTOFF_DAYS = 7;
|
|
348
|
+
const TRIAL_CHURN_DROP_LIKELIHOOD = 50;
|
|
349
|
+
|
|
350
|
+
const SMB_ALERT_DROP_LIKELIHOOD = 35;
|
|
351
|
+
|
|
352
|
+
const INVENTORY_SWEET_MIN = 5;
|
|
353
|
+
const INVENTORY_SWEET_MAX = 15;
|
|
354
|
+
const INVENTORY_OVER_THRESHOLD = 16;
|
|
355
|
+
const INVENTORY_PO_QUANTITY_BOOST = 1.4;
|
|
356
|
+
const INVENTORY_OVER_PO_DROP_LIKELIHOOD = 60;
|
|
357
|
+
|
|
358
|
+
const TTC_ENTERPRISE_FACTOR = 0.71;
|
|
359
|
+
const TTC_SMB_FACTOR = 1.3;
|
|
360
|
+
|
|
361
|
+
// ── DATA ARRAYS ──
|
|
362
|
+
const warehouseIds = v.range(1, 80).map(() => `WH_${v.uid(6)}`);
|
|
363
|
+
const supplierIds = v.range(1, 150).map(() => `SUP_${v.uid(6)}`);
|
|
364
|
+
|
|
365
|
+
// ── HELPER FUNCTIONS ──
|
|
366
|
+
function handleUserHooks(record) {
|
|
367
|
+
// H7: ENTERPRISE PROFILES — large warehouse_count + employee_count by tier.
|
|
368
|
+
if (record.company_tier === "enterprise") {
|
|
369
|
+
record.warehouse_count = chance.integer({ min: 5, max: 15 });
|
|
370
|
+
record.employee_count = chance.integer({ min: 200, max: 2000 });
|
|
371
|
+
} else if (record.company_tier === "mid_market") {
|
|
372
|
+
record.warehouse_count = chance.integer({ min: 2, max: 6 });
|
|
373
|
+
record.employee_count = chance.integer({ min: 20, max: 200 });
|
|
374
|
+
} else if (record.company_tier === "small_business") {
|
|
375
|
+
record.warehouse_count = chance.integer({ min: 1, max: 3 });
|
|
376
|
+
record.employee_count = chance.integer({ min: 5, max: 80 });
|
|
377
|
+
} else if (record.company_tier === "trial") {
|
|
378
|
+
record.warehouse_count = 1;
|
|
379
|
+
record.employee_count = chance.integer({ min: 1, max: 10 });
|
|
380
|
+
}
|
|
381
|
+
return record;
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
function handleEventHooks(record) {
|
|
385
|
+
// H2: RUSH ORDER PREMIUM — urgent purchase orders get 1.5x unit_cost.
|
|
386
|
+
if (record.event === "purchase order created" && record.priority === "urgent") {
|
|
387
|
+
record.unit_cost = Math.floor((record.unit_cost || 50) * RUSH_ORDER_COST_MULT);
|
|
388
|
+
}
|
|
389
|
+
return record;
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
function handleFunnelPostHooks(record, meta) {
|
|
393
|
+
// H10: ONBOARDING TIME-TO-CONVERT — enterprise 1.4x faster (0.71);
|
|
394
|
+
// small_business + trial 1.3x slower (1.3).
|
|
395
|
+
const segment = meta?.profile?.company_tier;
|
|
396
|
+
if (Array.isArray(record) && record.length > 1) {
|
|
397
|
+
const factor = (
|
|
398
|
+
segment === "enterprise" ? TTC_ENTERPRISE_FACTOR :
|
|
399
|
+
segment === "small_business" || segment === "trial" ? TTC_SMB_FACTOR :
|
|
400
|
+
1.0
|
|
401
|
+
);
|
|
402
|
+
if (factor !== 1.0) {
|
|
403
|
+
for (let i = 1; i < record.length; i++) {
|
|
404
|
+
const prev = dayjs(record[i - 1].time);
|
|
405
|
+
const newGap = Math.round(dayjs(record[i].time).diff(prev) * factor);
|
|
406
|
+
record[i].time = prev.add(newGap, "milliseconds").toISOString();
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
return record;
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
function handleEverythingHooks(record, meta) {
|
|
414
|
+
if (!record.length) return record;
|
|
415
|
+
const profile = meta.profile;
|
|
416
|
+
|
|
417
|
+
// ── SUPER-PROP STAMPING ──
|
|
418
|
+
// Stamp superProps from profile so they are consistent per-user.
|
|
419
|
+
if (profile) {
|
|
420
|
+
const plat = profile.Platform;
|
|
421
|
+
const plan = profile.subscription_plan;
|
|
422
|
+
record.forEach(e => {
|
|
423
|
+
if (plat) e.Platform = plat;
|
|
424
|
+
if (plan) e.subscription_plan = plan;
|
|
425
|
+
});
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
// H1: MONTH-END REPORTING SURGE — reports on calendar days 28-31 get
|
|
429
|
+
// 2.5x report_pages. Runs in everything (post-sessionization) so the
|
|
430
|
+
// day-of-month tag matches the final timestamp.
|
|
431
|
+
for (const e of record) {
|
|
432
|
+
if (e.event === "report generated") {
|
|
433
|
+
const dayOfMonth = new Date(e.time).getUTCDate();
|
|
434
|
+
if (dayOfMonth >= MONTH_END_DAY_THRESHOLD) {
|
|
435
|
+
e.report_pages = Math.floor((e.report_pages || 20) * MONTH_END_PAGES_MULT);
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
// H8: SMALL-BUSINESS CONVERSION DROP — small_business users lose
|
|
441
|
+
// ~35% of "alert configured" events (last step of Integration Setup
|
|
442
|
+
// funnel), simulating lower conversion without dedicated IT.
|
|
443
|
+
if (profile && profile.company_tier === "small_business") {
|
|
444
|
+
record = record.filter(e => {
|
|
445
|
+
if (e.event === "alert configured" && chance.bool({ likelihood: SMB_ALERT_DROP_LIKELIHOOD })) {
|
|
446
|
+
return false;
|
|
447
|
+
}
|
|
448
|
+
return true;
|
|
449
|
+
});
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
// H3: REORDER ACCURACY BY TIER — enterprise users get 10% of
|
|
453
|
+
// stockout alerts removed (better forecasting + safety stock).
|
|
454
|
+
if (profile && profile.company_tier === "enterprise") {
|
|
455
|
+
for (let i = record.length - 1; i >= 0; i--) {
|
|
456
|
+
if (record[i].event === "stockout alert" && chance.bool({ likelihood: ENTERPRISE_STOCKOUT_DROP_LIKELIHOOD })) {
|
|
457
|
+
record.splice(i, 1);
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
// H4: INTEGRATION COMPLETION DRIVES RETENTION — users with 3+
|
|
463
|
+
// "integration connected" events get cloned "report generated" events.
|
|
464
|
+
const integrationCount = record.filter(e => e.event === "integration connected").length;
|
|
465
|
+
if (integrationCount >= INTEGRATION_THRESHOLD) {
|
|
466
|
+
const templateReport = record.find(e => e.event === "report generated");
|
|
467
|
+
if (templateReport) {
|
|
468
|
+
const integrationEvents = record.filter(e => e.event === "integration connected");
|
|
469
|
+
integrationEvents.forEach(ie => {
|
|
470
|
+
if (chance.bool({ likelihood: INTEGRATION_REPORT_CLONE_LIKELIHOOD })) {
|
|
471
|
+
record.push({
|
|
472
|
+
...templateReport,
|
|
473
|
+
time: dayjs(ie.time).add(chance.integer({ min: 1, max: 5 }), "days").toISOString(),
|
|
474
|
+
user_id: ie.user_id,
|
|
475
|
+
report_type: "integration_summary",
|
|
476
|
+
report_pages: chance.integer({ min: 5, max: 25 }),
|
|
477
|
+
});
|
|
478
|
+
}
|
|
479
|
+
});
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
// H5: ALERT FATIGUE — users with >30 stockout alerts get increasing
|
|
484
|
+
// response_time_hours starting at the 20th alert.
|
|
485
|
+
const alertEvents = record.filter(e => e.event === "stockout alert");
|
|
486
|
+
if (alertEvents.length > ALERT_FATIGUE_THRESHOLD) {
|
|
487
|
+
alertEvents.forEach((alert, idx) => {
|
|
488
|
+
if (idx >= ALERT_FATIGUE_START_IDX) {
|
|
489
|
+
const fatigueMultiplier = ALERT_FATIGUE_BASE_MULT + ((idx - ALERT_FATIGUE_START_IDX) / alertEvents.length) * ALERT_FATIGUE_RAMP_MULT;
|
|
490
|
+
alert.response_time_hours = Math.floor((alert.response_time_hours || 4) * fatigueMultiplier);
|
|
491
|
+
}
|
|
492
|
+
});
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
// H6: TRIAL CHURN — users with <10 total events lose 50% after day 7.
|
|
496
|
+
if (record.length < TRIAL_CHURN_EVENT_THRESHOLD) {
|
|
497
|
+
const userFirstEvent = record[0];
|
|
498
|
+
if (userFirstEvent) {
|
|
499
|
+
const firstTime = dayjs(userFirstEvent.time);
|
|
500
|
+
const cutoff = firstTime.add(TRIAL_CHURN_CUTOFF_DAYS, "days");
|
|
501
|
+
for (let i = record.length - 1; i >= 0; i--) {
|
|
502
|
+
if (dayjs(record[i].time).isAfter(cutoff) && chance.bool({ likelihood: TRIAL_CHURN_DROP_LIKELIHOOD })) {
|
|
503
|
+
record.splice(i, 1);
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
// H9: INVENTORY-CHECK MAGIC NUMBER (no flags) — sweet 5-15 inventory
|
|
510
|
+
// checks → +25% PO quantity (1.4x to overcome dilution); over 16+ →
|
|
511
|
+
// drop 60% of PO created events.
|
|
512
|
+
const invCheckCount = record.filter(e => e.event === "inventory checked").length;
|
|
513
|
+
if (invCheckCount >= INVENTORY_SWEET_MIN && invCheckCount <= INVENTORY_SWEET_MAX) {
|
|
514
|
+
record.forEach(e => {
|
|
515
|
+
if (e.event === "purchase order created" && typeof e.quantity === "number") {
|
|
516
|
+
e.quantity = Math.round(e.quantity * INVENTORY_PO_QUANTITY_BOOST);
|
|
517
|
+
}
|
|
518
|
+
});
|
|
519
|
+
} else if (invCheckCount >= INVENTORY_OVER_THRESHOLD) {
|
|
520
|
+
for (let i = record.length - 1; i >= 0; i--) {
|
|
521
|
+
if (record[i].event === "purchase order created" && chance.bool({ likelihood: INVENTORY_OVER_PO_DROP_LIKELIHOOD })) {
|
|
522
|
+
record.splice(i, 1);
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
return record;
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
// ── CONFIG ──
|
|
342
531
|
/** @type {Config} */
|
|
343
532
|
const config = {
|
|
344
533
|
version: 2,
|
|
345
|
-
token,
|
|
346
534
|
seed: SEED,
|
|
347
|
-
datasetStart:
|
|
348
|
-
datasetEnd:
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
numUsers: num_users,
|
|
352
|
-
hasAnonIds: true,
|
|
353
|
-
avgDevicePerUser: 2,
|
|
354
|
-
hasSessionIds: true,
|
|
535
|
+
datasetStart: DATASET_START,
|
|
536
|
+
datasetEnd: DATASET_END,
|
|
537
|
+
avgEventsPerUserPerDay: EVENTS_PER_DAY,
|
|
538
|
+
numUsers: NUM_USERS,
|
|
355
539
|
format: "json",
|
|
356
540
|
gzip: true,
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
541
|
+
credentials: {
|
|
542
|
+
token,
|
|
543
|
+
},
|
|
544
|
+
switches: {
|
|
545
|
+
hasSessionIds: true,
|
|
546
|
+
alsoInferFunnels: false,
|
|
547
|
+
hasLocation: true,
|
|
548
|
+
hasAndroidDevices: false,
|
|
549
|
+
hasIOSDevices: false,
|
|
550
|
+
hasDesktopDevices: true,
|
|
551
|
+
hasBrowser: true,
|
|
552
|
+
hasCampaigns: false,
|
|
553
|
+
isAnonymous: false,
|
|
554
|
+
hasAdSpend: false,
|
|
555
|
+
hasAvatar: true,
|
|
556
|
+
},
|
|
557
|
+
identity: {
|
|
558
|
+
avgDevicePerUser: 2,
|
|
559
|
+
},
|
|
367
560
|
concurrency: 1,
|
|
368
561
|
writeToDisk: false,
|
|
369
562
|
scdProps: {
|
|
@@ -781,185 +974,11 @@ const config = {
|
|
|
781
974
|
},
|
|
782
975
|
],
|
|
783
976
|
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
if (type === "funnel-post") {
|
|
790
|
-
const segment = meta?.profile?.company_tier;
|
|
791
|
-
if (Array.isArray(record) && record.length > 1) {
|
|
792
|
-
const factor = (
|
|
793
|
-
segment === "enterprise" ? 0.71 :
|
|
794
|
-
segment === "small_business" || segment === "trial" ? 1.3 :
|
|
795
|
-
1.0
|
|
796
|
-
);
|
|
797
|
-
if (factor !== 1.0) {
|
|
798
|
-
for (let i = 1; i < record.length; i++) {
|
|
799
|
-
const prev = dayjs(record[i - 1].time);
|
|
800
|
-
const newGap = Math.round(dayjs(record[i].time).diff(prev) * factor);
|
|
801
|
-
record[i].time = prev.add(newGap, "milliseconds").toISOString();
|
|
802
|
-
}
|
|
803
|
-
}
|
|
804
|
-
}
|
|
805
|
-
}
|
|
806
|
-
|
|
807
|
-
// -- HOOK 7: ENTERPRISE PROFILES (user) -----------------------
|
|
808
|
-
// Enterprise ops get large warehouse_count and employee_count.
|
|
809
|
-
if (type === "user") {
|
|
810
|
-
if (record.company_tier === "enterprise") {
|
|
811
|
-
record.warehouse_count = chance.integer({ min: 5, max: 15 });
|
|
812
|
-
record.employee_count = chance.integer({ min: 200, max: 2000 });
|
|
813
|
-
} else if (record.company_tier === "mid_market") {
|
|
814
|
-
record.warehouse_count = chance.integer({ min: 2, max: 6 });
|
|
815
|
-
record.employee_count = chance.integer({ min: 20, max: 200 });
|
|
816
|
-
} else if (record.company_tier === "small_business") {
|
|
817
|
-
record.warehouse_count = chance.integer({ min: 1, max: 3 });
|
|
818
|
-
record.employee_count = chance.integer({ min: 5, max: 80 });
|
|
819
|
-
} else if (record.company_tier === "trial") {
|
|
820
|
-
record.warehouse_count = 1;
|
|
821
|
-
record.employee_count = chance.integer({ min: 1, max: 10 });
|
|
822
|
-
}
|
|
823
|
-
}
|
|
824
|
-
|
|
825
|
-
// -- HOOK 8: ENTERPRISE INTEGRATION FUNNEL LIFT (funnel-pre) --
|
|
826
|
-
// (conversionRate boost removed — filtering applied in everything hook instead)
|
|
827
|
-
if (type === "funnel-pre") {
|
|
828
|
-
// no-op: conversion differentiation handled via event filtering below
|
|
829
|
-
}
|
|
830
|
-
|
|
831
|
-
// -- HOOK 1: MONTH-END REPORTING SURGE ------------------------
|
|
832
|
-
// Moved to everything hook (after sessionization) so day-of-month
|
|
833
|
-
// tags match final timestamps. See everything hook below.
|
|
834
|
-
|
|
835
|
-
// -- HOOK 2: RUSH ORDER PREMIUM (event) -------------------
|
|
836
|
-
// Urgent purchase orders get 1.5x unit_cost.
|
|
837
|
-
if (type === "event") {
|
|
838
|
-
if (record.event === "purchase order created" && record.priority === "urgent") {
|
|
839
|
-
record.unit_cost = Math.floor((record.unit_cost || 50) * 1.5);
|
|
840
|
-
}
|
|
841
|
-
}
|
|
842
|
-
|
|
843
|
-
// -- EVERYTHING HOOKS -----------------------------------------
|
|
844
|
-
if (type === "everything") {
|
|
845
|
-
if (!record.length) return record;
|
|
846
|
-
const profile = meta.profile;
|
|
847
|
-
|
|
848
|
-
// -- SUPER-PROP STAMPING ----------------------------------
|
|
849
|
-
// Stamp superProps from profile so they are consistent per-user.
|
|
850
|
-
if (profile) {
|
|
851
|
-
const plat = profile.Platform;
|
|
852
|
-
const plan = profile.subscription_plan;
|
|
853
|
-
record.forEach(e => {
|
|
854
|
-
if (plat) e.Platform = plat;
|
|
855
|
-
if (plan) e.subscription_plan = plan;
|
|
856
|
-
});
|
|
857
|
-
}
|
|
858
|
-
|
|
859
|
-
// -- HOOK 1: MONTH-END REPORTING SURGE --------------------
|
|
860
|
-
// Reports on days 28-31 get 2x report_pages.
|
|
861
|
-
// Runs after sessionization so day-of-month matches final timestamps.
|
|
862
|
-
for (const e of record) {
|
|
863
|
-
if (e.event === 'report generated') {
|
|
864
|
-
const dayOfMonth = new Date(e.time).getUTCDate();
|
|
865
|
-
if (dayOfMonth >= 28) {
|
|
866
|
-
e.report_pages = Math.floor((e.report_pages || 20) * 2.5);
|
|
867
|
-
}
|
|
868
|
-
}
|
|
869
|
-
}
|
|
870
|
-
|
|
871
|
-
// -- HOOK 8: SMALL-BUSINESS CONVERSION DROP ---------------
|
|
872
|
-
// Small-business users lose ~35% of "alert configured" events
|
|
873
|
-
// (last step of Integration Setup funnel), simulating lower
|
|
874
|
-
// conversion for smaller teams without dedicated IT resources.
|
|
875
|
-
if (profile && profile.company_tier === "small_business") {
|
|
876
|
-
record = record.filter(e => {
|
|
877
|
-
if (e.event === "alert configured" && chance.bool({ likelihood: 35 })) {
|
|
878
|
-
return false;
|
|
879
|
-
}
|
|
880
|
-
return true;
|
|
881
|
-
});
|
|
882
|
-
}
|
|
883
|
-
|
|
884
|
-
// -- HOOK 3: REORDER ACCURACY BY TIER ---------------------
|
|
885
|
-
// Enterprise users get 10% of stockout alerts removed.
|
|
886
|
-
if (profile && profile.company_tier === "enterprise") {
|
|
887
|
-
for (let i = record.length - 1; i >= 0; i--) {
|
|
888
|
-
if (record[i].event === "stockout alert" && chance.bool({ likelihood: 10 })) {
|
|
889
|
-
record.splice(i, 1);
|
|
890
|
-
}
|
|
891
|
-
}
|
|
892
|
-
}
|
|
893
|
-
|
|
894
|
-
// -- HOOK 4: INTEGRATION COMPLETION DRIVES RETENTION ------
|
|
895
|
-
// Users with 3+ integration events get cloned report events.
|
|
896
|
-
const integrationCount = record.filter(e => e.event === "integration connected").length;
|
|
897
|
-
if (integrationCount >= 3) {
|
|
898
|
-
const templateReport = record.find(e => e.event === "report generated");
|
|
899
|
-
if (templateReport) {
|
|
900
|
-
const integrationEvents = record.filter(e => e.event === "integration connected");
|
|
901
|
-
integrationEvents.forEach(ie => {
|
|
902
|
-
if (chance.bool({ likelihood: 65 })) {
|
|
903
|
-
record.push({
|
|
904
|
-
...templateReport,
|
|
905
|
-
time: dayjs(ie.time).add(chance.integer({ min: 1, max: 5 }), "days").toISOString(),
|
|
906
|
-
user_id: ie.user_id,
|
|
907
|
-
report_type: "integration_summary",
|
|
908
|
-
report_pages: chance.integer({ min: 5, max: 25 }),
|
|
909
|
-
});
|
|
910
|
-
}
|
|
911
|
-
});
|
|
912
|
-
}
|
|
913
|
-
}
|
|
914
|
-
|
|
915
|
-
// -- HOOK 5: ALERT FATIGUE --------------------------------
|
|
916
|
-
// Users with >30 stockout alerts get increasing response times.
|
|
917
|
-
const alertEvents = record.filter(e => e.event === "stockout alert");
|
|
918
|
-
if (alertEvents.length > 30) {
|
|
919
|
-
alertEvents.forEach((alert, idx) => {
|
|
920
|
-
if (idx >= 20) {
|
|
921
|
-
const fatigueMultiplier = 1.5 + ((idx - 20) / alertEvents.length) * 1.5;
|
|
922
|
-
alert.response_time_hours = Math.floor((alert.response_time_hours || 4) * fatigueMultiplier);
|
|
923
|
-
}
|
|
924
|
-
});
|
|
925
|
-
}
|
|
926
|
-
|
|
927
|
-
// -- HOOK 6: TRIAL CHURN ----------------------------------
|
|
928
|
-
// Users with <10 events lose 50% after day 7.
|
|
929
|
-
if (record.length < 10) {
|
|
930
|
-
const userFirstEvent = record[0];
|
|
931
|
-
if (userFirstEvent) {
|
|
932
|
-
const firstTime = dayjs(userFirstEvent.time);
|
|
933
|
-
const cutoff = firstTime.add(7, "days");
|
|
934
|
-
for (let i = record.length - 1; i >= 0; i--) {
|
|
935
|
-
if (dayjs(record[i].time).isAfter(cutoff) && chance.bool({ likelihood: 50 })) {
|
|
936
|
-
record.splice(i, 1);
|
|
937
|
-
}
|
|
938
|
-
}
|
|
939
|
-
}
|
|
940
|
-
}
|
|
941
|
-
|
|
942
|
-
// -- HOOK 9: INVENTORY-CHECK MAGIC NUMBER (no flags) ------
|
|
943
|
-
// Sweet 5-15 inventory checks → +25% PO quantity.
|
|
944
|
-
// Over 16+ → drop 60% of PO created events (high rate to overcome persona event multiplier dilution).
|
|
945
|
-
const invCheckCount = record.filter(e => e.event === 'inventory checked').length;
|
|
946
|
-
if (invCheckCount >= 5 && invCheckCount <= 15) {
|
|
947
|
-
record.forEach(e => {
|
|
948
|
-
if (e.event === 'purchase order created' && typeof e.quantity === 'number') {
|
|
949
|
-
e.quantity = Math.round(e.quantity * 1.4);
|
|
950
|
-
}
|
|
951
|
-
});
|
|
952
|
-
} else if (invCheckCount >= 16) {
|
|
953
|
-
for (let i = record.length - 1; i >= 0; i--) {
|
|
954
|
-
if (record[i].event === 'purchase order created' && chance.bool({ likelihood: 60 })) {
|
|
955
|
-
record.splice(i, 1);
|
|
956
|
-
}
|
|
957
|
-
}
|
|
958
|
-
}
|
|
959
|
-
|
|
960
|
-
return record;
|
|
961
|
-
}
|
|
962
|
-
|
|
977
|
+
hook(record, type, meta) {
|
|
978
|
+
if (type === "user") return handleUserHooks(record);
|
|
979
|
+
if (type === "event") return handleEventHooks(record);
|
|
980
|
+
if (type === "funnel-post") return handleFunnelPostHooks(record, meta);
|
|
981
|
+
if (type === "everything") return handleEverythingHooks(record, meta);
|
|
963
982
|
return record;
|
|
964
983
|
},
|
|
965
984
|
};
|