@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,70 +1,57 @@
|
|
|
1
|
-
// ──
|
|
2
|
-
const SEED = "harness-sass";
|
|
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
8
|
import { findFirstSequence, scaleFunnelTTC } from "../../lib/hook-helpers/timing.js";
|
|
17
|
-
|
|
18
|
-
dayjs.extend(utc);
|
|
19
|
-
const chance = u.initChance(SEED);
|
|
20
|
-
|
|
21
9
|
/** @typedef {import("../../types").Dungeon} Config */
|
|
22
10
|
|
|
11
|
+
// ── OVERVIEW ──
|
|
23
12
|
/*
|
|
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
|
-
*
|
|
50
|
-
*
|
|
51
|
-
*
|
|
13
|
+
* NAME: CloudForge
|
|
14
|
+
* APP: B2B SaaS that fuses infrastructure monitoring (Datadog-style) with
|
|
15
|
+
* deployment automation (Terraform-style). Engineering teams create
|
|
16
|
+
* workspaces, deploy services across AWS/GCP/Azure, monitor uptime
|
|
17
|
+
* and cost, and respond to alerts via Slack/PagerDuty runbooks.
|
|
18
|
+
* Pricing: Free / Team / Business / Enterprise (seats + usage).
|
|
19
|
+
* SCALE: 10,000 users, ~1.4M events, 121 days (2026-01-01 → 2026-05-01)
|
|
20
|
+
* CORE LOOP: workspace created → service deployed → dashboard viewed → alert/resolve
|
|
21
|
+
*
|
|
22
|
+
* EVENTS (19):
|
|
23
|
+
* dashboard viewed (20) > api call (16) > query executed (15) > alert triggered (12)
|
|
24
|
+
* > service deployed (10) > deployment pipeline run (9) > alert acknowledged (8)
|
|
25
|
+
* > alert resolved (7) > documentation viewed (7) > security scan (6)
|
|
26
|
+
* > infrastructure scaled (5) > cost report generated (4) > integration configured (4)
|
|
27
|
+
* > feature flag toggled (4) > team member invited (3) > runbook executed (3)
|
|
28
|
+
* > billing event (3) > workspace created (1) > incident created (1)
|
|
29
|
+
*
|
|
30
|
+
* FUNNELS (8):
|
|
31
|
+
* - Onboarding: workspace created → service deployed → dashboard viewed (70%)
|
|
32
|
+
* - Daily Monitoring: dashboard viewed → query executed → api call (80%)
|
|
33
|
+
* - Incident Response: alert triggered → alert acknowledged → alert resolved (55%)
|
|
34
|
+
* - Deployment: deployment pipeline run → service deployed → dashboard viewed (65%, Canary A/B)
|
|
35
|
+
* - Infrastructure Mgmt: cost report generated → infrastructure scaled → security scan (50%)
|
|
36
|
+
* - Team & Config: team member invited → integration configured → feature flag toggled (40%)
|
|
37
|
+
* - Docs & Runbooks: documentation viewed → runbook executed → service deployed (45%)
|
|
38
|
+
* - Billing: billing event → dashboard viewed (60%)
|
|
39
|
+
*
|
|
40
|
+
* USER PROPS: company_size, primary_role, team_name, seat_count, annual_contract_value,
|
|
41
|
+
* customer_success_manager, customer_health_score, plan_tier, cloud_provider
|
|
42
|
+
* SUPER PROPS: plan_tier, cloud_provider
|
|
43
|
+
* SCD PROPS: primary_role (viewer/editor/admin/owner, monthly fuzzy, max 6),
|
|
44
|
+
* plan_tier (starter/growth/enterprise/scale, monthly fixed, max 6, company_id-scoped)
|
|
45
|
+
* GROUPS: company_id (300 companies)
|
|
52
46
|
*/
|
|
53
47
|
|
|
48
|
+
// ── HOOK STORIES ──
|
|
54
49
|
/*
|
|
55
|
-
*
|
|
56
|
-
*
|
|
57
|
-
* ═══════════════════════════════════════════════════════════════════════════════
|
|
58
|
-
*
|
|
59
|
-
* 10 deliberately architected patterns hidden in the data. NOTE: All cohort
|
|
60
|
-
* effects are HIDDEN — no flag stamping. Discoverable via behavioral cohorts
|
|
61
|
-
* or raw-prop breakdowns (company_size, day, doc_section). Adds:
|
|
62
|
-
* 9. INCIDENT RESPONSE TIME-TO-CONVERT (Enterprise 0.67x gap vs Startup 1.5x)
|
|
63
|
-
* [everything hook: scales response_time_mins and resolution_time_mins by company_size]
|
|
64
|
-
* 10. DOCS MAGIC NUMBER (sweet 4-7 docs → +40% deploys; over 8+ → drop 25%)
|
|
50
|
+
* NOTE: All cohort effects are HIDDEN — no flag stamping. Discoverable via
|
|
51
|
+
* behavioral cohorts or raw-prop breakdowns (company_size, day, doc_section).
|
|
65
52
|
*
|
|
66
53
|
* ─────────────────────────────────────────────────────────────────────────────
|
|
67
|
-
* 1. END-OF-QUARTER SPIKE (
|
|
54
|
+
* 1. END-OF-QUARTER SPIKE (everything)
|
|
68
55
|
* ─────────────────────────────────────────────────────────────────────────────
|
|
69
56
|
*
|
|
70
57
|
* PATTERN: Days 100-110 billing events shift event_type toward "plan_upgraded"
|
|
@@ -217,13 +204,13 @@ const chance = u.initChance(SEED);
|
|
|
217
204
|
* downscale; no engineer ignores a 25% month-over-month cost jump.
|
|
218
205
|
*
|
|
219
206
|
* ─────────────────────────────────────────────────────────────────────────────
|
|
220
|
-
* 7. FAILED DEPLOYMENT RECOVERY (
|
|
207
|
+
* 7. FAILED DEPLOYMENT RECOVERY (everything)
|
|
221
208
|
* ─────────────────────────────────────────────────────────────────────────────
|
|
222
209
|
*
|
|
223
210
|
* PATTERN: After a failed pipeline run, the user's next successful deploy has
|
|
224
|
-
* duration_sec * 1.5 (recovery deploys are slower).
|
|
225
|
-
*
|
|
226
|
-
*
|
|
211
|
+
* duration_sec * 1.5 (recovery deploys are slower). No flag — discover by
|
|
212
|
+
* sequencing failed → next-success pipeline events per user and comparing
|
|
213
|
+
* duration.
|
|
227
214
|
*
|
|
228
215
|
* HOW TO FIND IT IN MIXPANEL:
|
|
229
216
|
*
|
|
@@ -397,73 +384,292 @@ const chance = u.initChance(SEED);
|
|
|
397
384
|
* Docs Magic Number | over (8+) deploys/user | 1x | ~0.75x | -25%
|
|
398
385
|
* Deploy Experiment | Canary conversion | 65% | ~78% | 1.2x
|
|
399
386
|
* Deploy Experiment | Canary TTC | 1d | ~0.85d | 0.85x
|
|
400
|
-
*
|
|
401
|
-
* ─────────────────────────────────────────────────────────────────────────────
|
|
402
|
-
* ADVANCED ANALYSIS IDEAS
|
|
403
|
-
* ─────────────────────────────────────────────────────────────────────────────
|
|
404
|
-
*
|
|
405
|
-
* CROSS-HOOK PATTERNS:
|
|
406
|
-
* - Churned + Enterprise: Do churned accounts skew toward startups or are
|
|
407
|
-
* enterprise accounts also silenced?
|
|
408
|
-
* - Integration + Cost: Do teams with full integrations manage costs better?
|
|
409
|
-
* - Docs + Deploys + Failures: Do docs readers have fewer failed deployments?
|
|
410
|
-
* - Quarter Spike + Churn: Are quarter-end upgrades correlated with later churn?
|
|
411
|
-
* - Enterprise Recovery: Do enterprise customers recover from failed deploys
|
|
412
|
-
* differently than startups?
|
|
413
|
-
*
|
|
414
|
-
* COHORT ANALYSIS:
|
|
415
|
-
* - By company_size: Compare all metrics across startup/smb/mid_market/enterprise
|
|
416
|
-
* - By plan_tier: Free vs. Team vs. Business vs. Enterprise engagement
|
|
417
|
-
* - By cloud_provider: AWS vs. GCP vs. Azure deployment and alert patterns
|
|
418
|
-
* - By primary_role: Engineer vs. SRE vs. DevOps vs. Manager behaviors
|
|
419
|
-
*
|
|
420
|
-
* KEY METRICS:
|
|
421
|
-
* - MTTR: alert triggered → alert resolved duration
|
|
422
|
-
* - Deployment Frequency: service deployed per user per week
|
|
423
|
-
* - Deployment Success Rate: pipeline success vs. failure ratio
|
|
424
|
-
* - Cost Efficiency: total_cost trend over time per company
|
|
425
|
-
* - Feature Adoption: integration configured events by type
|
|
426
|
-
* - Documentation Engagement: documentation viewed by section
|
|
427
387
|
*/
|
|
428
388
|
|
|
429
|
-
//
|
|
389
|
+
// ── SCALE ──
|
|
390
|
+
const SEED = "harness-sass";
|
|
391
|
+
const NUM_USERS = 10_000;
|
|
392
|
+
const DATASET_START = "2026-01-01T00:00:00Z";
|
|
393
|
+
const DATASET_END = "2026-05-01T23:59:59Z";
|
|
394
|
+
const EVENTS_PER_DAY = 1.2;
|
|
395
|
+
const token = process.env.MP_TOKEN || "your-mixpanel-token";
|
|
396
|
+
|
|
397
|
+
const chance = u.initChance(SEED);
|
|
398
|
+
|
|
399
|
+
// ── KNOBS (tweak these to reshape stories) ──
|
|
400
|
+
const EOQ_START_DAY = 100;
|
|
401
|
+
const EOQ_END_DAY = 110;
|
|
402
|
+
const EOQ_UPGRADE_LIKELIHOOD = 40;
|
|
403
|
+
const EOQ_INVITE_CLONE_LIKELIHOOD = 50;
|
|
404
|
+
|
|
405
|
+
const CHURN_USER_HASH_MOD = 5;
|
|
406
|
+
const CHURN_CUTOFF_DAYS = 30;
|
|
407
|
+
|
|
408
|
+
const ALERT_ESCALATION_LIKELIHOOD = 30;
|
|
409
|
+
|
|
410
|
+
const INTEGRATION_RESPONSE_FACTOR = 0.4;
|
|
411
|
+
const INTEGRATION_RESOLUTION_FACTOR = 0.5;
|
|
412
|
+
|
|
413
|
+
const COST_OVERRUN_THRESHOLD = 25;
|
|
414
|
+
|
|
415
|
+
const FAILED_DEPLOY_RECOVERY_MULT = 1.5;
|
|
416
|
+
|
|
417
|
+
const DOCS_SWEET_MIN = 4;
|
|
418
|
+
const DOCS_SWEET_MAX = 7;
|
|
419
|
+
const DOCS_OVER_THRESHOLD = 8;
|
|
420
|
+
const DOCS_EXTRA_DEPLOYS_MIN = 2;
|
|
421
|
+
const DOCS_EXTRA_DEPLOYS_MAX = 3;
|
|
422
|
+
const DOCS_DEPLOY_DROP_LIKELIHOOD = 25;
|
|
423
|
+
|
|
424
|
+
const TTC_ENTERPRISE_FACTOR = 0.67;
|
|
425
|
+
const TTC_STARTUP_FACTOR = 1.5;
|
|
426
|
+
const INCIDENT_SEQ_WINDOW_MINS = 60 * 24 * 30;
|
|
427
|
+
|
|
428
|
+
// ── DATA ARRAYS ──
|
|
430
429
|
const serviceIds = v.range(1, 201).map(() => `svc_${v.uid(8)}`);
|
|
431
430
|
const alertIds = v.range(1, 501).map(() => `alert_${v.uid(6)}`);
|
|
432
431
|
const pipelineIds = v.range(1, 101).map(() => `pipe_${v.uid(6)}`);
|
|
433
432
|
const runbookIds = v.range(1, 51).map(() => `rb_${v.uid(6)}`);
|
|
434
|
-
const companyIds = v.range(1, 301).map(() => `comp_${v.uid(8)}`);
|
|
435
433
|
|
|
436
|
-
//
|
|
434
|
+
// ── HOOK STATE ──
|
|
435
|
+
// Module-level Map for closure-based state tracking across event-hook calls
|
|
437
436
|
const costOverrunUsers = new Map();
|
|
438
|
-
const failedDeployUsers = new Map();
|
|
439
437
|
|
|
438
|
+
// ── HELPER FUNCTIONS ──
|
|
439
|
+
function handleEventHooks(record) {
|
|
440
|
+
// H3: ALERT ESCALATION REPLACEMENT — critical/emergency alerts sometimes
|
|
441
|
+
// become incident-created events.
|
|
442
|
+
if (record.event === "alert triggered") {
|
|
443
|
+
const severity = record.severity;
|
|
444
|
+
if ((severity === "critical" || severity === "emergency") && chance.bool({ likelihood: ALERT_ESCALATION_LIKELIHOOD })) {
|
|
445
|
+
return {
|
|
446
|
+
...record,
|
|
447
|
+
event: "incident created",
|
|
448
|
+
escalation_level: chance.pickone(["P1", "P2"]),
|
|
449
|
+
teams_paged: chance.integer({ min: 1, max: 5 }),
|
|
450
|
+
incident_id: `inc_${v.uid(8)}`,
|
|
451
|
+
original_severity: severity,
|
|
452
|
+
original_alert_type: record.alert_type,
|
|
453
|
+
auto_escalated: true,
|
|
454
|
+
};
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
// H6: COST OVERRUN PATTERN — cost reports with cost_change > 25% record
|
|
459
|
+
// the user, then the next infrastructure-scaled event from that user is
|
|
460
|
+
// forced to scale_direction = "down".
|
|
461
|
+
if (record.event === "cost report generated" && record.cost_change_percent > COST_OVERRUN_THRESHOLD) {
|
|
462
|
+
costOverrunUsers.set(record.user_id, true);
|
|
463
|
+
}
|
|
464
|
+
if (record.event === "infrastructure scaled" && costOverrunUsers.has(record.user_id)) {
|
|
465
|
+
record.scale_direction = "down";
|
|
466
|
+
costOverrunUsers.delete(record.user_id);
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
return record;
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
function handleUserHooks(record) {
|
|
473
|
+
// H8: ENTERPRISE VS STARTUP — company size determines seat count, ACV,
|
|
474
|
+
// and CSM. Real profile attrs.
|
|
475
|
+
const companySize = record.company_size;
|
|
476
|
+
if (companySize === "enterprise") {
|
|
477
|
+
record.seat_count = chance.integer({ min: 50, max: 500 });
|
|
478
|
+
record.annual_contract_value = chance.integer({ min: 50000, max: 500000 });
|
|
479
|
+
record.customer_success_manager = true;
|
|
480
|
+
} else if (companySize === "mid_market") {
|
|
481
|
+
record.seat_count = chance.integer({ min: 10, max: 50 });
|
|
482
|
+
record.annual_contract_value = chance.integer({ min: 12000, max: 50000 });
|
|
483
|
+
record.customer_success_manager = false;
|
|
484
|
+
} else if (companySize === "smb") {
|
|
485
|
+
record.seat_count = chance.integer({ min: 3, max: 10 });
|
|
486
|
+
record.annual_contract_value = chance.integer({ min: 3600, max: 12000 });
|
|
487
|
+
record.customer_success_manager = false;
|
|
488
|
+
} else if (companySize === "startup") {
|
|
489
|
+
record.seat_count = chance.integer({ min: 1, max: 5 });
|
|
490
|
+
record.annual_contract_value = chance.integer({ min: 0, max: 3600 });
|
|
491
|
+
record.customer_success_manager = false;
|
|
492
|
+
}
|
|
493
|
+
record.customer_health_score = chance.integer({ min: 1, max: 100 });
|
|
494
|
+
return record;
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
function handleEverythingHooks(record, meta) {
|
|
498
|
+
const datasetStart = dayjs.unix(meta.datasetStart);
|
|
499
|
+
const userEvents = record;
|
|
500
|
+
const profile = meta.profile;
|
|
501
|
+
|
|
502
|
+
userEvents.forEach(e => {
|
|
503
|
+
e.plan_tier = profile.plan_tier;
|
|
504
|
+
e.cloud_provider = profile.cloud_provider;
|
|
505
|
+
});
|
|
506
|
+
|
|
507
|
+
// H1a: END-OF-QUARTER SPIKE — days 100-110, billing events flip
|
|
508
|
+
// event_type to plan_upgraded 40% of the time.
|
|
509
|
+
userEvents.forEach(e => {
|
|
510
|
+
if (e.event !== "billing event") return;
|
|
511
|
+
const dayInDataset = dayjs(e.time).diff(datasetStart, "days", true);
|
|
512
|
+
if (dayInDataset >= EOQ_START_DAY && dayInDataset <= EOQ_END_DAY && chance.bool({ likelihood: EOQ_UPGRADE_LIKELIHOOD })) {
|
|
513
|
+
e.event_type = "plan_upgraded";
|
|
514
|
+
}
|
|
515
|
+
});
|
|
516
|
+
|
|
517
|
+
// H1b: END-OF-QUARTER TEAM INVITE SPIKE — days 100-110, clone 50% of
|
|
518
|
+
// team-member-invited events (push, not return).
|
|
519
|
+
for (let i = userEvents.length - 1; i >= 0; i--) {
|
|
520
|
+
const e = userEvents[i];
|
|
521
|
+
if (e.event !== "team member invited") continue;
|
|
522
|
+
const dayInDataset = dayjs(e.time).diff(datasetStart, "days", true);
|
|
523
|
+
if (dayInDataset >= EOQ_START_DAY && dayInDataset <= EOQ_END_DAY && chance.bool({ likelihood: EOQ_INVITE_CLONE_LIKELIHOOD })) {
|
|
524
|
+
userEvents.push({
|
|
525
|
+
...e,
|
|
526
|
+
time: dayjs(e.time).add(chance.integer({ min: 1, max: 60 }), "minutes").toISOString(),
|
|
527
|
+
user_id: e.user_id,
|
|
528
|
+
role: chance.pickone(["editor", "viewer"]),
|
|
529
|
+
invitation_method: chance.pickone(["email", "sso", "slack"]),
|
|
530
|
+
});
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
// H2: CHURNED ACCOUNT SILENCING — ~20% of users (hash %5) have post-day-30
|
|
535
|
+
// events removed.
|
|
536
|
+
if (userEvents && userEvents.length > 0) {
|
|
537
|
+
const firstEvent = userEvents[0];
|
|
538
|
+
const idHash = String(firstEvent.user_id || firstEvent.device_id).split("").reduce((acc, char) => acc + char.charCodeAt(0), 0);
|
|
539
|
+
if ((idHash % CHURN_USER_HASH_MOD) === 0) {
|
|
540
|
+
for (let i = userEvents.length - 1; i >= 0; i--) {
|
|
541
|
+
const dayInDataset = dayjs(userEvents[i].time).diff(datasetStart, "days", true);
|
|
542
|
+
if (dayInDataset > CHURN_CUTOFF_DAYS) {
|
|
543
|
+
userEvents.splice(i, 1);
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
// H4: INTEGRATION USERS SUCCEED — Slack+PagerDuty users get alert
|
|
550
|
+
// response_time_mins 0.4x and resolution_time_mins 0.5x.
|
|
551
|
+
let hasSlack = false;
|
|
552
|
+
let hasPagerduty = false;
|
|
553
|
+
userEvents.forEach((event) => {
|
|
554
|
+
if (event.event === "integration configured") {
|
|
555
|
+
if (event.integration_type === "slack") hasSlack = true;
|
|
556
|
+
if (event.integration_type === "pagerduty") hasPagerduty = true;
|
|
557
|
+
}
|
|
558
|
+
});
|
|
559
|
+
if (hasSlack && hasPagerduty) {
|
|
560
|
+
userEvents.forEach((event) => {
|
|
561
|
+
if (event.event === "alert acknowledged" && event.response_time_mins) {
|
|
562
|
+
event.response_time_mins = Math.floor(event.response_time_mins * INTEGRATION_RESPONSE_FACTOR);
|
|
563
|
+
}
|
|
564
|
+
if (event.event === "alert resolved" && event.resolution_time_mins) {
|
|
565
|
+
event.resolution_time_mins = Math.floor(event.resolution_time_mins * INTEGRATION_RESOLUTION_FACTOR);
|
|
566
|
+
}
|
|
567
|
+
});
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
// H5 + H10: DOCS MAGIC NUMBER — sweet 4-7 docs → +40% extra cloned
|
|
571
|
+
// service-deployed events; over 8+ → drop 25% of service-deployed events.
|
|
572
|
+
const docsCount = userEvents.filter(e => e.event === "documentation viewed").length;
|
|
573
|
+
const deployTemplate = userEvents.find(e => e.event === "service deployed");
|
|
574
|
+
if (docsCount >= DOCS_SWEET_MIN && docsCount <= DOCS_SWEET_MAX && deployTemplate) {
|
|
575
|
+
const lastEvent = userEvents[userEvents.length - 1];
|
|
576
|
+
const extraDeploys = chance.integer({ min: DOCS_EXTRA_DEPLOYS_MIN, max: DOCS_EXTRA_DEPLOYS_MAX });
|
|
577
|
+
for (let i = 0; i < extraDeploys; i++) {
|
|
578
|
+
userEvents.push({
|
|
579
|
+
...deployTemplate,
|
|
580
|
+
time: dayjs(lastEvent.time).add(chance.integer({ min: 1, max: 48 }), "hours").toISOString(),
|
|
581
|
+
user_id: lastEvent.user_id,
|
|
582
|
+
service_id: chance.pickone(serviceIds),
|
|
583
|
+
service_type: chance.pickone(["web_app", "api", "database", "cache", "queue", "ml_model"]),
|
|
584
|
+
environment: "production",
|
|
585
|
+
cloud_provider: profile.cloud_provider,
|
|
586
|
+
});
|
|
587
|
+
}
|
|
588
|
+
} else if (docsCount >= DOCS_OVER_THRESHOLD) {
|
|
589
|
+
for (let i = userEvents.length - 1; i >= 0; i--) {
|
|
590
|
+
if (userEvents[i].event === "service deployed" && chance.bool({ likelihood: DOCS_DEPLOY_DROP_LIKELIHOOD })) {
|
|
591
|
+
userEvents.splice(i, 1);
|
|
592
|
+
}
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
// H7: FAILED DEPLOYMENT RECOVERY — find failed→success pairs in this
|
|
597
|
+
// user's pipeline events, multiply duration_sec by 1.5 on the recovery
|
|
598
|
+
// deploy.
|
|
599
|
+
const pipelineEvents = userEvents
|
|
600
|
+
.filter(e => e.event === "deployment pipeline run")
|
|
601
|
+
.sort((a, b) => a.time.localeCompare(b.time));
|
|
602
|
+
for (let i = 1; i < pipelineEvents.length; i++) {
|
|
603
|
+
if (pipelineEvents[i - 1].status === "failed" && pipelineEvents[i].status === "success") {
|
|
604
|
+
pipelineEvents[i].duration_sec = Math.floor((pipelineEvents[i].duration_sec || 300) * FAILED_DEPLOY_RECOVERY_MULT);
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
// H9: INCIDENT RESPONSE TTC — enterprise resolves faster, startup
|
|
609
|
+
// resolves slower. Scale response_time_mins on acknowledged events and
|
|
610
|
+
// resolution_time_mins on resolved events by company_size, and shift the
|
|
611
|
+
// first incident-response funnel sequence timestamps for funnel TTC
|
|
612
|
+
// reports. Compounds with H4 (integration users).
|
|
613
|
+
const companySegment = profile?.company_size;
|
|
614
|
+
const ttcFactor = (
|
|
615
|
+
companySegment === "enterprise" ? TTC_ENTERPRISE_FACTOR :
|
|
616
|
+
companySegment === "startup" ? TTC_STARTUP_FACTOR :
|
|
617
|
+
1.0
|
|
618
|
+
);
|
|
619
|
+
if (ttcFactor !== 1.0) {
|
|
620
|
+
// Timestamp shift: affects Mixpanel funnel TTC
|
|
621
|
+
const incidentSeq = findFirstSequence(
|
|
622
|
+
userEvents,
|
|
623
|
+
["alert triggered", "alert acknowledged", "alert resolved"],
|
|
624
|
+
INCIDENT_SEQ_WINDOW_MINS
|
|
625
|
+
);
|
|
626
|
+
if (incidentSeq) scaleFunnelTTC(incidentSeq, ttcFactor);
|
|
627
|
+
// Property scale: affects Insights AVG reports
|
|
628
|
+
userEvents.forEach(e => {
|
|
629
|
+
if (e.event === "alert acknowledged" && e.response_time_mins) {
|
|
630
|
+
e.response_time_mins = Math.max(1, Math.round(e.response_time_mins * ttcFactor));
|
|
631
|
+
}
|
|
632
|
+
if (e.event === "alert resolved" && e.resolution_time_mins) {
|
|
633
|
+
e.resolution_time_mins = Math.max(1, Math.round(e.resolution_time_mins * ttcFactor));
|
|
634
|
+
}
|
|
635
|
+
});
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
return record;
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
// ── CONFIG ──
|
|
440
642
|
/** @type {Config} */
|
|
441
643
|
const config = {
|
|
442
|
-
token,
|
|
443
644
|
seed: SEED,
|
|
444
|
-
datasetStart:
|
|
445
|
-
datasetEnd:
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
645
|
+
datasetStart: DATASET_START,
|
|
646
|
+
datasetEnd: DATASET_END,
|
|
647
|
+
avgEventsPerUserPerDay: EVENTS_PER_DAY,
|
|
648
|
+
numUsers: NUM_USERS,
|
|
649
|
+
format: "json",
|
|
650
|
+
gzip: true,
|
|
651
|
+
credentials: {
|
|
652
|
+
token,
|
|
653
|
+
},
|
|
654
|
+
switches: {
|
|
655
|
+
hasSessionIds: true,
|
|
656
|
+
alsoInferFunnels: false,
|
|
657
|
+
hasLocation: true,
|
|
658
|
+
hasAndroidDevices: false,
|
|
659
|
+
hasIOSDevices: false,
|
|
660
|
+
hasDesktopDevices: true,
|
|
661
|
+
hasBrowser: true,
|
|
662
|
+
hasCampaigns: false,
|
|
663
|
+
isAnonymous: false,
|
|
664
|
+
hasAdSpend: false,
|
|
665
|
+
hasAvatar: true,
|
|
666
|
+
},
|
|
449
667
|
// Phase 2 identity model — B2B SaaS reference. Engineers commonly use 1-2
|
|
450
668
|
// devices (desktop + work laptop). avgDevicePerUser:2 puts a meaningful
|
|
451
669
|
// per-session sticky-device pattern in Mixpanel device dashboards.
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
format: "json",
|
|
456
|
-
gzip: true,
|
|
457
|
-
alsoInferFunnels: false,
|
|
458
|
-
hasLocation: true,
|
|
459
|
-
hasAndroidDevices: false,
|
|
460
|
-
hasIOSDevices: false,
|
|
461
|
-
hasDesktopDevices: true,
|
|
462
|
-
hasBrowser: true,
|
|
463
|
-
hasCampaigns: false,
|
|
464
|
-
isAnonymous: false,
|
|
465
|
-
hasAdSpend: false,
|
|
466
|
-
hasAvatar: true,
|
|
670
|
+
identity: {
|
|
671
|
+
avgDevicePerUser: 2,
|
|
672
|
+
},
|
|
467
673
|
concurrency: 1,
|
|
468
674
|
writeToDisk: false,
|
|
469
675
|
scdProps: {
|
|
@@ -778,230 +984,10 @@ const config = {
|
|
|
778
984
|
|
|
779
985
|
lookupTables: [],
|
|
780
986
|
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
* Hook 11 (Deploy Pipeline Experiment) is engine-managed via funnel
|
|
786
|
-
* experiment config — no hook code needed.
|
|
787
|
-
*
|
|
788
|
-
* 1. END-OF-QUARTER SPIKE: Days 100-110 drive plan upgrades and team expansion
|
|
789
|
-
* 2. CHURNED ACCOUNT SILENCING: ~10% of users go completely silent after month 1
|
|
790
|
-
* 3. ALERT ESCALATION REPLACEMENT: Critical alerts become "incident created" events
|
|
791
|
-
* 4. INTEGRATION USERS SUCCEED: Slack+PagerDuty users resolve incidents 50-60% faster
|
|
792
|
-
* 5. DOCS READERS DEPLOY MORE: Best practices readers get extra production deploys
|
|
793
|
-
* 6. COST OVERRUN PATTERN: Budget-exceeded users react by scaling down infrastructure
|
|
794
|
-
* 7. FAILED DEPLOYMENT RECOVERY: Recovery deploys take 1.5x longer, tracked across calls
|
|
795
|
-
* 8. ENTERPRISE VS STARTUP: Company size determines seat count, ACV, and health score
|
|
796
|
-
* 9. INCIDENT RESPONSE TTC: Enterprise 0.67x faster, startup 1.5x slower incident resolution
|
|
797
|
-
* 10. DOCS MAGIC NUMBER: Sweet 4-7 docs → extra deploys; over 8+ → drop 25% of deploys
|
|
798
|
-
* 11. DEPLOY PIPELINE EXPERIMENT: Canary Deploys A/B test on deployment funnel (engine-managed)
|
|
799
|
-
*/
|
|
800
|
-
hook: function (record, type, meta) {
|
|
801
|
-
// (Hook 1a moved to everything hook for reliable datasetStart access)
|
|
802
|
-
|
|
803
|
-
// HOOK 3: ALERT ESCALATION REPLACEMENT (event) — critical/emergency
|
|
804
|
-
// alerts sometimes become incident-created events. Real product flow.
|
|
805
|
-
if (type === "event") {
|
|
806
|
-
if (record.event === "alert triggered") {
|
|
807
|
-
const severity = record.severity;
|
|
808
|
-
if ((severity === "critical" || severity === "emergency") && chance.bool({ likelihood: 30 })) {
|
|
809
|
-
return {
|
|
810
|
-
...record,
|
|
811
|
-
event: "incident created",
|
|
812
|
-
escalation_level: chance.pickone(["P1", "P2"]),
|
|
813
|
-
teams_paged: chance.integer({ min: 1, max: 5 }),
|
|
814
|
-
incident_id: `inc_${v.uid(8)}`,
|
|
815
|
-
original_severity: severity,
|
|
816
|
-
original_alert_type: record.alert_type,
|
|
817
|
-
auto_escalated: true,
|
|
818
|
-
};
|
|
819
|
-
}
|
|
820
|
-
}
|
|
821
|
-
}
|
|
822
|
-
|
|
823
|
-
// HOOK 6: COST OVERRUN PATTERN (event) — cost reports with cost_change
|
|
824
|
-
// > 25% record user, then next infrastructure-scaled event from that
|
|
825
|
-
// user gets scale_direction = "down". No flag.
|
|
826
|
-
if (type === "event") {
|
|
827
|
-
if (record.event === "cost report generated" && record.cost_change_percent > 25) {
|
|
828
|
-
costOverrunUsers.set(record.user_id, true);
|
|
829
|
-
}
|
|
830
|
-
if (record.event === "infrastructure scaled" && costOverrunUsers.has(record.user_id)) {
|
|
831
|
-
record.scale_direction = "down";
|
|
832
|
-
costOverrunUsers.delete(record.user_id);
|
|
833
|
-
}
|
|
834
|
-
}
|
|
835
|
-
|
|
836
|
-
if (type === "everything") {
|
|
837
|
-
const datasetStart = dayjs.unix(meta.datasetStart);
|
|
838
|
-
const userEvents = record;
|
|
839
|
-
const profile = meta.profile;
|
|
840
|
-
|
|
841
|
-
userEvents.forEach(e => {
|
|
842
|
-
e.plan_tier = profile.plan_tier;
|
|
843
|
-
e.cloud_provider = profile.cloud_provider;
|
|
844
|
-
});
|
|
845
|
-
|
|
846
|
-
// HOOK 1a: END-OF-QUARTER SPIKE — days 100-110, billing events flip
|
|
847
|
-
// event_type to plan_upgraded 40% of the time. No flag.
|
|
848
|
-
userEvents.forEach(e => {
|
|
849
|
-
if (e.event !== "billing event") return;
|
|
850
|
-
const dayInDataset = dayjs(e.time).diff(datasetStart, "days", true);
|
|
851
|
-
if (dayInDataset >= 100 && dayInDataset <= 110 && chance.bool({ likelihood: 40 })) {
|
|
852
|
-
e.event_type = "plan_upgraded";
|
|
853
|
-
}
|
|
854
|
-
});
|
|
855
|
-
|
|
856
|
-
// HOOK 1b: END-OF-QUARTER TEAM INVITE SPIKE — days 100-110, clone
|
|
857
|
-
// 50% of team-member-invited events (push, not return). No flag.
|
|
858
|
-
for (let i = userEvents.length - 1; i >= 0; i--) {
|
|
859
|
-
const e = userEvents[i];
|
|
860
|
-
if (e.event !== "team member invited") continue;
|
|
861
|
-
const dayInDataset = dayjs(e.time).diff(datasetStart, "days", true);
|
|
862
|
-
if (dayInDataset >= 100 && dayInDataset <= 110 && chance.bool({ likelihood: 50 })) {
|
|
863
|
-
userEvents.push({
|
|
864
|
-
...e,
|
|
865
|
-
time: dayjs(e.time).add(chance.integer({ min: 1, max: 60 }), "minutes").toISOString(),
|
|
866
|
-
user_id: e.user_id,
|
|
867
|
-
role: chance.pickone(["editor", "viewer"]),
|
|
868
|
-
invitation_method: chance.pickone(["email", "sso", "slack"]),
|
|
869
|
-
});
|
|
870
|
-
}
|
|
871
|
-
}
|
|
872
|
-
|
|
873
|
-
// HOOK 2: CHURNED ACCOUNT SILENCING — ~20% of users (hash %5)
|
|
874
|
-
// have post-day-30 events removed. No flag.
|
|
875
|
-
if (userEvents && userEvents.length > 0) {
|
|
876
|
-
const firstEvent = userEvents[0];
|
|
877
|
-
const idHash = String(firstEvent.user_id || firstEvent.device_id).split("").reduce((acc, char) => acc + char.charCodeAt(0), 0);
|
|
878
|
-
if ((idHash % 5) === 0) {
|
|
879
|
-
for (let i = userEvents.length - 1; i >= 0; i--) {
|
|
880
|
-
const dayInDataset = dayjs(userEvents[i].time).diff(datasetStart, "days", true);
|
|
881
|
-
if (dayInDataset > 30) {
|
|
882
|
-
userEvents.splice(i, 1);
|
|
883
|
-
}
|
|
884
|
-
}
|
|
885
|
-
}
|
|
886
|
-
}
|
|
887
|
-
|
|
888
|
-
// HOOK 4: INTEGRATION USERS SUCCEED — Slack+PagerDuty users get
|
|
889
|
-
// alert response_time_mins 0.4x and resolution_time_mins 0.5x.
|
|
890
|
-
// Mutates raw props. No flag.
|
|
891
|
-
let hasSlack = false;
|
|
892
|
-
let hasPagerduty = false;
|
|
893
|
-
userEvents.forEach((event) => {
|
|
894
|
-
if (event.event === "integration configured") {
|
|
895
|
-
if (event.integration_type === "slack") hasSlack = true;
|
|
896
|
-
if (event.integration_type === "pagerduty") hasPagerduty = true;
|
|
897
|
-
}
|
|
898
|
-
});
|
|
899
|
-
if (hasSlack && hasPagerduty) {
|
|
900
|
-
userEvents.forEach((event) => {
|
|
901
|
-
if (event.event === "alert acknowledged" && event.response_time_mins) {
|
|
902
|
-
event.response_time_mins = Math.floor(event.response_time_mins * 0.4);
|
|
903
|
-
}
|
|
904
|
-
if (event.event === "alert resolved" && event.resolution_time_mins) {
|
|
905
|
-
event.resolution_time_mins = Math.floor(event.resolution_time_mins * 0.5);
|
|
906
|
-
}
|
|
907
|
-
});
|
|
908
|
-
}
|
|
909
|
-
|
|
910
|
-
// HOOK 5 + HOOK 10: DOCS MAGIC NUMBER (no flags)
|
|
911
|
-
// Sweet 4-7 documentation-viewed events → +40% extra cloned
|
|
912
|
-
// service-deployed events. Over 8+ → drop 25% of service-deployed
|
|
913
|
-
// events. No flag.
|
|
914
|
-
const docsCount = userEvents.filter(e => e.event === "documentation viewed").length;
|
|
915
|
-
const deployTemplate = userEvents.find(e => e.event === "service deployed");
|
|
916
|
-
if (docsCount >= 4 && docsCount <= 7 && deployTemplate) {
|
|
917
|
-
const lastEvent = userEvents[userEvents.length - 1];
|
|
918
|
-
const extraDeploys = chance.integer({ min: 2, max: 3 });
|
|
919
|
-
for (let i = 0; i < extraDeploys; i++) {
|
|
920
|
-
userEvents.push({
|
|
921
|
-
...deployTemplate,
|
|
922
|
-
time: dayjs(lastEvent.time).add(chance.integer({ min: 1, max: 48 }), "hours").toISOString(),
|
|
923
|
-
user_id: lastEvent.user_id,
|
|
924
|
-
service_id: chance.pickone(serviceIds),
|
|
925
|
-
service_type: chance.pickone(["web_app", "api", "database", "cache", "queue", "ml_model"]),
|
|
926
|
-
environment: "production",
|
|
927
|
-
cloud_provider: profile.cloud_provider,
|
|
928
|
-
});
|
|
929
|
-
}
|
|
930
|
-
} else if (docsCount >= 8) {
|
|
931
|
-
for (let i = userEvents.length - 1; i >= 0; i--) {
|
|
932
|
-
if (userEvents[i].event === "service deployed" && chance.bool({ likelihood: 25 })) {
|
|
933
|
-
userEvents.splice(i, 1);
|
|
934
|
-
}
|
|
935
|
-
}
|
|
936
|
-
}
|
|
937
|
-
|
|
938
|
-
// HOOK 7: FAILED DEPLOYMENT RECOVERY — find failed→success pairs
|
|
939
|
-
// in this user's pipeline events, multiply duration_sec by 1.5 on
|
|
940
|
-
// the recovery deploy. Full control via everything hook.
|
|
941
|
-
const pipelineEvents = userEvents
|
|
942
|
-
.filter(e => e.event === "deployment pipeline run")
|
|
943
|
-
.sort((a, b) => a.time.localeCompare(b.time));
|
|
944
|
-
for (let i = 1; i < pipelineEvents.length; i++) {
|
|
945
|
-
if (pipelineEvents[i - 1].status === "failed" && pipelineEvents[i].status === "success") {
|
|
946
|
-
pipelineEvents[i].duration_sec = Math.floor((pipelineEvents[i].duration_sec || 300) * 1.5);
|
|
947
|
-
}
|
|
948
|
-
}
|
|
949
|
-
|
|
950
|
-
// HOOK 9: INCIDENT RESPONSE TTC — enterprise resolves faster,
|
|
951
|
-
// startup resolves slower. Scale response_time_mins on
|
|
952
|
-
// acknowledged events and resolution_time_mins on resolved
|
|
953
|
-
// events by company_size. This compounds with H4 (integration
|
|
954
|
-
// users) — realistic: enterprise + good tooling = fastest.
|
|
955
|
-
const companySegment = profile?.company_size;
|
|
956
|
-
const ttcFactor = (
|
|
957
|
-
companySegment === "enterprise" ? 0.67 :
|
|
958
|
-
companySegment === "startup" ? 1.5 :
|
|
959
|
-
1.0
|
|
960
|
-
);
|
|
961
|
-
if (ttcFactor !== 1.0) {
|
|
962
|
-
// Timestamp shift: affects Mixpanel funnel TTC
|
|
963
|
-
const incidentSeq = findFirstSequence(
|
|
964
|
-
userEvents,
|
|
965
|
-
["alert triggered", "alert acknowledged", "alert resolved"],
|
|
966
|
-
60 * 24 * 30
|
|
967
|
-
);
|
|
968
|
-
if (incidentSeq) scaleFunnelTTC(incidentSeq, ttcFactor);
|
|
969
|
-
// Property scale: affects Insights AVG reports
|
|
970
|
-
userEvents.forEach(e => {
|
|
971
|
-
if (e.event === "alert acknowledged" && e.response_time_mins) {
|
|
972
|
-
e.response_time_mins = Math.max(1, Math.round(e.response_time_mins * ttcFactor));
|
|
973
|
-
}
|
|
974
|
-
if (e.event === "alert resolved" && e.resolution_time_mins) {
|
|
975
|
-
e.resolution_time_mins = Math.max(1, Math.round(e.resolution_time_mins * ttcFactor));
|
|
976
|
-
}
|
|
977
|
-
});
|
|
978
|
-
}
|
|
979
|
-
}
|
|
980
|
-
|
|
981
|
-
// HOOK 8: ENTERPRISE VS STARTUP (user) — company size determines
|
|
982
|
-
// seat count, ACV, and CSM. Real profile attrs.
|
|
983
|
-
if (type === "user") {
|
|
984
|
-
const companySize = record.company_size;
|
|
985
|
-
if (companySize === "enterprise") {
|
|
986
|
-
record.seat_count = chance.integer({ min: 50, max: 500 });
|
|
987
|
-
record.annual_contract_value = chance.integer({ min: 50000, max: 500000 });
|
|
988
|
-
record.customer_success_manager = true;
|
|
989
|
-
} else if (companySize === "mid_market") {
|
|
990
|
-
record.seat_count = chance.integer({ min: 10, max: 50 });
|
|
991
|
-
record.annual_contract_value = chance.integer({ min: 12000, max: 50000 });
|
|
992
|
-
record.customer_success_manager = false;
|
|
993
|
-
} else if (companySize === "smb") {
|
|
994
|
-
record.seat_count = chance.integer({ min: 3, max: 10 });
|
|
995
|
-
record.annual_contract_value = chance.integer({ min: 3600, max: 12000 });
|
|
996
|
-
record.customer_success_manager = false;
|
|
997
|
-
} else if (companySize === "startup") {
|
|
998
|
-
record.seat_count = chance.integer({ min: 1, max: 5 });
|
|
999
|
-
record.annual_contract_value = chance.integer({ min: 0, max: 3600 });
|
|
1000
|
-
record.customer_success_manager = false;
|
|
1001
|
-
}
|
|
1002
|
-
record.customer_health_score = chance.integer({ min: 1, max: 100 });
|
|
1003
|
-
}
|
|
1004
|
-
|
|
987
|
+
hook(record, type, meta) {
|
|
988
|
+
if (type === "event") return handleEventHooks(record);
|
|
989
|
+
if (type === "user") return handleUserHooks(record);
|
|
990
|
+
if (type === "everything") return handleEverythingHooks(record, meta);
|
|
1005
991
|
return record;
|
|
1006
992
|
}
|
|
1007
993
|
};
|