@ak--47/dungeon-master 1.3.0 → 1.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +58 -0
- package/dungeons/technical/array-of-object-lookup.js +0 -2
- package/dungeons/technical/hook-helpers-verify.js +89 -0
- package/dungeons/technical/identity-model-verify.js +47 -0
- package/dungeons/technical/pattern-aggregate-by-bin.js +41 -0
- package/dungeons/technical/pattern-attributed-by-source.js +42 -0
- package/dungeons/technical/pattern-frequency-by-frequency.js +40 -0
- package/dungeons/technical/pattern-funnel-frequency.js +54 -0
- package/dungeons/technical/pattern-ttc-by-segment.js +45 -0
- package/dungeons/technical/simple.js +3 -4
- package/dungeons/technical/simplest-schema.json +5 -0
- package/dungeons/technical/text-generation.js +1 -1
- package/dungeons/vertical/ai-platform.js +216 -164
- package/dungeons/vertical/community.js +74 -41
- package/dungeons/vertical/crypto.js +343 -233
- package/dungeons/vertical/dating.js +246 -292
- package/dungeons/vertical/devtools.js +179 -70
- package/dungeons/vertical/ecommerce.js +275 -122
- package/dungeons/vertical/education.js +345 -312
- package/dungeons/vertical/fintech.js +445 -314
- package/dungeons/vertical/fitness.js +199 -165
- package/dungeons/vertical/food-delivery.js +309 -330
- package/dungeons/vertical/gaming.js +387 -241
- package/dungeons/vertical/healthcare.js +149 -66
- package/dungeons/vertical/insurance-application.js +173 -75
- package/dungeons/vertical/logistics.js +107 -12
- package/dungeons/vertical/marketplace.js +171 -76
- package/dungeons/vertical/media.js +259 -402
- package/dungeons/vertical/real-estate.js +336 -318
- package/dungeons/vertical/sass.js +294 -288
- package/dungeons/vertical/social.js +289 -234
- package/dungeons/vertical/travel.js +66 -20
- package/index.js +17 -17
- package/lib/core/config-validator.js +226 -175
- package/lib/core/context.js +10 -24
- package/lib/core/storage.js +6 -1
- package/lib/generators/events.js +57 -104
- package/lib/generators/funnels.js +212 -93
- package/lib/generators/mirror.js +3 -2
- package/lib/generators/product-names.js +1 -1
- package/lib/generators/scd.js +2 -1
- package/lib/generators/text.js +1 -1
- package/lib/hook-helpers/_internal.js +23 -0
- package/lib/hook-helpers/cohort.js +124 -0
- package/lib/hook-helpers/identity.js +56 -0
- package/lib/hook-helpers/index.js +44 -0
- package/lib/hook-helpers/inject.js +99 -0
- package/lib/hook-helpers/mutate.js +151 -0
- package/lib/hook-helpers/timing.js +99 -0
- package/lib/hook-patterns/aggregate-per-user-by-bin.js +38 -0
- package/lib/hook-patterns/attributed-by-source.js +72 -0
- package/lib/hook-patterns/frequency-by-frequency.js +46 -0
- package/lib/hook-patterns/funnel-frequency-breakdown.js +73 -0
- package/lib/hook-patterns/index.js +14 -0
- package/lib/hook-patterns/time-to-convert-by-segment.js +41 -0
- package/lib/orchestrators/user-loop.js +143 -293
- package/lib/utils/utils.js +97 -52
- package/lib/verify/emulate-breakdown.js +281 -0
- package/lib/verify/index.js +12 -0
- package/lib/verify/verify-dungeon.js +61 -0
- package/package.json +6 -4
- package/scripts/smoke-test-all.mjs +162 -0
- package/scripts/verify-runner.mjs +72 -24
- package/types.d.ts +434 -225
- package/dungeons/technical/ad-spend-schema.json +0 -128
- package/dungeons/technical/anonymous-users-schema.json +0 -92
- package/dungeons/technical/array-of-object-lookup-schema.json +0 -191
- package/dungeons/technical/experiments-schema.json +0 -203
- package/dungeons/technical/foobar-schema.json +0 -362
- package/dungeons/technical/group-analytics-schema.json +0 -241
- package/dungeons/technical/mirror-strategies-schema.json +0 -84
- package/dungeons/technical/nested-objects-schema.json +0 -145
- package/dungeons/technical/retention-cadence-schema.json +0 -37
- package/dungeons/technical/sanity-schema.json +0 -185
- package/dungeons/technical/scale-test-schema.json +0 -70
- package/dungeons/technical/scd-schema.json +0 -467
- package/dungeons/technical/simple-schema.json +0 -362
- package/dungeons/technical/text-generation-schema.json +0 -1062
- package/dungeons/vertical/ai-platform-schema.json +0 -617
- package/dungeons/vertical/community-schema.json +0 -579
- package/dungeons/vertical/crypto-schema.json +0 -546
- package/dungeons/vertical/dating-schema.json +0 -401
- package/dungeons/vertical/devtools-schema.json +0 -601
- package/dungeons/vertical/ecommerce-schema.json +0 -604
- package/dungeons/vertical/education-schema.json +0 -5686
- package/dungeons/vertical/fintech-schema.json +0 -630
- package/dungeons/vertical/fitness-schema.json +0 -530
- package/dungeons/vertical/food-delivery-schema.json +0 -36728
- package/dungeons/vertical/gaming-schema.json +0 -2703
- package/dungeons/vertical/healthcare-schema.json +0 -549
- package/dungeons/vertical/insurance-application-schema.json +0 -485
- package/dungeons/vertical/logistics-schema.json +0 -574
- package/dungeons/vertical/marketplace-schema.json +0 -533
- package/dungeons/vertical/media-schema.json +0 -4749
- package/dungeons/vertical/real-estate-schema.json +0 -527
- package/dungeons/vertical/sass-schema.json +0 -3128
- package/dungeons/vertical/social-schema.json +0 -620
- package/dungeons/vertical/travel-schema.json +0 -580
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// ── TWEAK THESE ──
|
|
2
2
|
const SEED = "harness-sass";
|
|
3
|
-
const num_days =
|
|
4
|
-
const num_users =
|
|
3
|
+
const num_days = 120;
|
|
4
|
+
const num_users = 10_000;
|
|
5
5
|
const avg_events_per_user_per_day = 1.2;
|
|
6
6
|
let token = "your-mixpanel-token";
|
|
7
7
|
|
|
@@ -52,153 +52,217 @@ const chance = u.initChance(SEED);
|
|
|
52
52
|
|
|
53
53
|
/*
|
|
54
54
|
* ═══════════════════════════════════════════════════════════════════════════════
|
|
55
|
-
* ANALYTICS HOOKS
|
|
55
|
+
* ANALYTICS HOOKS (10 hooks)
|
|
56
56
|
* ═══════════════════════════════════════════════════════════════════════════════
|
|
57
57
|
*
|
|
58
|
-
*
|
|
59
|
-
*
|
|
60
|
-
*
|
|
58
|
+
* 10 deliberately architected patterns hidden in the data. NOTE: All cohort
|
|
59
|
+
* effects are HIDDEN — no flag stamping. Discoverable via behavioral cohorts
|
|
60
|
+
* or raw-prop breakdowns (company_size, day, doc_section). Adds:
|
|
61
|
+
* 9. INCIDENT RESPONSE TIME-TO-CONVERT (Enterprise 1.4x faster vs Startup)
|
|
62
|
+
* [funnel-post: visible only in Mixpanel funnel median TTC; cross-event
|
|
63
|
+
* MIN→MIN SQL queries do NOT show this]
|
|
64
|
+
* 10. DOCS MAGIC NUMBER (sweet 4-7 docs → +40% deploys; over 8+ → drop 25%)
|
|
61
65
|
*
|
|
62
66
|
* ─────────────────────────────────────────────────────────────────────────────
|
|
63
|
-
* 1. END-OF-QUARTER SPIKE (event
|
|
67
|
+
* 1. END-OF-QUARTER SPIKE (event)
|
|
64
68
|
* ─────────────────────────────────────────────────────────────────────────────
|
|
65
69
|
*
|
|
66
|
-
* Days
|
|
67
|
-
* member invitations are duplicated 50% of the time.
|
|
70
|
+
* PATTERN: Days 100-110 billing events shift event_type toward "plan_upgraded"
|
|
71
|
+
* 40% of the time and team member invitations are duplicated 50% of the time.
|
|
72
|
+
* No flag — discover via line chart by day.
|
|
68
73
|
*
|
|
69
|
-
*
|
|
70
|
-
* • Insights line chart
|
|
71
|
-
* • Event: "billing event", filter "event_type" = "plan_upgraded"
|
|
72
|
-
* • Daily trend
|
|
73
|
-
* • Expected: Spike in plan upgrades during days 80-90 (4x normal volume)
|
|
74
|
+
* HOW TO FIND IT IN MIXPANEL:
|
|
74
75
|
*
|
|
75
|
-
*
|
|
76
|
-
*
|
|
77
|
-
*
|
|
78
|
-
*
|
|
79
|
-
*
|
|
76
|
+
* Report 1: Plan Upgrades Over Time
|
|
77
|
+
* - Report type: Insights
|
|
78
|
+
* - Event: "billing event"
|
|
79
|
+
* - Measure: Total
|
|
80
|
+
* - Filter: event_type = "plan_upgraded"
|
|
81
|
+
* - Line chart by day
|
|
82
|
+
* - Expected: ~4x normal upgrade volume during days 100-110
|
|
83
|
+
*
|
|
84
|
+
* Report 2: Team Expansion Surge
|
|
85
|
+
* - Report type: Insights
|
|
86
|
+
* - Event: "team member invited"
|
|
87
|
+
* - Measure: Total
|
|
88
|
+
* - Line chart by day
|
|
89
|
+
* - Expected: clear volume spike in the final 10 days from duplicated invites
|
|
90
|
+
*
|
|
91
|
+
* REAL-WORLD ANALOGUE: B2B SaaS revenue clusters at quarter-close as sales
|
|
92
|
+
* teams pull deals forward and customers expand seats to lock in pricing.
|
|
80
93
|
*
|
|
81
94
|
* ─────────────────────────────────────────────────────────────────────────────
|
|
82
|
-
* 2. CHURNED ACCOUNT SILENCING (everything
|
|
95
|
+
* 2. CHURNED ACCOUNT SILENCING (everything)
|
|
83
96
|
* ─────────────────────────────────────────────────────────────────────────────
|
|
84
97
|
*
|
|
85
|
-
* ~10% of users (
|
|
86
|
-
* after day 30.
|
|
87
|
-
*
|
|
98
|
+
* PATTERN: ~10-20% of users (deterministic via distinct_id char hash) go
|
|
99
|
+
* completely silent after day 30. All post-d30 events are removed via splice().
|
|
100
|
+
* No flag — derive cohort via behavioral retention bucket (users with zero
|
|
101
|
+
* activity past d30 vs the rest).
|
|
102
|
+
*
|
|
103
|
+
* HOW TO FIND IT IN MIXPANEL:
|
|
88
104
|
*
|
|
89
|
-
*
|
|
90
|
-
*
|
|
91
|
-
*
|
|
92
|
-
*
|
|
93
|
-
* • Expected: churned_account=true shows 0% retention after day 30
|
|
105
|
+
* Report 1: Retention Cliff
|
|
106
|
+
* - Cohort A: users with at least 1 event AFTER day 30
|
|
107
|
+
* - Cohort B: users with events ONLY in days 1-30
|
|
108
|
+
* - Compare cohort sizes — B should be ~10-20% of total
|
|
94
109
|
*
|
|
95
|
-
*
|
|
96
|
-
*
|
|
97
|
-
*
|
|
98
|
-
*
|
|
99
|
-
*
|
|
100
|
-
*
|
|
110
|
+
* Report 2: Activity Volume Pre/Post Day 30
|
|
111
|
+
* - Report type: Insights
|
|
112
|
+
* - Event: any event
|
|
113
|
+
* - Measure: Total per user
|
|
114
|
+
* - Line chart by day
|
|
115
|
+
* - Expected: a visible drop after d30 driven by the silent cohort
|
|
116
|
+
*
|
|
117
|
+
* REAL-WORLD ANALOGUE: Most SaaS churn happens silently — accounts simply
|
|
118
|
+
* stop logging in long before the formal cancellation lands.
|
|
101
119
|
*
|
|
102
120
|
* ─────────────────────────────────────────────────────────────────────────────
|
|
103
|
-
* 3. ALERT ESCALATION REPLACEMENT (event
|
|
121
|
+
* 3. ALERT ESCALATION REPLACEMENT (event)
|
|
104
122
|
* ─────────────────────────────────────────────────────────────────────────────
|
|
105
123
|
*
|
|
106
|
-
* 30% of critical/emergency "alert triggered" events are REPLACED
|
|
107
|
-
*
|
|
108
|
-
* escalation_level (P1/P2), teams_paged, incident_id.
|
|
124
|
+
* PATTERN: 30% of critical/emergency "alert triggered" events are REPLACED
|
|
125
|
+
* with a hook-only "incident created" event (not in the events array). The
|
|
126
|
+
* new event carries escalation_level (P1/P2), teams_paged, and incident_id.
|
|
127
|
+
*
|
|
128
|
+
* HOW TO FIND IT IN MIXPANEL:
|
|
109
129
|
*
|
|
110
|
-
*
|
|
111
|
-
*
|
|
112
|
-
*
|
|
113
|
-
*
|
|
114
|
-
*
|
|
130
|
+
* Report 1: Incident Created Discovery
|
|
131
|
+
* - Report type: Insights
|
|
132
|
+
* - Event: "incident created"
|
|
133
|
+
* - Measure: Total
|
|
134
|
+
* - Breakdown: "escalation_level"
|
|
135
|
+
* - Expected: P1 and P2 incidents, ~30% of critical/emergency alert volume
|
|
115
136
|
*
|
|
116
|
-
*
|
|
117
|
-
*
|
|
118
|
-
*
|
|
119
|
-
*
|
|
137
|
+
* Report 2: Alert vs Incident Ratio
|
|
138
|
+
* - Report type: Insights
|
|
139
|
+
* - Events: "alert triggered" AND "incident created"
|
|
140
|
+
* - Measure: Total
|
|
141
|
+
* - Expected: incident count ~ 30% of critical+emergency alert count
|
|
142
|
+
*
|
|
143
|
+
* REAL-WORLD ANALOGUE: Severe alerts get auto-promoted into incident
|
|
144
|
+
* tickets that page on-call engineers and trigger customer comms.
|
|
120
145
|
*
|
|
121
146
|
* ─────────────────────────────────────────────────────────────────────────────
|
|
122
|
-
* 4. INTEGRATION USERS SUCCEED (everything
|
|
147
|
+
* 4. INTEGRATION USERS SUCCEED (everything)
|
|
123
148
|
* ─────────────────────────────────────────────────────────────────────────────
|
|
124
149
|
*
|
|
125
|
-
* Users with BOTH
|
|
126
|
-
* response_time_mins reduced 60%, resolution_time_mins
|
|
127
|
-
*
|
|
150
|
+
* PATTERN: Users with BOTH "slack" AND "pagerduty" "integration configured"
|
|
151
|
+
* events resolve alerts faster: response_time_mins reduced 60%, resolution_time_mins
|
|
152
|
+
* reduced 50%. No flag — derive cohort behaviorally.
|
|
153
|
+
*
|
|
154
|
+
* HOW TO FIND IT IN MIXPANEL:
|
|
155
|
+
*
|
|
156
|
+
* Report 1: Response Time by Integration Cohort
|
|
157
|
+
* - Cohort A: users who configured BOTH slack AND pagerduty integrations
|
|
158
|
+
* - Cohort B: rest
|
|
159
|
+
* - Event: "alert acknowledged"
|
|
160
|
+
* - Measure: Average of "response_time_mins"
|
|
161
|
+
* - Expected: A ~ 60% lower response time
|
|
128
162
|
*
|
|
129
|
-
*
|
|
130
|
-
*
|
|
131
|
-
*
|
|
132
|
-
*
|
|
133
|
-
*
|
|
163
|
+
* Report 2: Resolution Time by Integration Cohort
|
|
164
|
+
* - Cohort A vs B (as above)
|
|
165
|
+
* - Event: "alert resolved"
|
|
166
|
+
* - Measure: Average of "resolution_time_mins"
|
|
167
|
+
* - Expected: A ~ 50% faster resolution
|
|
134
168
|
*
|
|
135
|
-
*
|
|
136
|
-
*
|
|
137
|
-
* • Event: "alert resolved", measure avg "resolution_time_mins"
|
|
138
|
-
* • Breakdown: "integrated_team"
|
|
139
|
-
* • Expected: integrated_team=true ~ 50% faster resolution
|
|
169
|
+
* REAL-WORLD ANALOGUE: Teams that wire alerting into their existing comms
|
|
170
|
+
* stack respond minutes faster — the alert literally finds the human.
|
|
140
171
|
*
|
|
141
172
|
* ─────────────────────────────────────────────────────────────────────────────
|
|
142
|
-
* 5. DOCS READERS DEPLOY MORE (everything
|
|
173
|
+
* 5. DOCS READERS DEPLOY MORE (everything)
|
|
143
174
|
* ─────────────────────────────────────────────────────────────────────────────
|
|
144
175
|
*
|
|
145
|
-
* Users with 3+ "
|
|
146
|
-
*
|
|
176
|
+
* PATTERN: Users with 3+ "documentation viewed" events where doc_section
|
|
177
|
+
* (or equivalent prop) indicates best-practices reading get 2-3 extra
|
|
178
|
+
* production deploys spliced in. No flag — derive cohort by counting
|
|
179
|
+
* doc views per user.
|
|
147
180
|
*
|
|
148
|
-
*
|
|
149
|
-
* • Insights report
|
|
150
|
-
* • Event: "service deployed", filter "environment" = "production"
|
|
151
|
-
* • Breakdown: "docs_informed"
|
|
152
|
-
* • Expected: docs_informed=true shows extra production deployments
|
|
181
|
+
* HOW TO FIND IT IN MIXPANEL:
|
|
153
182
|
*
|
|
154
|
-
*
|
|
155
|
-
*
|
|
156
|
-
*
|
|
157
|
-
*
|
|
158
|
-
*
|
|
183
|
+
* Report 1: Per-User Deploy Volume by Docs Cohort
|
|
184
|
+
* - Cohort A: users with >= 3 "documentation viewed" events
|
|
185
|
+
* - Cohort B: users with < 3
|
|
186
|
+
* - Event: "service deployed"
|
|
187
|
+
* - Measure: Total per user
|
|
188
|
+
* - Expected: A ~ 1.8x B
|
|
189
|
+
*
|
|
190
|
+
* REAL-WORLD ANALOGUE: Engineers who read the docs ship more confidently
|
|
191
|
+
* and more often than those who guess at the platform.
|
|
159
192
|
*
|
|
160
193
|
* ─────────────────────────────────────────────────────────────────────────────
|
|
161
|
-
* 6. COST OVERRUN PATTERN (event
|
|
194
|
+
* 6. COST OVERRUN PATTERN (event — closure state)
|
|
162
195
|
* ─────────────────────────────────────────────────────────────────────────────
|
|
163
196
|
*
|
|
164
|
-
* When cost_change_percent > 25 on a "cost report generated" event,
|
|
165
|
-
* is stored in a module-level Map. Their next "infrastructure scaled"
|
|
166
|
-
* is forced to scale_direction
|
|
197
|
+
* PATTERN: When cost_change_percent > 25 on a "cost report generated" event,
|
|
198
|
+
* the user is stored in a module-level Map. Their next "infrastructure scaled"
|
|
199
|
+
* event is forced to scale_direction = "down". No flag — discover by
|
|
200
|
+
* sequencing cost-report → infrastructure-scaled per user.
|
|
201
|
+
*
|
|
202
|
+
* HOW TO FIND IT IN MIXPANEL:
|
|
167
203
|
*
|
|
168
|
-
*
|
|
169
|
-
*
|
|
170
|
-
*
|
|
171
|
-
*
|
|
172
|
-
*
|
|
204
|
+
* Report 1: Scale Direction Distribution
|
|
205
|
+
* - Report type: Insights
|
|
206
|
+
* - Event: "infrastructure scaled"
|
|
207
|
+
* - Measure: Total
|
|
208
|
+
* - Breakdown: "scale_direction"
|
|
209
|
+
* - Expected: "down" share is elevated above the configured baseline
|
|
210
|
+
*
|
|
211
|
+
* Report 2: Sequencing Check
|
|
212
|
+
* - Inspect users with cost_change_percent > 25 on cost report;
|
|
213
|
+
* their next "infrastructure scaled" should be scale_direction="down"
|
|
214
|
+
* - Expected: ~100% match for the next-scale event after a cost spike
|
|
215
|
+
*
|
|
216
|
+
* REAL-WORLD ANALOGUE: A surprise cloud bill triggers an immediate
|
|
217
|
+
* downscale; no engineer ignores a 25% month-over-month cost jump.
|
|
173
218
|
*
|
|
174
219
|
* ─────────────────────────────────────────────────────────────────────────────
|
|
175
|
-
* 7. FAILED DEPLOYMENT RECOVERY (event
|
|
220
|
+
* 7. FAILED DEPLOYMENT RECOVERY (event — closure state)
|
|
176
221
|
* ─────────────────────────────────────────────────────────────────────────────
|
|
177
222
|
*
|
|
178
|
-
* After a failed pipeline run, the user's next successful deploy has
|
|
179
|
-
* duration_sec * 1.5 (recovery deploys are slower).
|
|
180
|
-
*
|
|
223
|
+
* PATTERN: After a failed pipeline run, the user's next successful deploy has
|
|
224
|
+
* duration_sec * 1.5 (recovery deploys are slower). Uses a module-level Map
|
|
225
|
+
* for cross-call state. No flag — discover by sequencing failed → next-success
|
|
226
|
+
* pipeline events per user and comparing duration.
|
|
227
|
+
*
|
|
228
|
+
* HOW TO FIND IT IN MIXPANEL:
|
|
181
229
|
*
|
|
182
|
-
*
|
|
183
|
-
*
|
|
184
|
-
*
|
|
185
|
-
*
|
|
186
|
-
*
|
|
230
|
+
* Report 1: Pipeline Duration After Failure (sequencing query)
|
|
231
|
+
* - For each user, find runs where prior run was status="failed"
|
|
232
|
+
* - Compare avg duration_sec of those "next" runs vs all other successful runs
|
|
233
|
+
* - Expected: post-failure runs ~ 1.5x longer duration
|
|
234
|
+
*
|
|
235
|
+
* REAL-WORLD ANALOGUE: After a bad deploy, teams add manual gates and
|
|
236
|
+
* extra verification steps that slow the very next release.
|
|
187
237
|
*
|
|
188
238
|
* ─────────────────────────────────────────────────────────────────────────────
|
|
189
|
-
* 8. ENTERPRISE VS STARTUP (user
|
|
239
|
+
* 8. ENTERPRISE VS STARTUP (user)
|
|
190
240
|
* ─────────────────────────────────────────────────────────────────────────────
|
|
191
241
|
*
|
|
192
|
-
* Company size determines seat_count, annual_contract_value, and
|
|
193
|
-
* customer_success_manager (enterprise only). All users get
|
|
242
|
+
* PATTERN: Company size determines seat_count, annual_contract_value, and
|
|
243
|
+
* customer_success_manager (enterprise only). All users get a
|
|
244
|
+
* customer_health_score on the profile.
|
|
245
|
+
*
|
|
246
|
+
* HOW TO FIND IT IN MIXPANEL:
|
|
194
247
|
*
|
|
195
|
-
*
|
|
196
|
-
*
|
|
197
|
-
*
|
|
198
|
-
*
|
|
199
|
-
*
|
|
248
|
+
* Report 1: ACV by Company Size
|
|
249
|
+
* - Report type: Insights
|
|
250
|
+
* - Event: any event
|
|
251
|
+
* - Measure: Unique users
|
|
252
|
+
* - Breakdown: "company_size" (user property)
|
|
253
|
+
* - Expected: startup ($0-3.6K), smb ($3.6K-12K), mid_market ($12K-50K),
|
|
200
254
|
* enterprise ($50K-500K)
|
|
201
255
|
*
|
|
256
|
+
* Report 2: Seat Count by Company Size
|
|
257
|
+
* - Report type: Insights
|
|
258
|
+
* - Event: any event
|
|
259
|
+
* - Measure: Average of "seat_count" (user property)
|
|
260
|
+
* - Breakdown: "company_size"
|
|
261
|
+
* - Expected: monotonic ramp from startup to enterprise
|
|
262
|
+
*
|
|
263
|
+
* REAL-WORLD ANALOGUE: B2B SaaS pricing scales orders of magnitude across
|
|
264
|
+
* customer segments — from a $99/mo startup to a $500K Fortune 500 contract.
|
|
265
|
+
*
|
|
202
266
|
* ─────────────────────────────────────────────────────────────────────────────
|
|
203
267
|
* EXPECTED METRICS SUMMARY
|
|
204
268
|
* ─────────────────────────────────────────────────────────────────────────────
|
|
@@ -257,10 +321,16 @@ const failedDeployUsers = new Map();
|
|
|
257
321
|
const config = {
|
|
258
322
|
token,
|
|
259
323
|
seed: SEED,
|
|
260
|
-
|
|
324
|
+
datasetStart: "2026-01-01T00:00:00Z",
|
|
325
|
+
datasetEnd: "2026-05-01T23:59:59Z",
|
|
326
|
+
// numDays: num_days,
|
|
261
327
|
avgEventsPerUserPerDay: avg_events_per_user_per_day,
|
|
262
328
|
numUsers: num_users,
|
|
263
|
-
|
|
329
|
+
// Phase 2 identity model — B2B SaaS reference. Engineers commonly use 1-2
|
|
330
|
+
// devices (desktop + work laptop). avgDevicePerUser:2 puts a meaningful
|
|
331
|
+
// per-session sticky-device pattern in Mixpanel device dashboards.
|
|
332
|
+
hasAnonIds: true,
|
|
333
|
+
avgDevicePerUser: 2,
|
|
264
334
|
hasSessionIds: true,
|
|
265
335
|
format: "json",
|
|
266
336
|
gzip: true,
|
|
@@ -294,10 +364,13 @@ const config = {
|
|
|
294
364
|
|
|
295
365
|
funnels: [
|
|
296
366
|
{
|
|
367
|
+
// First funnel — `workspace created` is the auth event for B2B users.
|
|
368
|
+
// Models real B2B onboarding: most teams take ≤1 retry before sticking.
|
|
297
369
|
sequence: ["workspace created", "service deployed", "dashboard viewed"],
|
|
298
370
|
isFirstFunnel: true,
|
|
299
371
|
conversionRate: 70,
|
|
300
372
|
timeToConvert: 2,
|
|
373
|
+
attempts: { min: 0, max: 1 },
|
|
301
374
|
},
|
|
302
375
|
{
|
|
303
376
|
// Daily monitoring: dashboards, queries, API calls (most common)
|
|
@@ -355,6 +428,10 @@ const config = {
|
|
|
355
428
|
event: "workspace created",
|
|
356
429
|
weight: 1,
|
|
357
430
|
isFirstEvent: true,
|
|
431
|
+
// Phase 2 identity: workspace creation is the B2B equivalent of Sign Up
|
|
432
|
+
// — engine stamps user_id+device_id on this event when it fires inside
|
|
433
|
+
// the user's first funnel.
|
|
434
|
+
isAuthEvent: true,
|
|
358
435
|
properties: {
|
|
359
436
|
company_size: ["startup", "smb", "mid_market", "enterprise"],
|
|
360
437
|
industry: ["tech", "finance", "healthcare", "retail", "media"],
|
|
@@ -368,7 +445,6 @@ const config = {
|
|
|
368
445
|
service_type: ["web_app", "api", "database", "cache", "queue", "ml_model"],
|
|
369
446
|
environment: ["production", "staging", "dev"],
|
|
370
447
|
cloud_provider: ["aws", "gcp", "azure"],
|
|
371
|
-
docs_informed: [false],
|
|
372
448
|
}
|
|
373
449
|
},
|
|
374
450
|
{
|
|
@@ -409,7 +485,6 @@ const config = {
|
|
|
409
485
|
alert_id: alertIds,
|
|
410
486
|
response_time_mins: u.weighNumRange(1, 120),
|
|
411
487
|
acknowledged_by_role: ["engineer", "sre", "manager", "oncall"],
|
|
412
|
-
integrated_team: [false],
|
|
413
488
|
}
|
|
414
489
|
},
|
|
415
490
|
{
|
|
@@ -419,7 +494,6 @@ const config = {
|
|
|
419
494
|
alert_id: alertIds,
|
|
420
495
|
resolution_time_mins: u.weighNumRange(5, 1440),
|
|
421
496
|
root_cause: ["config_change", "capacity", "bug", "dependency", "network"],
|
|
422
|
-
integrated_team: [false],
|
|
423
497
|
}
|
|
424
498
|
},
|
|
425
499
|
{
|
|
@@ -430,7 +504,6 @@ const config = {
|
|
|
430
504
|
status: ["success", "failed", "cancelled"],
|
|
431
505
|
duration_sec: u.weighNumRange(30, 1800),
|
|
432
506
|
commit_count: u.weighNumRange(1, 20),
|
|
433
|
-
recovery_deployment: [false],
|
|
434
507
|
}
|
|
435
508
|
},
|
|
436
509
|
{
|
|
@@ -438,11 +511,10 @@ const config = {
|
|
|
438
511
|
weight: 5,
|
|
439
512
|
properties: {
|
|
440
513
|
service_id: serviceIds,
|
|
441
|
-
scale_direction: ["
|
|
514
|
+
scale_direction: ["up", "up", "up", "down"],
|
|
442
515
|
previous_capacity: u.weighNumRange(1, 100),
|
|
443
516
|
new_capacity: u.weighNumRange(1, 100),
|
|
444
517
|
auto_scaled: [false, false, false, false, false, false, true],
|
|
445
|
-
cost_reaction: [false],
|
|
446
518
|
}
|
|
447
519
|
},
|
|
448
520
|
{
|
|
@@ -452,8 +524,6 @@ const config = {
|
|
|
452
524
|
report_period: ["daily", "weekly", "monthly"],
|
|
453
525
|
total_cost: u.weighNumRange(100, 50000),
|
|
454
526
|
cost_change_percent: u.weighNumRange(-30, 50),
|
|
455
|
-
cost_alert: [false],
|
|
456
|
-
budget_exceeded: [false],
|
|
457
527
|
}
|
|
458
528
|
},
|
|
459
529
|
{
|
|
@@ -462,8 +532,6 @@ const config = {
|
|
|
462
532
|
properties: {
|
|
463
533
|
role: ["admin", "editor", "viewer", "billing"],
|
|
464
534
|
invitation_method: ["email", "sso", "slack"],
|
|
465
|
-
quarter_end_push: [false],
|
|
466
|
-
duplicate_invite: [false],
|
|
467
535
|
}
|
|
468
536
|
},
|
|
469
537
|
{
|
|
@@ -496,9 +564,8 @@ const config = {
|
|
|
496
564
|
event: "billing event",
|
|
497
565
|
weight: 3,
|
|
498
566
|
properties: {
|
|
499
|
-
event_type: ["invoice_generated", "payment_received", "payment_failed", "plan_upgraded", "plan_downgraded"],
|
|
567
|
+
event_type: ["invoice_generated", "invoice_generated", "payment_received", "payment_received", "payment_received", "payment_failed", "plan_upgraded", "plan_downgraded"],
|
|
500
568
|
amount: u.weighNumRange(99, 25000),
|
|
501
|
-
quarter_end_push: [false],
|
|
502
569
|
}
|
|
503
570
|
},
|
|
504
571
|
{
|
|
@@ -548,7 +615,6 @@ const config = {
|
|
|
548
615
|
company_size: ["startup", "startup", "smb", "mid_market", "enterprise"],
|
|
549
616
|
primary_role: ["engineer", "sre", "devops", "manager", "executive"],
|
|
550
617
|
team_name: ["Platform", "Backend", "Frontend", "Data", "Security", "Infrastructure"],
|
|
551
|
-
churned_account: [false],
|
|
552
618
|
seat_count: [1],
|
|
553
619
|
annual_contract_value: [0],
|
|
554
620
|
customer_success_manager: [false],
|
|
@@ -577,7 +643,7 @@ const config = {
|
|
|
577
643
|
*
|
|
578
644
|
* This hook function creates 8 deliberate patterns in the data:
|
|
579
645
|
*
|
|
580
|
-
* 1. END-OF-QUARTER SPIKE: Days
|
|
646
|
+
* 1. END-OF-QUARTER SPIKE: Days 100-110 drive plan upgrades and team expansion
|
|
581
647
|
* 2. CHURNED ACCOUNT SILENCING: ~10% of users go completely silent after month 1
|
|
582
648
|
* 3. ALERT ESCALATION REPLACEMENT: Critical alerts become "incident created" events
|
|
583
649
|
* 4. INTEGRATION USERS SUCCEED: Slack+PagerDuty users resolve incidents 50-60% faster
|
|
@@ -587,55 +653,14 @@ const config = {
|
|
|
587
653
|
* 8. ENTERPRISE VS STARTUP: Company size determines seat count, ACV, and health score
|
|
588
654
|
*/
|
|
589
655
|
hook: function (record, type, meta) {
|
|
590
|
-
|
|
591
|
-
const DATASET_START = NOW.subtract(num_days, "days");
|
|
656
|
+
// (Hook 1a moved to everything hook for reliable datasetStart access)
|
|
592
657
|
|
|
593
|
-
//
|
|
594
|
-
//
|
|
595
|
-
// Days 80-90: billing upgrades and team expansion surge
|
|
596
|
-
// ─────────────────────────────────────────────────────────────
|
|
597
|
-
if (type === "event") {
|
|
598
|
-
const EVENT_TIME = dayjs(record.time);
|
|
599
|
-
const dayInDataset = EVENT_TIME.diff(DATASET_START, "days", true);
|
|
600
|
-
|
|
601
|
-
if (record.event === "billing event") {
|
|
602
|
-
if (dayInDataset >= 80 && dayInDataset <= 90 && chance.bool({ likelihood: 40 })) {
|
|
603
|
-
record.event_type = "plan_upgraded";
|
|
604
|
-
record.quarter_end_push = true;
|
|
605
|
-
} else {
|
|
606
|
-
record.quarter_end_push = false;
|
|
607
|
-
}
|
|
608
|
-
}
|
|
609
|
-
|
|
610
|
-
if (record.event === "team member invited") {
|
|
611
|
-
if (dayInDataset >= 80 && dayInDataset <= 90) {
|
|
612
|
-
record.quarter_end_push = true;
|
|
613
|
-
// 50% of the time duplicate the invite event (hiring push)
|
|
614
|
-
if (chance.bool({ likelihood: 50 })) {
|
|
615
|
-
return {
|
|
616
|
-
...record,
|
|
617
|
-
time: EVENT_TIME.add(chance.integer({ min: 1, max: 60 }), "minutes").toISOString(),
|
|
618
|
-
role: chance.pickone(["editor", "viewer"]),
|
|
619
|
-
invitation_method: chance.pickone(["email", "sso", "slack"]),
|
|
620
|
-
quarter_end_push: true,
|
|
621
|
-
duplicate_invite: true,
|
|
622
|
-
};
|
|
623
|
-
}
|
|
624
|
-
} else {
|
|
625
|
-
record.quarter_end_push = false;
|
|
626
|
-
}
|
|
627
|
-
}
|
|
628
|
-
}
|
|
629
|
-
|
|
630
|
-
// ─────────────────────────────────────────────────────────────
|
|
631
|
-
// Hook #3: ALERT ESCALATION REPLACEMENT (event)
|
|
632
|
-
// Critical/emergency alerts sometimes become formal incidents
|
|
633
|
-
// ─────────────────────────────────────────────────────────────
|
|
658
|
+
// HOOK 3: ALERT ESCALATION REPLACEMENT (event) — critical/emergency
|
|
659
|
+
// alerts sometimes become incident-created events. Real product flow.
|
|
634
660
|
if (type === "event") {
|
|
635
661
|
if (record.event === "alert triggered") {
|
|
636
662
|
const severity = record.severity;
|
|
637
663
|
if ((severity === "critical" || severity === "emergency") && chance.bool({ likelihood: 30 })) {
|
|
638
|
-
// REPLACE the event entirely with an "incident created" event
|
|
639
664
|
return {
|
|
640
665
|
...record,
|
|
641
666
|
event: "incident created",
|
|
@@ -650,178 +675,160 @@ const config = {
|
|
|
650
675
|
}
|
|
651
676
|
}
|
|
652
677
|
|
|
653
|
-
//
|
|
654
|
-
//
|
|
655
|
-
//
|
|
656
|
-
// Uses module-level costOverrunUsers Map for cross-call state
|
|
657
|
-
// ─────────────────────────────────────────────────────────────
|
|
678
|
+
// HOOK 6: COST OVERRUN PATTERN (event) — cost reports with cost_change
|
|
679
|
+
// > 25% record user, then next infrastructure-scaled event from that
|
|
680
|
+
// user gets scale_direction = "down". No flag.
|
|
658
681
|
if (type === "event") {
|
|
659
|
-
if (record.event === "cost report generated") {
|
|
660
|
-
|
|
661
|
-
if (costChange > 25) {
|
|
662
|
-
record.cost_alert = true;
|
|
663
|
-
record.budget_exceeded = true;
|
|
664
|
-
costOverrunUsers.set(record.user_id, true);
|
|
665
|
-
} else {
|
|
666
|
-
record.cost_alert = false;
|
|
667
|
-
record.budget_exceeded = false;
|
|
668
|
-
}
|
|
682
|
+
if (record.event === "cost report generated" && record.cost_change_percent > 25) {
|
|
683
|
+
costOverrunUsers.set(record.user_id, true);
|
|
669
684
|
}
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
record.scale_direction = "down";
|
|
674
|
-
record.cost_reaction = true;
|
|
675
|
-
costOverrunUsers.delete(record.user_id);
|
|
676
|
-
} else {
|
|
677
|
-
record.cost_reaction = false;
|
|
678
|
-
}
|
|
685
|
+
if (record.event === "infrastructure scaled" && costOverrunUsers.has(record.user_id)) {
|
|
686
|
+
record.scale_direction = "down";
|
|
687
|
+
costOverrunUsers.delete(record.user_id);
|
|
679
688
|
}
|
|
680
689
|
}
|
|
681
690
|
|
|
682
|
-
// ─────────────────────────────────────────────────────────────
|
|
683
|
-
// Hook #7: FAILED DEPLOYMENT RECOVERY (event)
|
|
684
|
-
// Recovery deploys take 1.5x longer after a failure
|
|
685
|
-
// Uses module-level failedDeployUsers Map for cross-call state
|
|
686
|
-
// ─────────────────────────────────────────────────────────────
|
|
687
|
-
if (type === "event") {
|
|
688
|
-
if (record.event === "deployment pipeline run") {
|
|
689
|
-
const status = record.status;
|
|
690
|
-
if (status === "failed") {
|
|
691
|
-
failedDeployUsers.set(record.user_id, true);
|
|
692
|
-
record.recovery_deployment = false;
|
|
693
|
-
} else if (status === "success" && failedDeployUsers.has(record.user_id)) {
|
|
694
|
-
record.duration_sec = Math.floor((record.duration_sec || 300) * 1.5);
|
|
695
|
-
record.recovery_deployment = true;
|
|
696
|
-
failedDeployUsers.delete(record.user_id);
|
|
697
|
-
} else {
|
|
698
|
-
record.recovery_deployment = false;
|
|
699
|
-
}
|
|
700
|
-
}
|
|
701
|
-
}
|
|
702
|
-
|
|
703
|
-
// ─────────────────────────────────────────────────────────────
|
|
704
|
-
// Hook #2: CHURNED ACCOUNT SILENCING (everything)
|
|
705
|
-
// ~20% targeted (hash % 5), yielding ~10% visible after accounting for invisible churned users
|
|
706
|
-
// ─────────────────────────────────────────────────────────────
|
|
707
691
|
if (type === "everything") {
|
|
692
|
+
const datasetStart = dayjs.unix(meta.datasetStart);
|
|
708
693
|
const userEvents = record;
|
|
709
694
|
const profile = meta.profile;
|
|
710
695
|
|
|
711
|
-
// Stamp superProps from profile for consistency
|
|
712
696
|
userEvents.forEach(e => {
|
|
713
697
|
e.plan_tier = profile.plan_tier;
|
|
714
698
|
e.cloud_provider = profile.cloud_provider;
|
|
715
699
|
});
|
|
716
700
|
|
|
701
|
+
// HOOK 1a: END-OF-QUARTER SPIKE — days 100-110, billing events flip
|
|
702
|
+
// event_type to plan_upgraded 40% of the time. No flag.
|
|
703
|
+
userEvents.forEach(e => {
|
|
704
|
+
if (e.event !== "billing event") return;
|
|
705
|
+
const dayInDataset = dayjs(e.time).diff(datasetStart, "days", true);
|
|
706
|
+
if (dayInDataset >= 100 && dayInDataset <= 110 && chance.bool({ likelihood: 40 })) {
|
|
707
|
+
e.event_type = "plan_upgraded";
|
|
708
|
+
}
|
|
709
|
+
});
|
|
710
|
+
|
|
711
|
+
// HOOK 1b: END-OF-QUARTER TEAM INVITE SPIKE — days 100-110, clone
|
|
712
|
+
// 50% of team-member-invited events (push, not return). No flag.
|
|
713
|
+
for (let i = userEvents.length - 1; i >= 0; i--) {
|
|
714
|
+
const e = userEvents[i];
|
|
715
|
+
if (e.event !== "team member invited") continue;
|
|
716
|
+
const dayInDataset = dayjs(e.time).diff(datasetStart, "days", true);
|
|
717
|
+
if (dayInDataset >= 100 && dayInDataset <= 110 && chance.bool({ likelihood: 50 })) {
|
|
718
|
+
userEvents.push({
|
|
719
|
+
...e,
|
|
720
|
+
time: dayjs(e.time).add(chance.integer({ min: 1, max: 60 }), "minutes").toISOString(),
|
|
721
|
+
user_id: e.user_id,
|
|
722
|
+
role: chance.pickone(["editor", "viewer"]),
|
|
723
|
+
invitation_method: chance.pickone(["email", "sso", "slack"]),
|
|
724
|
+
});
|
|
725
|
+
}
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
// HOOK 2: CHURNED ACCOUNT SILENCING — ~20% of users (hash %5)
|
|
729
|
+
// have post-day-30 events removed. No flag.
|
|
717
730
|
if (userEvents && userEvents.length > 0) {
|
|
718
731
|
const firstEvent = userEvents[0];
|
|
719
732
|
const idHash = String(firstEvent.user_id || firstEvent.device_id).split("").reduce((acc, char) => acc + char.charCodeAt(0), 0);
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
if (isChurnedAccount) {
|
|
733
|
+
if ((idHash % 5) === 0) {
|
|
723
734
|
for (let i = userEvents.length - 1; i >= 0; i--) {
|
|
724
|
-
const
|
|
725
|
-
const dayInDataset = dayjs(evt.time).diff(DATASET_START, "days", true);
|
|
735
|
+
const dayInDataset = dayjs(userEvents[i].time).diff(datasetStart, "days", true);
|
|
726
736
|
if (dayInDataset > 30) {
|
|
727
737
|
userEvents.splice(i, 1);
|
|
728
738
|
}
|
|
729
739
|
}
|
|
730
740
|
}
|
|
731
741
|
}
|
|
732
|
-
}
|
|
733
|
-
|
|
734
|
-
// ─────────────────────────────────────────────────────────────
|
|
735
|
-
// Hook #4: INTEGRATION USERS SUCCEED (everything)
|
|
736
|
-
// Users with both Slack AND PagerDuty integrations resolve faster
|
|
737
|
-
// ─────────────────────────────────────────────────────────────
|
|
738
|
-
if (type === "everything") {
|
|
739
|
-
const userEvents = record;
|
|
740
742
|
|
|
741
|
-
//
|
|
743
|
+
// HOOK 4: INTEGRATION USERS SUCCEED — Slack+PagerDuty users get
|
|
744
|
+
// alert response_time_mins 0.4x and resolution_time_mins 0.5x.
|
|
745
|
+
// Mutates raw props. No flag.
|
|
742
746
|
let hasSlack = false;
|
|
743
747
|
let hasPagerduty = false;
|
|
744
|
-
|
|
745
748
|
userEvents.forEach((event) => {
|
|
746
749
|
if (event.event === "integration configured") {
|
|
747
|
-
|
|
748
|
-
if (
|
|
749
|
-
if (integrationType === "pagerduty") hasPagerduty = true;
|
|
750
|
+
if (event.integration_type === "slack") hasSlack = true;
|
|
751
|
+
if (event.integration_type === "pagerduty") hasPagerduty = true;
|
|
750
752
|
}
|
|
751
753
|
});
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
// Second pass: set integrated_team on all alert events, then boost for integrated users
|
|
756
|
-
userEvents.forEach((event) => {
|
|
757
|
-
if (event.event === "alert acknowledged") {
|
|
758
|
-
if (hasFullIntegration && event.response_time_mins) {
|
|
754
|
+
if (hasSlack && hasPagerduty) {
|
|
755
|
+
userEvents.forEach((event) => {
|
|
756
|
+
if (event.event === "alert acknowledged" && event.response_time_mins) {
|
|
759
757
|
event.response_time_mins = Math.floor(event.response_time_mins * 0.4);
|
|
760
|
-
event.integrated_team = true;
|
|
761
|
-
} else {
|
|
762
|
-
event.integrated_team = false;
|
|
763
758
|
}
|
|
764
|
-
|
|
765
|
-
if (event.event === "alert resolved") {
|
|
766
|
-
if (hasFullIntegration && event.resolution_time_mins) {
|
|
759
|
+
if (event.event === "alert resolved" && event.resolution_time_mins) {
|
|
767
760
|
event.resolution_time_mins = Math.floor(event.resolution_time_mins * 0.5);
|
|
768
|
-
event.integrated_team = true;
|
|
769
|
-
} else {
|
|
770
|
-
event.integrated_team = false;
|
|
771
761
|
}
|
|
772
|
-
}
|
|
773
|
-
}
|
|
774
|
-
}
|
|
762
|
+
});
|
|
763
|
+
}
|
|
775
764
|
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
const
|
|
765
|
+
// HOOK 5 + HOOK 10: DOCS MAGIC NUMBER (no flags)
|
|
766
|
+
// Sweet 4-7 documentation-viewed events → +40% extra cloned
|
|
767
|
+
// service-deployed events. Over 8+ → drop 25% of service-deployed
|
|
768
|
+
// events. No flag.
|
|
769
|
+
const docsCount = userEvents.filter(e => e.event === "documentation viewed").length;
|
|
770
|
+
const deployTemplate = userEvents.find(e => e.event === "service deployed");
|
|
771
|
+
if (docsCount >= 4 && docsCount <= 7 && deployTemplate) {
|
|
772
|
+
const lastEvent = userEvents[userEvents.length - 1];
|
|
773
|
+
const extraDeploys = chance.integer({ min: 2, max: 3 });
|
|
774
|
+
for (let i = 0; i < extraDeploys; i++) {
|
|
775
|
+
userEvents.push({
|
|
776
|
+
...deployTemplate,
|
|
777
|
+
time: dayjs(lastEvent.time).add(chance.integer({ min: 1, max: 48 }), "hours").toISOString(),
|
|
778
|
+
user_id: lastEvent.user_id,
|
|
779
|
+
service_id: chance.pickone(serviceIds),
|
|
780
|
+
service_type: chance.pickone(["web_app", "api", "database", "cache", "queue", "ml_model"]),
|
|
781
|
+
environment: "production",
|
|
782
|
+
cloud_provider: profile.cloud_provider,
|
|
783
|
+
});
|
|
784
|
+
}
|
|
785
|
+
} else if (docsCount >= 8) {
|
|
786
|
+
for (let i = userEvents.length - 1; i >= 0; i--) {
|
|
787
|
+
if (userEvents[i].event === "service deployed" && chance.bool({ likelihood: 25 })) {
|
|
788
|
+
userEvents.splice(i, 1);
|
|
789
|
+
}
|
|
790
|
+
}
|
|
791
|
+
}
|
|
782
792
|
|
|
783
|
-
//
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
793
|
+
// HOOK 7: FAILED DEPLOYMENT RECOVERY — find failed→success pairs
|
|
794
|
+
// in this user's pipeline events, multiply duration_sec by 1.5 on
|
|
795
|
+
// the recovery deploy. Full control via everything hook.
|
|
796
|
+
const pipelineEvents = userEvents
|
|
797
|
+
.filter(e => e.event === "deployment pipeline run")
|
|
798
|
+
.sort((a, b) => a.time.localeCompare(b.time));
|
|
799
|
+
for (let i = 1; i < pipelineEvents.length; i++) {
|
|
800
|
+
if (pipelineEvents[i - 1].status === "failed" && pipelineEvents[i].status === "success") {
|
|
801
|
+
pipelineEvents[i].duration_sec = Math.floor((pipelineEvents[i].duration_sec || 300) * 1.5);
|
|
788
802
|
}
|
|
789
|
-
}
|
|
803
|
+
}
|
|
790
804
|
|
|
791
|
-
//
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
805
|
+
// HOOK 9: INCIDENT RESPONSE TTC — enterprise resolves faster,
|
|
806
|
+
// startup resolves slower. Scale the time gap between alert
|
|
807
|
+
// triggered → acknowledged → resolved for this user.
|
|
808
|
+
const segment = profile?.company_size;
|
|
809
|
+
const ttcFactor = (
|
|
810
|
+
segment === "enterprise" ? 0.5 :
|
|
811
|
+
segment === "startup" ? 1.8 :
|
|
812
|
+
1.0
|
|
813
|
+
);
|
|
814
|
+
if (ttcFactor !== 1.0) {
|
|
815
|
+
const alertEvents = userEvents
|
|
816
|
+
.filter(e => e.event === "alert triggered" || e.event === "alert acknowledged" || e.event === "alert resolved")
|
|
817
|
+
.sort((a, b) => a.time.localeCompare(b.time));
|
|
818
|
+
for (let i = 1; i < alertEvents.length; i++) {
|
|
819
|
+
const prev = dayjs(alertEvents[i - 1].time);
|
|
820
|
+
const curr = dayjs(alertEvents[i].time);
|
|
821
|
+
const gap = curr.diff(prev);
|
|
822
|
+
const newGap = Math.round(gap * ttcFactor);
|
|
823
|
+
alertEvents[i].time = prev.add(newGap, "milliseconds").toISOString();
|
|
810
824
|
}
|
|
811
825
|
}
|
|
812
826
|
}
|
|
813
827
|
|
|
814
|
-
//
|
|
815
|
-
//
|
|
816
|
-
// Company size determines seat count, ACV, and health score
|
|
817
|
-
// ─────────────────────────────────────────────────────────────
|
|
828
|
+
// HOOK 8: ENTERPRISE VS STARTUP (user) — company size determines
|
|
829
|
+
// seat count, ACV, and CSM. Real profile attrs.
|
|
818
830
|
if (type === "user") {
|
|
819
|
-
// Hook #2 support: tag churned accounts on user profile for discoverability
|
|
820
|
-
const idHash = String(record.distinct_id || "").split("").reduce((acc, char) => acc + char.charCodeAt(0), 0);
|
|
821
|
-
record.churned_account = (idHash % 5) === 0;
|
|
822
|
-
|
|
823
831
|
const companySize = record.company_size;
|
|
824
|
-
|
|
825
832
|
if (companySize === "enterprise") {
|
|
826
833
|
record.seat_count = chance.integer({ min: 50, max: 500 });
|
|
827
834
|
record.annual_contract_value = chance.integer({ min: 50000, max: 500000 });
|
|
@@ -839,7 +846,6 @@ const config = {
|
|
|
839
846
|
record.annual_contract_value = chance.integer({ min: 0, max: 3600 });
|
|
840
847
|
record.customer_success_manager = false;
|
|
841
848
|
}
|
|
842
|
-
|
|
843
849
|
record.customer_health_score = chance.integer({ min: 1, max: 100 });
|
|
844
850
|
}
|
|
845
851
|
|