@ak--47/dungeon-master 1.4.5 → 1.5.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/skills/analyze-soup/SKILL.md +158 -0
- package/.claude/skills/create-dungeon/SKILL.md +464 -0
- package/.claude/skills/verify-dungeon/SKILL.md +157 -0
- package/.claude/skills/verify-dungeon/references/counting-semantics.md +161 -0
- package/.claude/skills/verify-dungeon/references/report-format.md +216 -0
- package/.claude/skills/verify-dungeon/references/sql-recipes.md +857 -0
- package/.claude/skills/write-hooks/SKILL.md +468 -0
- package/CHANGELOG.md +182 -0
- package/HOOKS.md +1256 -597
- package/README.md +140 -5
- 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 +87 -0
- 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 +111 -65
- package/dungeons/technical/text-generation.js +110 -146
- package/dungeons/vertical/ai-platform.js +300 -333
- package/dungeons/vertical/community.js +290 -255
- package/dungeons/vertical/crypto.js +400 -391
- package/dungeons/vertical/dating.js +421 -375
- package/dungeons/vertical/devtools.js +346 -298
- package/dungeons/vertical/ecommerce.js +322 -394
- package/dungeons/vertical/education.js +380 -325
- package/dungeons/vertical/fintech.js +371 -325
- package/dungeons/vertical/fitness.js +345 -291
- package/dungeons/vertical/food-delivery.js +352 -307
- package/dungeons/vertical/gaming.js +490 -444
- package/dungeons/vertical/healthcare.js +311 -262
- package/dungeons/vertical/insurance-application.js +437 -409
- package/dungeons/vertical/logistics.js +278 -252
- package/dungeons/vertical/marketplace.js +340 -323
- package/dungeons/vertical/media.js +390 -335
- package/dungeons/vertical/real-estate.js +402 -347
- package/dungeons/vertical/sass.js +331 -333
- package/dungeons/vertical/social.js +377 -316
- package/dungeons/vertical/travel.js +302 -295
- package/index.js +64 -7
- package/lib/core/config-validator.js +378 -17
- package/lib/core/dungeon-loader.js +2 -5
- package/lib/generators/events.js +12 -13
- package/lib/generators/funnels.js +76 -2
- package/lib/hook-helpers/index.js +1 -0
- package/lib/hook-helpers/inject.js +95 -0
- package/lib/orchestrators/mixpanel-sender.js +7 -0
- package/lib/orchestrators/user-loop.js +598 -48
- package/lib/templates/defaults.js +59 -59
- package/lib/templates/macro-presets.js +53 -11
- package/lib/utils/dataset-context.js +103 -0
- package/lib/utils/retention-curve.js +140 -0
- package/lib/utils/utils.js +157 -109
- package/lib/verify/counting.js +360 -0
- package/lib/verify/emulate-breakdown.js +531 -108
- package/lib/verify/funnel-engine.js +539 -0
- package/lib/verify/identity.js +78 -0
- package/lib/verify/index.js +20 -0
- package/lib/verify/schema-validator.js +3 -1
- package/lib/verify/verify-dungeon.js +58 -0
- package/package.json +14 -3
- package/scripts/run-dungeon.mjs +12 -1
- package/types.d.ts +353 -4
- package/scripts/smoke-test-all.mjs +0 -162
|
@@ -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: {
|
|
@@ -568,6 +774,7 @@ const config = {
|
|
|
568
774
|
{
|
|
569
775
|
event: "service deployed",
|
|
570
776
|
weight: 10,
|
|
777
|
+
isStrictEvent: false,
|
|
571
778
|
properties: {
|
|
572
779
|
service_id: serviceIds,
|
|
573
780
|
service_type: ["web_app", "api", "database", "cache", "queue", "ml_model"],
|
|
@@ -578,6 +785,7 @@ const config = {
|
|
|
578
785
|
{
|
|
579
786
|
event: "dashboard viewed",
|
|
580
787
|
weight: 20,
|
|
788
|
+
isStrictEvent: false,
|
|
581
789
|
properties: {
|
|
582
790
|
dashboard_type: ["overview", "cost", "performance", "security", "custom"],
|
|
583
791
|
time_range: ["1h", "6h", "24h", "7d", "30d"],
|
|
@@ -586,6 +794,7 @@ const config = {
|
|
|
586
794
|
{
|
|
587
795
|
event: "alert triggered",
|
|
588
796
|
weight: 12,
|
|
797
|
+
isStrictEvent: false,
|
|
589
798
|
properties: {
|
|
590
799
|
alert_id: alertIds,
|
|
591
800
|
severity: ["info", "warning", "critical", "emergency"],
|
|
@@ -609,6 +818,7 @@ const config = {
|
|
|
609
818
|
{
|
|
610
819
|
event: "alert acknowledged",
|
|
611
820
|
weight: 8,
|
|
821
|
+
isStrictEvent: false,
|
|
612
822
|
properties: {
|
|
613
823
|
alert_id: alertIds,
|
|
614
824
|
response_time_mins: u.weighNumRange(1, 120),
|
|
@@ -618,6 +828,7 @@ const config = {
|
|
|
618
828
|
{
|
|
619
829
|
event: "alert resolved",
|
|
620
830
|
weight: 7,
|
|
831
|
+
isStrictEvent: false,
|
|
621
832
|
properties: {
|
|
622
833
|
alert_id: alertIds,
|
|
623
834
|
resolution_time_mins: u.weighNumRange(5, 1440),
|
|
@@ -627,6 +838,7 @@ const config = {
|
|
|
627
838
|
{
|
|
628
839
|
event: "deployment pipeline run",
|
|
629
840
|
weight: 9,
|
|
841
|
+
isStrictEvent: false,
|
|
630
842
|
properties: {
|
|
631
843
|
pipeline_id: pipelineIds,
|
|
632
844
|
status: ["success", "failed", "cancelled"],
|
|
@@ -637,6 +849,7 @@ const config = {
|
|
|
637
849
|
{
|
|
638
850
|
event: "infrastructure scaled",
|
|
639
851
|
weight: 5,
|
|
852
|
+
isStrictEvent: false,
|
|
640
853
|
properties: {
|
|
641
854
|
service_id: serviceIds,
|
|
642
855
|
scale_direction: ["up", "up", "up", "down"],
|
|
@@ -648,6 +861,7 @@ const config = {
|
|
|
648
861
|
{
|
|
649
862
|
event: "cost report generated",
|
|
650
863
|
weight: 4,
|
|
864
|
+
isStrictEvent: false,
|
|
651
865
|
properties: {
|
|
652
866
|
report_period: ["daily", "weekly", "monthly"],
|
|
653
867
|
total_cost: u.weighNumRange(100, 50000),
|
|
@@ -657,6 +871,7 @@ const config = {
|
|
|
657
871
|
{
|
|
658
872
|
event: "team member invited",
|
|
659
873
|
weight: 3,
|
|
874
|
+
isStrictEvent: false,
|
|
660
875
|
properties: {
|
|
661
876
|
role: ["admin", "editor", "viewer", "billing"],
|
|
662
877
|
invitation_method: ["email", "sso", "slack"],
|
|
@@ -665,6 +880,7 @@ const config = {
|
|
|
665
880
|
{
|
|
666
881
|
event: "integration configured",
|
|
667
882
|
weight: 4,
|
|
883
|
+
isStrictEvent: false,
|
|
668
884
|
properties: {
|
|
669
885
|
integration_type: ["slack", "pagerduty", "jira", "github", "datadog", "terraform"],
|
|
670
886
|
status: ["active", "paused", "error"],
|
|
@@ -691,6 +907,7 @@ const config = {
|
|
|
691
907
|
{
|
|
692
908
|
event: "billing event",
|
|
693
909
|
weight: 3,
|
|
910
|
+
isStrictEvent: false,
|
|
694
911
|
properties: {
|
|
695
912
|
event_type: ["invoice_generated", "invoice_generated", "payment_received", "payment_received", "payment_received", "payment_failed", "plan_upgraded", "plan_downgraded"],
|
|
696
913
|
amount: u.weighNumRange(99, 25000),
|
|
@@ -718,6 +935,7 @@ const config = {
|
|
|
718
935
|
{
|
|
719
936
|
event: "documentation viewed",
|
|
720
937
|
weight: 7,
|
|
938
|
+
isStrictEvent: false,
|
|
721
939
|
properties: {
|
|
722
940
|
doc_section: ["getting_started", "api_reference", "best_practices", "troubleshooting", "changelog"],
|
|
723
941
|
time_on_page_sec: u.weighNumRange(5, 600),
|
|
@@ -766,230 +984,10 @@ const config = {
|
|
|
766
984
|
|
|
767
985
|
lookupTables: [],
|
|
768
986
|
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
* Hook 11 (Deploy Pipeline Experiment) is engine-managed via funnel
|
|
774
|
-
* experiment config — no hook code needed.
|
|
775
|
-
*
|
|
776
|
-
* 1. END-OF-QUARTER SPIKE: Days 100-110 drive plan upgrades and team expansion
|
|
777
|
-
* 2. CHURNED ACCOUNT SILENCING: ~10% of users go completely silent after month 1
|
|
778
|
-
* 3. ALERT ESCALATION REPLACEMENT: Critical alerts become "incident created" events
|
|
779
|
-
* 4. INTEGRATION USERS SUCCEED: Slack+PagerDuty users resolve incidents 50-60% faster
|
|
780
|
-
* 5. DOCS READERS DEPLOY MORE: Best practices readers get extra production deploys
|
|
781
|
-
* 6. COST OVERRUN PATTERN: Budget-exceeded users react by scaling down infrastructure
|
|
782
|
-
* 7. FAILED DEPLOYMENT RECOVERY: Recovery deploys take 1.5x longer, tracked across calls
|
|
783
|
-
* 8. ENTERPRISE VS STARTUP: Company size determines seat count, ACV, and health score
|
|
784
|
-
* 9. INCIDENT RESPONSE TTC: Enterprise 0.67x faster, startup 1.5x slower incident resolution
|
|
785
|
-
* 10. DOCS MAGIC NUMBER: Sweet 4-7 docs → extra deploys; over 8+ → drop 25% of deploys
|
|
786
|
-
* 11. DEPLOY PIPELINE EXPERIMENT: Canary Deploys A/B test on deployment funnel (engine-managed)
|
|
787
|
-
*/
|
|
788
|
-
hook: function (record, type, meta) {
|
|
789
|
-
// (Hook 1a moved to everything hook for reliable datasetStart access)
|
|
790
|
-
|
|
791
|
-
// HOOK 3: ALERT ESCALATION REPLACEMENT (event) — critical/emergency
|
|
792
|
-
// alerts sometimes become incident-created events. Real product flow.
|
|
793
|
-
if (type === "event") {
|
|
794
|
-
if (record.event === "alert triggered") {
|
|
795
|
-
const severity = record.severity;
|
|
796
|
-
if ((severity === "critical" || severity === "emergency") && chance.bool({ likelihood: 30 })) {
|
|
797
|
-
return {
|
|
798
|
-
...record,
|
|
799
|
-
event: "incident created",
|
|
800
|
-
escalation_level: chance.pickone(["P1", "P2"]),
|
|
801
|
-
teams_paged: chance.integer({ min: 1, max: 5 }),
|
|
802
|
-
incident_id: `inc_${v.uid(8)}`,
|
|
803
|
-
original_severity: severity,
|
|
804
|
-
original_alert_type: record.alert_type,
|
|
805
|
-
auto_escalated: true,
|
|
806
|
-
};
|
|
807
|
-
}
|
|
808
|
-
}
|
|
809
|
-
}
|
|
810
|
-
|
|
811
|
-
// HOOK 6: COST OVERRUN PATTERN (event) — cost reports with cost_change
|
|
812
|
-
// > 25% record user, then next infrastructure-scaled event from that
|
|
813
|
-
// user gets scale_direction = "down". No flag.
|
|
814
|
-
if (type === "event") {
|
|
815
|
-
if (record.event === "cost report generated" && record.cost_change_percent > 25) {
|
|
816
|
-
costOverrunUsers.set(record.user_id, true);
|
|
817
|
-
}
|
|
818
|
-
if (record.event === "infrastructure scaled" && costOverrunUsers.has(record.user_id)) {
|
|
819
|
-
record.scale_direction = "down";
|
|
820
|
-
costOverrunUsers.delete(record.user_id);
|
|
821
|
-
}
|
|
822
|
-
}
|
|
823
|
-
|
|
824
|
-
if (type === "everything") {
|
|
825
|
-
const datasetStart = dayjs.unix(meta.datasetStart);
|
|
826
|
-
const userEvents = record;
|
|
827
|
-
const profile = meta.profile;
|
|
828
|
-
|
|
829
|
-
userEvents.forEach(e => {
|
|
830
|
-
e.plan_tier = profile.plan_tier;
|
|
831
|
-
e.cloud_provider = profile.cloud_provider;
|
|
832
|
-
});
|
|
833
|
-
|
|
834
|
-
// HOOK 1a: END-OF-QUARTER SPIKE — days 100-110, billing events flip
|
|
835
|
-
// event_type to plan_upgraded 40% of the time. No flag.
|
|
836
|
-
userEvents.forEach(e => {
|
|
837
|
-
if (e.event !== "billing event") return;
|
|
838
|
-
const dayInDataset = dayjs(e.time).diff(datasetStart, "days", true);
|
|
839
|
-
if (dayInDataset >= 100 && dayInDataset <= 110 && chance.bool({ likelihood: 40 })) {
|
|
840
|
-
e.event_type = "plan_upgraded";
|
|
841
|
-
}
|
|
842
|
-
});
|
|
843
|
-
|
|
844
|
-
// HOOK 1b: END-OF-QUARTER TEAM INVITE SPIKE — days 100-110, clone
|
|
845
|
-
// 50% of team-member-invited events (push, not return). No flag.
|
|
846
|
-
for (let i = userEvents.length - 1; i >= 0; i--) {
|
|
847
|
-
const e = userEvents[i];
|
|
848
|
-
if (e.event !== "team member invited") continue;
|
|
849
|
-
const dayInDataset = dayjs(e.time).diff(datasetStart, "days", true);
|
|
850
|
-
if (dayInDataset >= 100 && dayInDataset <= 110 && chance.bool({ likelihood: 50 })) {
|
|
851
|
-
userEvents.push({
|
|
852
|
-
...e,
|
|
853
|
-
time: dayjs(e.time).add(chance.integer({ min: 1, max: 60 }), "minutes").toISOString(),
|
|
854
|
-
user_id: e.user_id,
|
|
855
|
-
role: chance.pickone(["editor", "viewer"]),
|
|
856
|
-
invitation_method: chance.pickone(["email", "sso", "slack"]),
|
|
857
|
-
});
|
|
858
|
-
}
|
|
859
|
-
}
|
|
860
|
-
|
|
861
|
-
// HOOK 2: CHURNED ACCOUNT SILENCING — ~20% of users (hash %5)
|
|
862
|
-
// have post-day-30 events removed. No flag.
|
|
863
|
-
if (userEvents && userEvents.length > 0) {
|
|
864
|
-
const firstEvent = userEvents[0];
|
|
865
|
-
const idHash = String(firstEvent.user_id || firstEvent.device_id).split("").reduce((acc, char) => acc + char.charCodeAt(0), 0);
|
|
866
|
-
if ((idHash % 5) === 0) {
|
|
867
|
-
for (let i = userEvents.length - 1; i >= 0; i--) {
|
|
868
|
-
const dayInDataset = dayjs(userEvents[i].time).diff(datasetStart, "days", true);
|
|
869
|
-
if (dayInDataset > 30) {
|
|
870
|
-
userEvents.splice(i, 1);
|
|
871
|
-
}
|
|
872
|
-
}
|
|
873
|
-
}
|
|
874
|
-
}
|
|
875
|
-
|
|
876
|
-
// HOOK 4: INTEGRATION USERS SUCCEED — Slack+PagerDuty users get
|
|
877
|
-
// alert response_time_mins 0.4x and resolution_time_mins 0.5x.
|
|
878
|
-
// Mutates raw props. No flag.
|
|
879
|
-
let hasSlack = false;
|
|
880
|
-
let hasPagerduty = false;
|
|
881
|
-
userEvents.forEach((event) => {
|
|
882
|
-
if (event.event === "integration configured") {
|
|
883
|
-
if (event.integration_type === "slack") hasSlack = true;
|
|
884
|
-
if (event.integration_type === "pagerduty") hasPagerduty = true;
|
|
885
|
-
}
|
|
886
|
-
});
|
|
887
|
-
if (hasSlack && hasPagerduty) {
|
|
888
|
-
userEvents.forEach((event) => {
|
|
889
|
-
if (event.event === "alert acknowledged" && event.response_time_mins) {
|
|
890
|
-
event.response_time_mins = Math.floor(event.response_time_mins * 0.4);
|
|
891
|
-
}
|
|
892
|
-
if (event.event === "alert resolved" && event.resolution_time_mins) {
|
|
893
|
-
event.resolution_time_mins = Math.floor(event.resolution_time_mins * 0.5);
|
|
894
|
-
}
|
|
895
|
-
});
|
|
896
|
-
}
|
|
897
|
-
|
|
898
|
-
// HOOK 5 + HOOK 10: DOCS MAGIC NUMBER (no flags)
|
|
899
|
-
// Sweet 4-7 documentation-viewed events → +40% extra cloned
|
|
900
|
-
// service-deployed events. Over 8+ → drop 25% of service-deployed
|
|
901
|
-
// events. No flag.
|
|
902
|
-
const docsCount = userEvents.filter(e => e.event === "documentation viewed").length;
|
|
903
|
-
const deployTemplate = userEvents.find(e => e.event === "service deployed");
|
|
904
|
-
if (docsCount >= 4 && docsCount <= 7 && deployTemplate) {
|
|
905
|
-
const lastEvent = userEvents[userEvents.length - 1];
|
|
906
|
-
const extraDeploys = chance.integer({ min: 2, max: 3 });
|
|
907
|
-
for (let i = 0; i < extraDeploys; i++) {
|
|
908
|
-
userEvents.push({
|
|
909
|
-
...deployTemplate,
|
|
910
|
-
time: dayjs(lastEvent.time).add(chance.integer({ min: 1, max: 48 }), "hours").toISOString(),
|
|
911
|
-
user_id: lastEvent.user_id,
|
|
912
|
-
service_id: chance.pickone(serviceIds),
|
|
913
|
-
service_type: chance.pickone(["web_app", "api", "database", "cache", "queue", "ml_model"]),
|
|
914
|
-
environment: "production",
|
|
915
|
-
cloud_provider: profile.cloud_provider,
|
|
916
|
-
});
|
|
917
|
-
}
|
|
918
|
-
} else if (docsCount >= 8) {
|
|
919
|
-
for (let i = userEvents.length - 1; i >= 0; i--) {
|
|
920
|
-
if (userEvents[i].event === "service deployed" && chance.bool({ likelihood: 25 })) {
|
|
921
|
-
userEvents.splice(i, 1);
|
|
922
|
-
}
|
|
923
|
-
}
|
|
924
|
-
}
|
|
925
|
-
|
|
926
|
-
// HOOK 7: FAILED DEPLOYMENT RECOVERY — find failed→success pairs
|
|
927
|
-
// in this user's pipeline events, multiply duration_sec by 1.5 on
|
|
928
|
-
// the recovery deploy. Full control via everything hook.
|
|
929
|
-
const pipelineEvents = userEvents
|
|
930
|
-
.filter(e => e.event === "deployment pipeline run")
|
|
931
|
-
.sort((a, b) => a.time.localeCompare(b.time));
|
|
932
|
-
for (let i = 1; i < pipelineEvents.length; i++) {
|
|
933
|
-
if (pipelineEvents[i - 1].status === "failed" && pipelineEvents[i].status === "success") {
|
|
934
|
-
pipelineEvents[i].duration_sec = Math.floor((pipelineEvents[i].duration_sec || 300) * 1.5);
|
|
935
|
-
}
|
|
936
|
-
}
|
|
937
|
-
|
|
938
|
-
// HOOK 9: INCIDENT RESPONSE TTC — enterprise resolves faster,
|
|
939
|
-
// startup resolves slower. Scale response_time_mins on
|
|
940
|
-
// acknowledged events and resolution_time_mins on resolved
|
|
941
|
-
// events by company_size. This compounds with H4 (integration
|
|
942
|
-
// users) — realistic: enterprise + good tooling = fastest.
|
|
943
|
-
const companySegment = profile?.company_size;
|
|
944
|
-
const ttcFactor = (
|
|
945
|
-
companySegment === "enterprise" ? 0.67 :
|
|
946
|
-
companySegment === "startup" ? 1.5 :
|
|
947
|
-
1.0
|
|
948
|
-
);
|
|
949
|
-
if (ttcFactor !== 1.0) {
|
|
950
|
-
// Timestamp shift: affects Mixpanel funnel TTC
|
|
951
|
-
const incidentSeq = findFirstSequence(
|
|
952
|
-
userEvents,
|
|
953
|
-
["alert triggered", "alert acknowledged", "alert resolved"],
|
|
954
|
-
60 * 24 * 30
|
|
955
|
-
);
|
|
956
|
-
if (incidentSeq) scaleFunnelTTC(incidentSeq, ttcFactor);
|
|
957
|
-
// Property scale: affects Insights AVG reports
|
|
958
|
-
userEvents.forEach(e => {
|
|
959
|
-
if (e.event === "alert acknowledged" && e.response_time_mins) {
|
|
960
|
-
e.response_time_mins = Math.max(1, Math.round(e.response_time_mins * ttcFactor));
|
|
961
|
-
}
|
|
962
|
-
if (e.event === "alert resolved" && e.resolution_time_mins) {
|
|
963
|
-
e.resolution_time_mins = Math.max(1, Math.round(e.resolution_time_mins * ttcFactor));
|
|
964
|
-
}
|
|
965
|
-
});
|
|
966
|
-
}
|
|
967
|
-
}
|
|
968
|
-
|
|
969
|
-
// HOOK 8: ENTERPRISE VS STARTUP (user) — company size determines
|
|
970
|
-
// seat count, ACV, and CSM. Real profile attrs.
|
|
971
|
-
if (type === "user") {
|
|
972
|
-
const companySize = record.company_size;
|
|
973
|
-
if (companySize === "enterprise") {
|
|
974
|
-
record.seat_count = chance.integer({ min: 50, max: 500 });
|
|
975
|
-
record.annual_contract_value = chance.integer({ min: 50000, max: 500000 });
|
|
976
|
-
record.customer_success_manager = true;
|
|
977
|
-
} else if (companySize === "mid_market") {
|
|
978
|
-
record.seat_count = chance.integer({ min: 10, max: 50 });
|
|
979
|
-
record.annual_contract_value = chance.integer({ min: 12000, max: 50000 });
|
|
980
|
-
record.customer_success_manager = false;
|
|
981
|
-
} else if (companySize === "smb") {
|
|
982
|
-
record.seat_count = chance.integer({ min: 3, max: 10 });
|
|
983
|
-
record.annual_contract_value = chance.integer({ min: 3600, max: 12000 });
|
|
984
|
-
record.customer_success_manager = false;
|
|
985
|
-
} else if (companySize === "startup") {
|
|
986
|
-
record.seat_count = chance.integer({ min: 1, max: 5 });
|
|
987
|
-
record.annual_contract_value = chance.integer({ min: 0, max: 3600 });
|
|
988
|
-
record.customer_success_manager = false;
|
|
989
|
-
}
|
|
990
|
-
record.customer_health_score = chance.integer({ min: 1, max: 100 });
|
|
991
|
-
}
|
|
992
|
-
|
|
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);
|
|
993
991
|
return record;
|
|
994
992
|
}
|
|
995
993
|
};
|