@ak--47/dungeon-master 1.2.1 → 1.2.3
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/lib/core/storage.js +14 -4
- package/lib/orchestrators/mixpanel-sender.js +64 -3
- package/package.json +2 -2
- package/dungeons/user/shalini.js +0 -847
package/lib/core/storage.js
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
/** @typedef {import('../../types.js').Storage} Storage */
|
|
9
9
|
/** @typedef {import('../../types.js').hookArrayOptions<any>} hookArrayOptions */
|
|
10
10
|
|
|
11
|
-
import { existsSync } from "fs";
|
|
11
|
+
import { existsSync, mkdirSync } from "fs";
|
|
12
12
|
import pLimit from 'p-limit';
|
|
13
13
|
import os from "os";
|
|
14
14
|
import path from "path";
|
|
@@ -60,8 +60,13 @@ export async function createHookArray(arr = [], opts) {
|
|
|
60
60
|
writeDir = path.resolve(os.tmpdir());
|
|
61
61
|
}
|
|
62
62
|
|
|
63
|
-
if (typeof config.writeToDisk === "string"
|
|
64
|
-
|
|
63
|
+
if (typeof config.writeToDisk === "string") {
|
|
64
|
+
if (config.writeToDisk.startsWith('gs://')) {
|
|
65
|
+
writeDir = config.writeToDisk;
|
|
66
|
+
} else {
|
|
67
|
+
writeDir = path.resolve(config.writeToDisk);
|
|
68
|
+
if (!existsSync(writeDir)) mkdirSync(writeDir, { recursive: true });
|
|
69
|
+
}
|
|
65
70
|
}
|
|
66
71
|
|
|
67
72
|
function getWritePath() {
|
|
@@ -213,7 +218,7 @@ export async function createHookArray(arr = [], opts) {
|
|
|
213
218
|
while (isWriting) {
|
|
214
219
|
await new Promise(resolve => setTimeout(resolve, 10));
|
|
215
220
|
}
|
|
216
|
-
|
|
221
|
+
|
|
217
222
|
isWriting = true;
|
|
218
223
|
try {
|
|
219
224
|
batch++;
|
|
@@ -221,6 +226,11 @@ export async function createHookArray(arr = [], opts) {
|
|
|
221
226
|
const dataToWrite = [...arr];
|
|
222
227
|
arr.length = 0; // Clear array after copying data
|
|
223
228
|
await FILE_CONN(() => writeToDisk(dataToWrite, { writePath }));
|
|
229
|
+
// Data now lives on disk, not in arr — mirror what transformThenPush
|
|
230
|
+
// does when crossing BATCH_SIZE so the Mixpanel sender knows to read
|
|
231
|
+
// from disk instead of from the (now empty) in-memory array.
|
|
232
|
+
isBatchMode = true;
|
|
233
|
+
runtime.isBatchMode = true;
|
|
224
234
|
} finally {
|
|
225
235
|
isWriting = false;
|
|
226
236
|
}
|
|
@@ -48,9 +48,10 @@ export async function sendToMixpanel(context) {
|
|
|
48
48
|
const commonOpts = {
|
|
49
49
|
region,
|
|
50
50
|
fixData: true,
|
|
51
|
+
v2_compat: true,
|
|
51
52
|
verbose: false,
|
|
52
53
|
forceStream: true,
|
|
53
|
-
strict:
|
|
54
|
+
strict: false,
|
|
54
55
|
epochEnd: dayjs().unix(),
|
|
55
56
|
dryRun: false,
|
|
56
57
|
abridged: false,
|
|
@@ -159,7 +160,6 @@ export async function sendToMixpanel(context) {
|
|
|
159
160
|
const imported = await mp(creds, groupEventDataToImport, {
|
|
160
161
|
recordType: "event",
|
|
161
162
|
...commonOpts,
|
|
162
|
-
strict: false
|
|
163
163
|
});
|
|
164
164
|
log(` -> ${comma(imported.success)} group events sent\n`);
|
|
165
165
|
importResults.groupEvents = imported;
|
|
@@ -198,7 +198,6 @@ export async function sendToMixpanel(context) {
|
|
|
198
198
|
scdKey,
|
|
199
199
|
scdType,
|
|
200
200
|
scdLabel: `${scdKey}`,
|
|
201
|
-
fixData: true,
|
|
202
201
|
...commonOpts,
|
|
203
202
|
};
|
|
204
203
|
|
|
@@ -227,6 +226,9 @@ export async function sendToMixpanel(context) {
|
|
|
227
226
|
}
|
|
228
227
|
}
|
|
229
228
|
|
|
229
|
+
importResults.problems = collectProblems(importResults);
|
|
230
|
+
logProblems(importResults.problems);
|
|
231
|
+
|
|
230
232
|
log(`${'─'.repeat(50)}\n`);
|
|
231
233
|
|
|
232
234
|
// Clean up batch files if needed
|
|
@@ -263,4 +265,63 @@ export async function sendToMixpanel(context) {
|
|
|
263
265
|
let _verbose = true;
|
|
264
266
|
function log(message) {
|
|
265
267
|
if (_verbose) console.log(message);
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
/**
|
|
271
|
+
* Walk importResults and collect any failure/error indicators per import.
|
|
272
|
+
* Returns an array of problem objects (empty when all imports succeeded).
|
|
273
|
+
* @param {Object} importResults
|
|
274
|
+
* @returns {Array<{label: string, failed: number, unparsable: number, serverErrors: number, clientErrors: number, errorCount: number, error?: string, errors?: any[]}>}
|
|
275
|
+
*/
|
|
276
|
+
function collectProblems(importResults) {
|
|
277
|
+
const entries = [];
|
|
278
|
+
for (const [key, value] of Object.entries(importResults)) {
|
|
279
|
+
if (!value || key === 'problems') continue;
|
|
280
|
+
if (Array.isArray(value)) {
|
|
281
|
+
value.forEach((v, i) => entries.push([`${key}[${i}]`, v]));
|
|
282
|
+
} else {
|
|
283
|
+
entries.push([key, value]);
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
const problems = [];
|
|
288
|
+
for (const [label, result] of entries) {
|
|
289
|
+
if (!result || typeof result !== 'object') continue;
|
|
290
|
+
const failed = result.failed || 0;
|
|
291
|
+
const unparsable = result.unparsable || 0;
|
|
292
|
+
const serverErrors = result.serverErrors || 0;
|
|
293
|
+
const clientErrors = result.clientErrors || 0;
|
|
294
|
+
const errorCount = Array.isArray(result.errors) ? result.errors.length : 0;
|
|
295
|
+
const error = result.error;
|
|
296
|
+
if (failed || unparsable || serverErrors || clientErrors || errorCount || error) {
|
|
297
|
+
problems.push({ label, failed, unparsable, serverErrors, clientErrors, errorCount, error, errors: result.errors });
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
return problems;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
/**
|
|
304
|
+
* Print the compact problem report. Silent when there are no problems.
|
|
305
|
+
* @param {ReturnType<typeof collectProblems>} problems
|
|
306
|
+
*/
|
|
307
|
+
function logProblems(problems) {
|
|
308
|
+
if (!problems || problems.length === 0) return;
|
|
309
|
+
|
|
310
|
+
log(` Problems`);
|
|
311
|
+
for (const p of problems) {
|
|
312
|
+
const parts = [];
|
|
313
|
+
if (p.failed) parts.push(`${comma(p.failed)} failed`);
|
|
314
|
+
if (p.unparsable) parts.push(`${comma(p.unparsable)} unparsable`);
|
|
315
|
+
if (p.serverErrors) parts.push(`${comma(p.serverErrors)} 5xx (retried)`);
|
|
316
|
+
if (p.clientErrors) parts.push(`${comma(p.clientErrors)} client errors (retried)`);
|
|
317
|
+
if (p.errorCount) parts.push(`${comma(p.errorCount)} error records`);
|
|
318
|
+
if (p.error) parts.push(`error: ${p.error}`);
|
|
319
|
+
log(` -> ${p.label}: ${parts.join(', ')}`);
|
|
320
|
+
if (Array.isArray(p.errors) && p.errors.length > 0) {
|
|
321
|
+
const sample = p.errors[0];
|
|
322
|
+
const summary = typeof sample === 'string' ? sample : JSON.stringify(sample).slice(0, 200);
|
|
323
|
+
log(` sample: ${summary}`);
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
log('');
|
|
266
327
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ak--47/dungeon-master",
|
|
3
|
-
"version": "1.2.
|
|
3
|
+
"version": "1.2.3",
|
|
4
4
|
"description": "generate fancy datasets",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "index.js",
|
|
@@ -66,7 +66,7 @@
|
|
|
66
66
|
"dotenv": "^16.4.5",
|
|
67
67
|
"hyparquet-writer": "^0.6.1",
|
|
68
68
|
"mixpanel": "^0.18.0",
|
|
69
|
-
"mixpanel-import": "^3.2.
|
|
69
|
+
"mixpanel-import": "^3.2.9",
|
|
70
70
|
"p-limit": "^3.1.0",
|
|
71
71
|
"pino": "^9.0.0",
|
|
72
72
|
"pino-pretty": "^11.0.0",
|
package/dungeons/user/shalini.js
DELETED
|
@@ -1,847 +0,0 @@
|
|
|
1
|
-
// ── TWEAK THESE ──
|
|
2
|
-
const SEED = "dm4-dungeon";
|
|
3
|
-
const num_days = 90;
|
|
4
|
-
const num_users = 2_000;
|
|
5
|
-
const avg_events_per_user = 120;
|
|
6
|
-
let token = "your-mixpanel-token";
|
|
7
|
-
|
|
8
|
-
// ── env overrides ──
|
|
9
|
-
if (process.env.MP_TOKEN) token = process.env.MP_TOKEN;
|
|
10
|
-
|
|
11
|
-
import dayjs from "dayjs";
|
|
12
|
-
import utc from "dayjs/plugin/utc.js";
|
|
13
|
-
import * as u from "@ak--47/dungeon-master/utils";
|
|
14
|
-
import * as v from "ak-tools";
|
|
15
|
-
|
|
16
|
-
dayjs.extend(utc);
|
|
17
|
-
const chance = u.initChance(SEED);
|
|
18
|
-
|
|
19
|
-
/** @typedef {import("@ak--47/dungeon-master").Dungeon} Config */
|
|
20
|
-
|
|
21
|
-
const lastSearchQueryType = new Map();
|
|
22
|
-
const workspaceAiUserCount = new Map();
|
|
23
|
-
const workspaceTotalUsers = new Map();
|
|
24
|
-
|
|
25
|
-
/**
|
|
26
|
-
* ═══════════════════════════════════════════════════════════════════════════
|
|
27
|
-
* DATASET OVERVIEW — SLACK AI PRODUCTIVITY & COLLABORATION PLATFORM
|
|
28
|
-
* ═══════════════════════════════════════════════════════════════════════════
|
|
29
|
-
*
|
|
30
|
-
* Slack AI is a modern team collaboration platform with AI-powered features:
|
|
31
|
-
* channel/thread summarization, intelligent natural-language search, and
|
|
32
|
-
* AI-assisted writing. This dungeon is designed for a CPO-level demo
|
|
33
|
-
* (Shalini Agarwal) focused on behavioral correlations between AI feature
|
|
34
|
-
* adoption and core platform metrics.
|
|
35
|
-
*
|
|
36
|
-
* Scale: 2,000 users · 500 workspaces · 90 days · 19 event types
|
|
37
|
-
* Groups: 500 workspace_id groups associated with messaging + workspace events
|
|
38
|
-
*
|
|
39
|
-
* Key Segments:
|
|
40
|
-
* - Power User: high session count, AI feature heavy, message volume leader
|
|
41
|
-
* - Traditionalist: message-first, low AI adoption, consistent but not viral
|
|
42
|
-
* - Lurker: reads and summarizes, rarely sends; high AI summary usage
|
|
43
|
-
*
|
|
44
|
-
* Subscription tiers: Pro / Business+ / Enterprise Grid
|
|
45
|
-
*
|
|
46
|
-
* Core loop:
|
|
47
|
-
* workspace_created → member_invited → session_start →
|
|
48
|
-
* message_sent / message_read / thread_replied →
|
|
49
|
-
* ai_prompt_viewed → ai_summary_generated / ai_search_performed →
|
|
50
|
-
* huddle_started → canvas_edited → reaction_added
|
|
51
|
-
*
|
|
52
|
-
* Monetization signals: integration_added, subscription tier (superProp),
|
|
53
|
-
* canvas adoption, AI feature depth (tokens_saved).
|
|
54
|
-
*/
|
|
55
|
-
|
|
56
|
-
/**
|
|
57
|
-
* ═══════════════════════════════════════════════════════════════════════════
|
|
58
|
-
* ANALYTICS HOOKS (7 architected patterns)
|
|
59
|
-
* ═══════════════════════════════════════════════════════════════════════════
|
|
60
|
-
*
|
|
61
|
-
* ───────────────────────────────────────────────────────────────────────────
|
|
62
|
-
* 1. THE AI MULTIPLIER (everything)
|
|
63
|
-
* ───────────────────────────────────────────────────────────────────────────
|
|
64
|
-
*
|
|
65
|
-
* PATTERN: Users who generate 5+ AI summaries in any 7-day rolling window
|
|
66
|
-
* become "AI-activated." For each such window, clone message_sent and
|
|
67
|
-
* thread_replied events into the FOLLOWING 7 days (3x volume boost with
|
|
68
|
-
* randomized time offsets so it looks organic). Events tagged ai_multiplier.
|
|
69
|
-
*
|
|
70
|
-
* HOW TO FIND IT IN MIXPANEL:
|
|
71
|
-
*
|
|
72
|
-
* Report 1: AI Multiplier — Message Volume
|
|
73
|
-
* • Report type: Insights
|
|
74
|
-
* • Event: "message_sent"
|
|
75
|
-
* • Measure: Total (per user)
|
|
76
|
-
* • Breakdown: "ai_multiplier"
|
|
77
|
-
* • Expected: ai_multiplier=true users show ~3x higher message volume
|
|
78
|
-
* than ai_multiplier=false users
|
|
79
|
-
*
|
|
80
|
-
* Report 2: AI Multiplier — Thread Engagement
|
|
81
|
-
* • Report type: Insights
|
|
82
|
-
* • Event: "thread_replied"
|
|
83
|
-
* • Measure: Total (per user)
|
|
84
|
-
* • Breakdown: "ai_multiplier"
|
|
85
|
-
* • Expected: ~3x higher thread reply count for AI-activated users
|
|
86
|
-
*
|
|
87
|
-
* REAL-WORLD ANALOGUE: Teams that discover AI summarization unlock faster
|
|
88
|
-
* context-switching and communicate more frequently as a result.
|
|
89
|
-
*
|
|
90
|
-
* ───────────────────────────────────────────────────────────────────────────
|
|
91
|
-
* 2. SEARCH EFFICIENCY (event + closure Map)
|
|
92
|
-
* ───────────────────────────────────────────────────────────────────────────
|
|
93
|
-
*
|
|
94
|
-
* PATTERN: Natural-language AI searches lead to 40% faster result clicks
|
|
95
|
-
* vs keyword searches. The event hook tracks the last ai_search_performed
|
|
96
|
-
* query_type per user in a module-level Map, then reads it on the next
|
|
97
|
-
* search_result_clicked to multiply time_to_click_ms by 0.6 for
|
|
98
|
-
* natural_language queries.
|
|
99
|
-
*
|
|
100
|
-
* HOW TO FIND IT IN MIXPANEL:
|
|
101
|
-
*
|
|
102
|
-
* Report 1: Search Click Speed by Query Type
|
|
103
|
-
* • Report type: Insights
|
|
104
|
-
* • Event: "search_result_clicked"
|
|
105
|
-
* • Measure: Average of "time_to_click_ms"
|
|
106
|
-
* • Breakdown: "query_type"
|
|
107
|
-
* • Expected: "natural_language" ≈ 3,000ms vs "keyword" ≈ 5,000ms (~40% faster)
|
|
108
|
-
*
|
|
109
|
-
* Report 2: AI Search Adoption Funnel
|
|
110
|
-
* • Report type: Funnels
|
|
111
|
-
* • Steps: "ai_search_performed" → "search_result_clicked"
|
|
112
|
-
* • Breakdown: "is_ai_powered"
|
|
113
|
-
* • Expected: is_ai_powered=true converts at ~80%, false at ~60%
|
|
114
|
-
*
|
|
115
|
-
* REAL-WORLD ANALOGUE: Natural language search reduces cognitive load —
|
|
116
|
-
* users find what they need faster because they can express intent in plain
|
|
117
|
-
* English rather than crafting keyword strings.
|
|
118
|
-
*
|
|
119
|
-
* ───────────────────────────────────────────────────────────────────────────
|
|
120
|
-
* 3. WORKSPACE STICKINESS (everything)
|
|
121
|
-
* ───────────────────────────────────────────────────────────────────────────
|
|
122
|
-
*
|
|
123
|
-
* PATTERN: Workspaces where fewer than 20% of members use AI features
|
|
124
|
-
* (ai_summary_generated or ai_search_performed) are "low-adoption." For
|
|
125
|
-
* users in low-adoption workspaces, 30% of events after day 30 are removed,
|
|
126
|
-
* simulating the retention gap between high- and low-AI workspaces.
|
|
127
|
-
*
|
|
128
|
-
* HOW TO FIND IT IN MIXPANEL:
|
|
129
|
-
*
|
|
130
|
-
* Report 1: Retention by Workspace AI Adoption
|
|
131
|
-
* • Report type: Retention
|
|
132
|
-
* • Event: Any Event (first event = session_start)
|
|
133
|
-
* • Breakdown: Group Property "workspace_id" → "ai_adoption_tier"
|
|
134
|
-
* • Expected: High-adoption workspaces show ~95% MoM retention;
|
|
135
|
-
* low-adoption workspaces show ~70% (≈25pp gap)
|
|
136
|
-
*
|
|
137
|
-
* Report 2: Event Volume Over Time
|
|
138
|
-
* • Report type: Insights (line chart)
|
|
139
|
-
* • Event: Any Event
|
|
140
|
-
* • Measure: Total
|
|
141
|
-
* • Breakdown: "workspace_id"
|
|
142
|
-
* • Filter: events after day 30
|
|
143
|
-
* • Expected: Low-adoption workspaces show visible event drop-off
|
|
144
|
-
*
|
|
145
|
-
* REAL-WORLD ANALOGUE: AI adoption is a network effect — one power user
|
|
146
|
-
* sharing summaries pulls colleagues in. Below a critical mass threshold,
|
|
147
|
-
* the workspace stagnates and churns.
|
|
148
|
-
*
|
|
149
|
-
* ───────────────────────────────────────────────────────────────────────────
|
|
150
|
-
* 4. HUDDLE STICKINESS (everything)
|
|
151
|
-
* ───────────────────────────────────────────────────────────────────────────
|
|
152
|
-
*
|
|
153
|
-
* PATTERN: Users who start a huddle with ai_notes_enabled=true are 2x more
|
|
154
|
-
* likely to edit a Canvas within 1 hour afterward. If no canvas_edited
|
|
155
|
-
* event exists within 60 minutes of the huddle, one is cloned from the
|
|
156
|
-
* user's history and injected 20 minutes after the huddle_started event.
|
|
157
|
-
*
|
|
158
|
-
* HOW TO FIND IT IN MIXPANEL:
|
|
159
|
-
*
|
|
160
|
-
* Report 1: Huddle → Canvas Funnel by AI Notes
|
|
161
|
-
* • Report type: Funnels
|
|
162
|
-
* • Steps: "huddle_started" → "canvas_edited"
|
|
163
|
-
* • Conversion window: 1 hour
|
|
164
|
-
* • Breakdown: "ai_notes_enabled"
|
|
165
|
-
* • Expected: ai_notes_enabled=true converts at ~40% vs ~20% for false
|
|
166
|
-
*
|
|
167
|
-
* Report 2: Canvas Edits After Huddles
|
|
168
|
-
* • Report type: Insights
|
|
169
|
-
* • Event: "canvas_edited"
|
|
170
|
-
* • Measure: Total (per user)
|
|
171
|
-
* • Filter: Preceded by "huddle_started" within 1 hour
|
|
172
|
-
* • Breakdown: "ai_notes_enabled"
|
|
173
|
-
* • Expected: 2x more canvas edits for AI-notes users
|
|
174
|
-
*
|
|
175
|
-
* REAL-WORLD ANALOGUE: AI meeting notes lower the activation energy for
|
|
176
|
-
* documenting outcomes — users don't have to reconstruct what was said,
|
|
177
|
-
* so they're more likely to immediately capture action items in a Canvas.
|
|
178
|
-
*
|
|
179
|
-
* ───────────────────────────────────────────────────────────────────────────
|
|
180
|
-
* 5. LURKER SUMMARIZATION PATTERN (everything)
|
|
181
|
-
* ───────────────────────────────────────────────────────────────────────────
|
|
182
|
-
*
|
|
183
|
-
* PATTERN: Users with persona="Lurker" have 80% of their message_sent
|
|
184
|
-
* events removed (they read, don't write) but get their ai_summary_generated
|
|
185
|
-
* events quadrupled (they consume AI summaries voraciously). This creates a
|
|
186
|
-
* clear behavioral segment: high summaries, low messages.
|
|
187
|
-
*
|
|
188
|
-
* HOW TO FIND IT IN MIXPANEL:
|
|
189
|
-
*
|
|
190
|
-
* Report 1: AI Summary Usage by Persona
|
|
191
|
-
* • Report type: Insights
|
|
192
|
-
* • Event: "ai_summary_generated"
|
|
193
|
-
* • Measure: Total (per user)
|
|
194
|
-
* • Breakdown: User Property "persona"
|
|
195
|
-
* • Expected: "Lurker" shows ~4x higher summaries than "Power User"
|
|
196
|
-
* or "Traditionalist"
|
|
197
|
-
*
|
|
198
|
-
* Report 2: Message Volume by Persona
|
|
199
|
-
* • Report type: Insights
|
|
200
|
-
* • Event: "message_sent"
|
|
201
|
-
* • Measure: Total (per user)
|
|
202
|
-
* • Breakdown: User Property "persona"
|
|
203
|
-
* • Expected: "Lurker" shows ~0.2x messages vs "Power User"
|
|
204
|
-
*
|
|
205
|
-
* Report 3: Persona Behavioral Matrix
|
|
206
|
-
* • Report type: Insights
|
|
207
|
-
* • Events: "message_sent" + "ai_summary_generated" side by side
|
|
208
|
-
* • Measure: Total (per user)
|
|
209
|
-
* • Breakdown: User Property "persona"
|
|
210
|
-
* • Expected: Inverse relationship — Lurkers summarize more, send less
|
|
211
|
-
*
|
|
212
|
-
* REAL-WORLD ANALOGUE: In every Slack workspace, there are silent readers
|
|
213
|
-
* who follow dozens of channels but rarely post. AI summaries are their
|
|
214
|
-
* killer feature — they can stay informed without the noise.
|
|
215
|
-
*
|
|
216
|
-
* ───────────────────────────────────────────────────────────────────────────
|
|
217
|
-
* 6. MONDAY PRODUCTIVITY SPIKE (event)
|
|
218
|
-
* ───────────────────────────────────────────────────────────────────────────
|
|
219
|
-
*
|
|
220
|
-
* PATTERN: AI summary generation is 50% higher on Mondays as users catch up
|
|
221
|
-
* from the weekend. When an ai_summary_generated event fires on a Monday,
|
|
222
|
-
* the tokens_saved value is boosted by 1.5x and the event is cloned once
|
|
223
|
-
* with a short time offset to double Monday volume.
|
|
224
|
-
*
|
|
225
|
-
* HOW TO FIND IT IN MIXPANEL:
|
|
226
|
-
*
|
|
227
|
-
* Report 1: AI Summaries by Day of Week
|
|
228
|
-
* • Report type: Insights (bar chart)
|
|
229
|
-
* • Event: "ai_summary_generated"
|
|
230
|
-
* • Measure: Total
|
|
231
|
-
* • Time: Break down by Day of Week
|
|
232
|
-
* • Expected: Monday bar is visibly ~1.5x taller than Tue–Fri average
|
|
233
|
-
*
|
|
234
|
-
* Report 2: Tokens Saved on Mondays
|
|
235
|
-
* • Report type: Insights
|
|
236
|
-
* • Event: "ai_summary_generated"
|
|
237
|
-
* • Measure: Average of "tokens_saved"
|
|
238
|
-
* • Breakdown: "monday_spike"
|
|
239
|
-
* • Expected: monday_spike=true shows ~1.5x higher avg tokens_saved
|
|
240
|
-
*
|
|
241
|
-
* REAL-WORLD ANALOGUE: Monday morning is "catch-up time" — users open Slack
|
|
242
|
-
* and immediately want to know what happened over the weekend. AI summaries
|
|
243
|
-
* are the fastest path from inbox-zero anxiety to being in the loop.
|
|
244
|
-
*
|
|
245
|
-
* ───────────────────────────────────────────────────────────────────────────
|
|
246
|
-
* 7. AI WRITING ASSISTANCE VALUE (event)
|
|
247
|
-
* ───────────────────────────────────────────────────────────────────────────
|
|
248
|
-
*
|
|
249
|
-
* PATTERN: Messages marked as is_ai_assisted=true have 2x higher
|
|
250
|
-
* content_length. In the event hook, when message_sent fires with
|
|
251
|
-
* is_ai_assisted=true, content_length is multiplied by 2.0.
|
|
252
|
-
*
|
|
253
|
-
* HOW TO FIND IT IN MIXPANEL:
|
|
254
|
-
*
|
|
255
|
-
* Report 1: Message Length by AI Assistance
|
|
256
|
-
* • Report type: Insights
|
|
257
|
-
* • Event: "message_sent"
|
|
258
|
-
* • Measure: Average of "content_length"
|
|
259
|
-
* • Breakdown: "is_ai_assisted"
|
|
260
|
-
* • Expected: is_ai_assisted=true ≈ 300 chars vs false ≈ 150 chars (2x)
|
|
261
|
-
*
|
|
262
|
-
* Report 2: AI Writing Adoption Over Time
|
|
263
|
-
* • Report type: Insights (line chart)
|
|
264
|
-
* • Event: "message_sent"
|
|
265
|
-
* • Measure: Total
|
|
266
|
-
* • Filter: "is_ai_assisted" = true
|
|
267
|
-
* • Time: Weekly
|
|
268
|
-
* • Expected: Growing trend as users discover the feature (growth soup)
|
|
269
|
-
*
|
|
270
|
-
* REAL-WORLD ANALOGUE: AI writing assistance helps users write more complete,
|
|
271
|
-
* thoughtful messages — reducing back-and-forth and improving async
|
|
272
|
-
* communication quality. Longer messages carry more context.
|
|
273
|
-
*
|
|
274
|
-
* ═══════════════════════════════════════════════════════════════════════════
|
|
275
|
-
* EXPECTED METRICS SUMMARY
|
|
276
|
-
* ═══════════════════════════════════════════════════════════════════════════
|
|
277
|
-
*
|
|
278
|
-
* Hook | Metric | Baseline | Effect | Ratio
|
|
279
|
-
* ──────────────────────|───────────────────────|───────────|───────────|──────
|
|
280
|
-
* AI Multiplier | message_sent/user/wk | ~40 | ~120 | 3x
|
|
281
|
-
* Search Efficiency | time_to_click_ms | ~5,000ms | ~3,000ms | 0.6x
|
|
282
|
-
* Workspace Stickiness | MoM retention | ~70% | ~95% | 1.35x
|
|
283
|
-
* Huddle Stickiness | huddle→canvas conv | ~20% | ~40% | 2x
|
|
284
|
-
* Lurker Summarization | summaries/user | ~12 | ~48 | 4x
|
|
285
|
-
* Monday Spike | summaries on Monday | baseline | +50% | 1.5x
|
|
286
|
-
* AI Writing Value | content_length (avg) | ~150 | ~300 | 2x
|
|
287
|
-
*/
|
|
288
|
-
|
|
289
|
-
/** @type {Config} */
|
|
290
|
-
const config = {
|
|
291
|
-
token,
|
|
292
|
-
seed: SEED,
|
|
293
|
-
numDays: num_days,
|
|
294
|
-
numEvents: num_users * avg_events_per_user,
|
|
295
|
-
numUsers: num_users,
|
|
296
|
-
hasAnonIds: false,
|
|
297
|
-
hasSessionIds: true,
|
|
298
|
-
format: "json",
|
|
299
|
-
gzip: true,
|
|
300
|
-
alsoInferFunnels: false,
|
|
301
|
-
hasLocation: true,
|
|
302
|
-
hasAndroidDevices: true,
|
|
303
|
-
hasIOSDevices: true,
|
|
304
|
-
hasDesktopDevices: true,
|
|
305
|
-
hasBrowser: false,
|
|
306
|
-
hasCampaigns: false,
|
|
307
|
-
isAnonymous: false,
|
|
308
|
-
hasAdSpend: false,
|
|
309
|
-
percentUsersBornInDataset: 35,
|
|
310
|
-
hasAvatar: true,
|
|
311
|
-
batchSize: 2_500_000,
|
|
312
|
-
concurrency: 1,
|
|
313
|
-
writeToDisk: false,
|
|
314
|
-
scdProps: {},
|
|
315
|
-
mirrorProps: {},
|
|
316
|
-
lookupTables: [],
|
|
317
|
-
|
|
318
|
-
soup: "growth",
|
|
319
|
-
|
|
320
|
-
groupKeys: [
|
|
321
|
-
["workspace_id", 500, [
|
|
322
|
-
"message_sent",
|
|
323
|
-
"message_read",
|
|
324
|
-
"thread_replied",
|
|
325
|
-
"ai_summary_generated",
|
|
326
|
-
"ai_search_performed",
|
|
327
|
-
"huddle_started",
|
|
328
|
-
"huddle_joined",
|
|
329
|
-
"canvas_edited",
|
|
330
|
-
"channel_created",
|
|
331
|
-
"member_invited",
|
|
332
|
-
"workspace_created",
|
|
333
|
-
]],
|
|
334
|
-
],
|
|
335
|
-
|
|
336
|
-
groupProps: {
|
|
337
|
-
workspace_id: {
|
|
338
|
-
"workspace_name": () => `${chance.pickone(["Acme", "Globex", "Initech", "Umbrella", "Hooli", "Pied Piper", "Dunder Mifflin", "Vandelay", "Bluth", "Sterling"])} ${chance.pickone(["Corp", "Inc", "Labs", "HQ", "Team", "Group", "Co", "Ltd"])}`,
|
|
339
|
-
"workspace_plan": ["Pro", "Business+", "Business+", "Enterprise Grid"],
|
|
340
|
-
"member_count": u.weighNumRange(5, 500, 0.5, 30),
|
|
341
|
-
"ai_adoption_tier": ["low", "low", "medium", "high"],
|
|
342
|
-
"industry": ["Technology", "Finance", "Healthcare", "Retail", "Media", "Education", "Consulting"],
|
|
343
|
-
}
|
|
344
|
-
},
|
|
345
|
-
|
|
346
|
-
funnels: [
|
|
347
|
-
{
|
|
348
|
-
name: "Workspace Growth",
|
|
349
|
-
sequence: ["member_invited", "session_start", "ai_summary_generated"],
|
|
350
|
-
isFirstFunnel: true,
|
|
351
|
-
conversionRate: 60,
|
|
352
|
-
timeToConvert: 2,
|
|
353
|
-
},
|
|
354
|
-
{
|
|
355
|
-
name: "AI Adoption",
|
|
356
|
-
sequence: ["message_sent", "ai_prompt_viewed", "ai_summary_generated"],
|
|
357
|
-
conversionRate: 25,
|
|
358
|
-
timeToConvert: 0.1,
|
|
359
|
-
weight: 10,
|
|
360
|
-
},
|
|
361
|
-
{
|
|
362
|
-
name: "Collaboration Loop",
|
|
363
|
-
sequence: ["huddle_joined", "canvas_edited"],
|
|
364
|
-
conversionRate: 40,
|
|
365
|
-
timeToConvert: 0.04,
|
|
366
|
-
},
|
|
367
|
-
{
|
|
368
|
-
name: "Intelligent Search",
|
|
369
|
-
sequence: ["ai_search_performed", "search_result_clicked"],
|
|
370
|
-
conversionRate: 80,
|
|
371
|
-
timeToConvert: 0.01,
|
|
372
|
-
},
|
|
373
|
-
{
|
|
374
|
-
name: "Onboarding",
|
|
375
|
-
sequence: ["workspace_created", "channel_created", "member_invited"],
|
|
376
|
-
conversionRate: 45,
|
|
377
|
-
timeToConvert: 1,
|
|
378
|
-
},
|
|
379
|
-
],
|
|
380
|
-
|
|
381
|
-
events: [
|
|
382
|
-
{
|
|
383
|
-
event: "workspace_created",
|
|
384
|
-
weight: 1,
|
|
385
|
-
isFirstEvent: true,
|
|
386
|
-
properties: {
|
|
387
|
-
"template_used": ["blank", "engineering", "sales", "marketing", "hr", "product"],
|
|
388
|
-
"import_from": ["none", "none", "none", "microsoft_teams", "google_chat"],
|
|
389
|
-
}
|
|
390
|
-
},
|
|
391
|
-
{
|
|
392
|
-
event: "member_invited",
|
|
393
|
-
weight: 3,
|
|
394
|
-
isFirstEvent: true,
|
|
395
|
-
properties: {
|
|
396
|
-
"invite_method": ["email", "link", "directory_sync"],
|
|
397
|
-
"invitee_role": ["member", "member", "member", "admin", "guest"],
|
|
398
|
-
}
|
|
399
|
-
},
|
|
400
|
-
{
|
|
401
|
-
event: "session_start",
|
|
402
|
-
weight: 20,
|
|
403
|
-
isSessionStartEvent: true,
|
|
404
|
-
properties: {
|
|
405
|
-
"session_duration_sec": u.weighNumRange(60, 7200, 0.5, 900),
|
|
406
|
-
"notifications_pending": u.weighNumRange(0, 50, 0.8, 5),
|
|
407
|
-
}
|
|
408
|
-
},
|
|
409
|
-
{
|
|
410
|
-
event: "message_sent",
|
|
411
|
-
weight: 18,
|
|
412
|
-
properties: {
|
|
413
|
-
"channel_type": ["public", "private", "dm"],
|
|
414
|
-
"content_length": u.weighNumRange(10, 500, 0.5, 80),
|
|
415
|
-
"is_ai_assisted": u.pickAWinner([true, false], 0.25),
|
|
416
|
-
"has_attachment": u.pickAWinner([true, false], 0.2),
|
|
417
|
-
"ai_multiplier": [false],
|
|
418
|
-
}
|
|
419
|
-
},
|
|
420
|
-
{
|
|
421
|
-
event: "message_read",
|
|
422
|
-
weight: 15,
|
|
423
|
-
properties: {
|
|
424
|
-
"source": ["mobile", "desktop"],
|
|
425
|
-
"unread_count": u.weighNumRange(1, 100, 0.8, 10),
|
|
426
|
-
"channel_type": ["public", "private", "dm"],
|
|
427
|
-
}
|
|
428
|
-
},
|
|
429
|
-
{
|
|
430
|
-
event: "thread_replied",
|
|
431
|
-
weight: 10,
|
|
432
|
-
properties: {
|
|
433
|
-
"is_ai_assisted": u.pickAWinner([true, false], 0.2),
|
|
434
|
-
"reply_length": u.weighNumRange(5, 300, 0.5, 60),
|
|
435
|
-
"thread_depth": u.weighNumRange(1, 20, 0.8, 3),
|
|
436
|
-
"ai_multiplier": [false],
|
|
437
|
-
}
|
|
438
|
-
},
|
|
439
|
-
{
|
|
440
|
-
event: "reaction_added",
|
|
441
|
-
weight: 12,
|
|
442
|
-
properties: {
|
|
443
|
-
"emoji": ["+1", "clap", "fire", "eyes", "check"],
|
|
444
|
-
"message_age_hours": u.weighNumRange(0, 72, 0.8, 2),
|
|
445
|
-
}
|
|
446
|
-
},
|
|
447
|
-
{
|
|
448
|
-
event: "ai_prompt_viewed",
|
|
449
|
-
weight: 8,
|
|
450
|
-
properties: {
|
|
451
|
-
"feature": ["summarize", "search", "compose"],
|
|
452
|
-
"entry_point": ["channel_header", "search_bar", "compose_box", "shortcut"],
|
|
453
|
-
"converted": u.pickAWinner([true, false], 0.6),
|
|
454
|
-
}
|
|
455
|
-
},
|
|
456
|
-
{
|
|
457
|
-
event: "ai_summary_generated",
|
|
458
|
-
weight: 6,
|
|
459
|
-
properties: {
|
|
460
|
-
"source_type": ["channel", "thread", "huddle"],
|
|
461
|
-
"tokens_saved": u.weighNumRange(50, 2000, 0.5, 400),
|
|
462
|
-
"messages_summarized": u.weighNumRange(5, 200, 0.8, 25),
|
|
463
|
-
"monday_spike": [false],
|
|
464
|
-
}
|
|
465
|
-
},
|
|
466
|
-
{
|
|
467
|
-
event: "ai_search_performed",
|
|
468
|
-
weight: 7,
|
|
469
|
-
properties: {
|
|
470
|
-
"query_type": ["natural_language", "keyword"],
|
|
471
|
-
"is_ai_powered": u.pickAWinner([true, false], 0.55),
|
|
472
|
-
"results_count": u.weighNumRange(0, 50, 0.5, 12),
|
|
473
|
-
"query_length_chars": u.weighNumRange(5, 200, 0.5, 40),
|
|
474
|
-
}
|
|
475
|
-
},
|
|
476
|
-
{
|
|
477
|
-
event: "ai_writing_assisted",
|
|
478
|
-
weight: 4,
|
|
479
|
-
properties: {
|
|
480
|
-
"tone": ["professional", "casual", "concise"],
|
|
481
|
-
"words_suggested": u.weighNumRange(5, 100, 0.5, 25),
|
|
482
|
-
"accepted": u.pickAWinner([true, false], 0.7),
|
|
483
|
-
}
|
|
484
|
-
},
|
|
485
|
-
{
|
|
486
|
-
event: "search_result_clicked",
|
|
487
|
-
weight: 6,
|
|
488
|
-
properties: {
|
|
489
|
-
"time_to_click_ms": u.weighNumRange(1000, 15000, 1, 5000),
|
|
490
|
-
"position": u.weighNumRange(1, 10, 0.8, 3),
|
|
491
|
-
"result_type": ["message", "file", "channel", "person"],
|
|
492
|
-
"query_type": ["natural_language", "keyword"],
|
|
493
|
-
}
|
|
494
|
-
},
|
|
495
|
-
{
|
|
496
|
-
event: "huddle_started",
|
|
497
|
-
weight: 4,
|
|
498
|
-
properties: {
|
|
499
|
-
"ai_notes_enabled": u.pickAWinner([true, false], 0.4),
|
|
500
|
-
"is_scheduled": u.pickAWinner([true, false], 0.35),
|
|
501
|
-
"participant_count": u.weighNumRange(2, 15, 0.8, 4),
|
|
502
|
-
"duration_min": u.weighNumRange(5, 90, 0.5, 20),
|
|
503
|
-
}
|
|
504
|
-
},
|
|
505
|
-
{
|
|
506
|
-
event: "huddle_joined",
|
|
507
|
-
weight: 8,
|
|
508
|
-
properties: {
|
|
509
|
-
"join_method": ["link", "notification", "scheduled"],
|
|
510
|
-
"device_type": ["desktop", "mobile", "tablet"],
|
|
511
|
-
}
|
|
512
|
-
},
|
|
513
|
-
{
|
|
514
|
-
event: "canvas_edited",
|
|
515
|
-
weight: 6,
|
|
516
|
-
properties: {
|
|
517
|
-
"edit_type": ["text", "table", "checklist", "image"],
|
|
518
|
-
"canvas_age_days": u.weighNumRange(0, 90, 0.8, 7),
|
|
519
|
-
"collaborators_count": u.weighNumRange(1, 10, 0.8, 2),
|
|
520
|
-
}
|
|
521
|
-
},
|
|
522
|
-
{
|
|
523
|
-
event: "file_shared",
|
|
524
|
-
weight: 5,
|
|
525
|
-
properties: {
|
|
526
|
-
"extension": ["pdf", "docx", "png", "csv"],
|
|
527
|
-
"file_size_kb": u.weighNumRange(10, 50000, 0.3, 500),
|
|
528
|
-
"channel_type": ["public", "private", "dm"],
|
|
529
|
-
}
|
|
530
|
-
},
|
|
531
|
-
{
|
|
532
|
-
event: "channel_created",
|
|
533
|
-
weight: 2,
|
|
534
|
-
properties: {
|
|
535
|
-
"is_private": u.pickAWinner([true, false], 0.4),
|
|
536
|
-
"purpose_set": u.pickAWinner([true, false], 0.6),
|
|
537
|
-
"channel_type": ["topic", "project", "team", "social"],
|
|
538
|
-
}
|
|
539
|
-
},
|
|
540
|
-
{
|
|
541
|
-
event: "integration_added",
|
|
542
|
-
weight: 2,
|
|
543
|
-
properties: {
|
|
544
|
-
"category": ["crm", "dev_tools", "hr", "productivity"],
|
|
545
|
-
"integration_name": ["Salesforce", "GitHub", "Jira", "Zoom", "Google Drive", "Notion", "PagerDuty", "Figma"],
|
|
546
|
-
"installed_by_role": ["admin", "admin", "member"],
|
|
547
|
-
}
|
|
548
|
-
},
|
|
549
|
-
{
|
|
550
|
-
event: "user_churned",
|
|
551
|
-
weight: 0.5,
|
|
552
|
-
isChurnEvent: true,
|
|
553
|
-
isStrictEvent: true,
|
|
554
|
-
returnLikelihood: 0.1,
|
|
555
|
-
properties: {
|
|
556
|
-
"churn_reason": ["inactivity", "workspace_deleted", "plan_downgrade", "competitor"],
|
|
557
|
-
"days_active": u.weighNumRange(1, 90, 0.5, 20),
|
|
558
|
-
}
|
|
559
|
-
},
|
|
560
|
-
],
|
|
561
|
-
|
|
562
|
-
superProps: {
|
|
563
|
-
workspace_id: () => `T-${v.uid(8).toUpperCase()}`,
|
|
564
|
-
account_tier: ["Pro", "Pro", "Business+", "Business+", "Enterprise Grid"],
|
|
565
|
-
platform: ["macOS", "Windows", "iOS", "Android", "Web"],
|
|
566
|
-
},
|
|
567
|
-
|
|
568
|
-
userProps: {
|
|
569
|
-
"persona": ["Power User", "Power User", "Traditionalist", "Traditionalist", "Traditionalist", "Lurker"],
|
|
570
|
-
"job_role": ["Engineering", "Product", "Sales", "Marketing", "HR"],
|
|
571
|
-
"ai_onboarding_complete": u.pickAWinner([true, false], 0.55),
|
|
572
|
-
"department_size": u.weighNumRange(3, 200, 0.5, 20),
|
|
573
|
-
"tenure_days": u.weighNumRange(1, 730, 0.3, 90),
|
|
574
|
-
},
|
|
575
|
-
|
|
576
|
-
hook: function (record, type, meta) {
|
|
577
|
-
const NOW = dayjs();
|
|
578
|
-
const DATASET_START = NOW.subtract(num_days, "days");
|
|
579
|
-
|
|
580
|
-
// ═══════════════════════════════════════════════════════════════════
|
|
581
|
-
// HOOKS 2, 6, 7: EVENT HOOK
|
|
582
|
-
// Per-event property modifications and closure-based state tracking.
|
|
583
|
-
// ═══════════════════════════════════════════════════════════════════
|
|
584
|
-
if (type === "event") {
|
|
585
|
-
const EVENT_TIME = dayjs(record.time);
|
|
586
|
-
const dayOfWeek = EVENT_TIME.day();
|
|
587
|
-
const userId = record.user_id;
|
|
588
|
-
|
|
589
|
-
// ── Hook 2: SEARCH EFFICIENCY — track query type via closure Map ──
|
|
590
|
-
// Store the query_type of ai_search_performed so the next
|
|
591
|
-
// search_result_clicked can read it and apply the speed multiplier.
|
|
592
|
-
if (record.event === "ai_search_performed") {
|
|
593
|
-
lastSearchQueryType.set(userId, record.query_type);
|
|
594
|
-
}
|
|
595
|
-
|
|
596
|
-
// ── Hook 2: SEARCH EFFICIENCY — apply 40% speed boost on click ──
|
|
597
|
-
// natural_language → time_to_click_ms * 0.6 (40% faster)
|
|
598
|
-
if (record.event === "search_result_clicked") {
|
|
599
|
-
const lastQueryType = lastSearchQueryType.get(userId);
|
|
600
|
-
if (lastQueryType) {
|
|
601
|
-
record.query_type = lastQueryType;
|
|
602
|
-
if (lastQueryType === "natural_language") {
|
|
603
|
-
record.time_to_click_ms = Math.round(record.time_to_click_ms * 0.6);
|
|
604
|
-
}
|
|
605
|
-
lastSearchQueryType.delete(userId);
|
|
606
|
-
}
|
|
607
|
-
}
|
|
608
|
-
|
|
609
|
-
// ── Hook 6: MONDAY PRODUCTIVITY SPIKE — boost tokens_saved 1.5x ──
|
|
610
|
-
// Tag monday_spike=true; the everything hook will clone these events
|
|
611
|
-
// to produce the observable 1.5x volume increase on Mondays.
|
|
612
|
-
if (record.event === "ai_summary_generated") {
|
|
613
|
-
if (dayOfWeek === 1) {
|
|
614
|
-
record.tokens_saved = Math.round(record.tokens_saved * 1.5);
|
|
615
|
-
record.monday_spike = true;
|
|
616
|
-
} else {
|
|
617
|
-
record.monday_spike = false;
|
|
618
|
-
}
|
|
619
|
-
}
|
|
620
|
-
|
|
621
|
-
// ── Hook 7: AI WRITING VALUE — double content_length for AI messages ──
|
|
622
|
-
// is_ai_assisted=true messages get 2x content_length (capped at 500).
|
|
623
|
-
if (record.event === "message_sent" && record.is_ai_assisted === true) {
|
|
624
|
-
record.content_length = Math.min(500, Math.round(record.content_length * 2.0));
|
|
625
|
-
}
|
|
626
|
-
|
|
627
|
-
return record;
|
|
628
|
-
}
|
|
629
|
-
|
|
630
|
-
// ═══════════════════════════════════════════════════════════════════
|
|
631
|
-
// HOOKS 1, 3, 4, 5, 6 (clone pass): EVERYTHING HOOK
|
|
632
|
-
// Full event stream per user. meta.profile available for cross-table
|
|
633
|
-
// correlation. Runs after all events for one user are generated.
|
|
634
|
-
// ═══════════════════════════════════════════════════════════════════
|
|
635
|
-
if (type === "everything") {
|
|
636
|
-
const events = record;
|
|
637
|
-
if (!events || events.length === 0) return record;
|
|
638
|
-
|
|
639
|
-
const profile = meta && meta.profile ? meta.profile : {};
|
|
640
|
-
const persona = profile.persona || "Traditionalist";
|
|
641
|
-
const firstEventTime = dayjs(events[0].time);
|
|
642
|
-
|
|
643
|
-
// ── First pass: collect behavioral signals ──────────────────
|
|
644
|
-
const aiSummaryTimestamps = [];
|
|
645
|
-
const huddlesWithAiNotes = [];
|
|
646
|
-
const workspaceId = events[0] ? events[0].workspace_id : null;
|
|
647
|
-
|
|
648
|
-
events.forEach((event, idx) => {
|
|
649
|
-
if (event.event === "ai_summary_generated") {
|
|
650
|
-
aiSummaryTimestamps.push({ time: dayjs(event.time), idx });
|
|
651
|
-
}
|
|
652
|
-
if (event.event === "huddle_started" && event.ai_notes_enabled === true) {
|
|
653
|
-
huddlesWithAiNotes.push({ time: dayjs(event.time), idx });
|
|
654
|
-
}
|
|
655
|
-
});
|
|
656
|
-
|
|
657
|
-
// ── Track workspace AI adoption ratio across all users ──────
|
|
658
|
-
// Used by Hook 3 to determine low-adoption workspaces.
|
|
659
|
-
if (workspaceId) {
|
|
660
|
-
if (!workspaceTotalUsers.has(workspaceId)) {
|
|
661
|
-
workspaceTotalUsers.set(workspaceId, 0);
|
|
662
|
-
workspaceAiUserCount.set(workspaceId, 0);
|
|
663
|
-
}
|
|
664
|
-
workspaceTotalUsers.set(workspaceId, workspaceTotalUsers.get(workspaceId) + 1);
|
|
665
|
-
if (aiSummaryTimestamps.length > 0) {
|
|
666
|
-
workspaceAiUserCount.set(workspaceId, workspaceAiUserCount.get(workspaceId) + 1);
|
|
667
|
-
}
|
|
668
|
-
}
|
|
669
|
-
|
|
670
|
-
// ═══════════════════════════════════════════════════════════
|
|
671
|
-
// HOOK 1: THE AI MULTIPLIER
|
|
672
|
-
// Find 7-day windows with 5+ summaries → clone message_sent
|
|
673
|
-
// and thread_replied into the next 7 days (3x volume boost).
|
|
674
|
-
// ═══════════════════════════════════════════════════════════
|
|
675
|
-
const templateMsg = events.find(e => e.event === "message_sent");
|
|
676
|
-
const templateThread = events.find(e => e.event === "thread_replied");
|
|
677
|
-
const qualifyingWindowStarts = new Set();
|
|
678
|
-
|
|
679
|
-
for (let i = 0; i < aiSummaryTimestamps.length; i++) {
|
|
680
|
-
const windowStart = aiSummaryTimestamps[i].time;
|
|
681
|
-
const windowEnd = windowStart.add(7, "days");
|
|
682
|
-
let countInWindow = 0;
|
|
683
|
-
for (let j = i; j < aiSummaryTimestamps.length; j++) {
|
|
684
|
-
if (aiSummaryTimestamps[j].time.isBefore(windowEnd)) {
|
|
685
|
-
countInWindow++;
|
|
686
|
-
} else {
|
|
687
|
-
break;
|
|
688
|
-
}
|
|
689
|
-
}
|
|
690
|
-
if (countInWindow >= 5) {
|
|
691
|
-
qualifyingWindowStarts.add(windowStart.valueOf());
|
|
692
|
-
}
|
|
693
|
-
}
|
|
694
|
-
|
|
695
|
-
if (qualifyingWindowStarts.size > 0) {
|
|
696
|
-
const injected = [];
|
|
697
|
-
qualifyingWindowStarts.forEach(windowStartMs => {
|
|
698
|
-
const boostStart = dayjs(windowStartMs).add(7, "days");
|
|
699
|
-
|
|
700
|
-
// Clone 6 message_sent events with organic jitter (≈3x boost)
|
|
701
|
-
if (templateMsg) {
|
|
702
|
-
for (let k = 0; k < 6; k++) {
|
|
703
|
-
injected.push({
|
|
704
|
-
...templateMsg,
|
|
705
|
-
time: boostStart.add(chance.integer({ min: 10, max: 9000 }), "minutes").toISOString(),
|
|
706
|
-
user_id: templateMsg.user_id,
|
|
707
|
-
content_length: Math.round(templateMsg.content_length * (0.8 + Math.random() * 0.5)),
|
|
708
|
-
is_ai_assisted: chance.bool({ likelihood: 40 }),
|
|
709
|
-
ai_multiplier: true,
|
|
710
|
-
});
|
|
711
|
-
}
|
|
712
|
-
}
|
|
713
|
-
|
|
714
|
-
// Clone 3 thread_replied events
|
|
715
|
-
if (templateThread) {
|
|
716
|
-
for (let k = 0; k < 3; k++) {
|
|
717
|
-
injected.push({
|
|
718
|
-
...templateThread,
|
|
719
|
-
time: boostStart.add(chance.integer({ min: 15, max: 9000 }), "minutes").toISOString(),
|
|
720
|
-
user_id: templateThread.user_id,
|
|
721
|
-
reply_length: Math.round(templateThread.reply_length * (0.8 + Math.random() * 0.5)),
|
|
722
|
-
ai_multiplier: true,
|
|
723
|
-
});
|
|
724
|
-
}
|
|
725
|
-
}
|
|
726
|
-
});
|
|
727
|
-
|
|
728
|
-
if (injected.length > 0) {
|
|
729
|
-
events.push(...injected);
|
|
730
|
-
}
|
|
731
|
-
}
|
|
732
|
-
|
|
733
|
-
// ═══════════════════════════════════════════════════════════
|
|
734
|
-
// HOOK 3: WORKSPACE STICKINESS
|
|
735
|
-
// Low-adoption workspaces (<20% AI users) lose 30% of events
|
|
736
|
-
// after day 30 — simulating the retention gap.
|
|
737
|
-
// ═══════════════════════════════════════════════════════════
|
|
738
|
-
const totalInWs = workspaceId ? (workspaceTotalUsers.get(workspaceId) || 1) : 1;
|
|
739
|
-
const aiInWs = workspaceId ? (workspaceAiUserCount.get(workspaceId) || 0) : 0;
|
|
740
|
-
const isLowAdoption = (aiInWs / totalInWs) < 0.20;
|
|
741
|
-
|
|
742
|
-
if (isLowAdoption) {
|
|
743
|
-
const day30 = firstEventTime.add(30, "days");
|
|
744
|
-
for (let i = events.length - 1; i >= 0; i--) {
|
|
745
|
-
if (dayjs(events[i].time).isAfter(day30) && chance.bool({ likelihood: 30 })) {
|
|
746
|
-
events.splice(i, 1);
|
|
747
|
-
}
|
|
748
|
-
}
|
|
749
|
-
}
|
|
750
|
-
|
|
751
|
-
// ═══════════════════════════════════════════════════════════
|
|
752
|
-
// HOOK 4: HUDDLE STICKINESS
|
|
753
|
-
// For each huddle with ai_notes_enabled=true, inject a
|
|
754
|
-
// canvas_edited event 20 minutes later if none exists in
|
|
755
|
-
// the 60-minute window — producing 2x funnel conversion.
|
|
756
|
-
// ═══════════════════════════════════════════════════════════
|
|
757
|
-
const templateCanvas = events.find(e => e.event === "canvas_edited");
|
|
758
|
-
|
|
759
|
-
huddlesWithAiNotes.forEach(huddle => {
|
|
760
|
-
const huddleTime = huddle.time;
|
|
761
|
-
const windowEnd = huddleTime.add(60, "minutes");
|
|
762
|
-
|
|
763
|
-
const hasNearbyCanvas = events.some(e =>
|
|
764
|
-
e.event === "canvas_edited" &&
|
|
765
|
-
dayjs(e.time).isAfter(huddleTime) &&
|
|
766
|
-
dayjs(e.time).isBefore(windowEnd)
|
|
767
|
-
);
|
|
768
|
-
|
|
769
|
-
// Inject if no canvas nearby, or 50% of the time even if one exists
|
|
770
|
-
// (the 50% case pushes the ratio toward 2x)
|
|
771
|
-
if (templateCanvas && (!hasNearbyCanvas || chance.bool({ likelihood: 50 }))) {
|
|
772
|
-
events.push({
|
|
773
|
-
...templateCanvas,
|
|
774
|
-
time: huddleTime.add(20, "minutes").toISOString(),
|
|
775
|
-
user_id: templateCanvas.user_id,
|
|
776
|
-
edit_type: chance.pickone(["text", "checklist"]),
|
|
777
|
-
collaborators_count: chance.integer({ min: 1, max: 4 }),
|
|
778
|
-
});
|
|
779
|
-
}
|
|
780
|
-
});
|
|
781
|
-
|
|
782
|
-
// ═══════════════════════════════════════════════════════════
|
|
783
|
-
// HOOK 5: LURKER SUMMARIZATION PATTERN
|
|
784
|
-
// Lurkers: drop 80% of message_sent, quadruple ai_summary_generated.
|
|
785
|
-
// Creates the inverse behavioral signature: high summaries, low messages.
|
|
786
|
-
// ═══════════════════════════════════════════════════════════
|
|
787
|
-
if (persona === "Lurker") {
|
|
788
|
-
// Remove 80% of message_sent events (iterate backwards)
|
|
789
|
-
for (let i = events.length - 1; i >= 0; i--) {
|
|
790
|
-
if (events[i].event === "message_sent" && chance.bool({ likelihood: 80 })) {
|
|
791
|
-
events.splice(i, 1);
|
|
792
|
-
}
|
|
793
|
-
}
|
|
794
|
-
|
|
795
|
-
// Clone 3 extra summaries per existing one (= 4x total)
|
|
796
|
-
const summarySnapshot = events.filter(e => e.event === "ai_summary_generated");
|
|
797
|
-
const extraSummaries = [];
|
|
798
|
-
summarySnapshot.forEach(s => {
|
|
799
|
-
for (let k = 0; k < 3; k++) {
|
|
800
|
-
extraSummaries.push({
|
|
801
|
-
...s,
|
|
802
|
-
time: dayjs(s.time).add(chance.integer({ min: 1, max: 48 }), "hours").toISOString(),
|
|
803
|
-
user_id: s.user_id,
|
|
804
|
-
source_type: chance.pickone(["channel", "thread", "huddle"]),
|
|
805
|
-
tokens_saved: chance.integer({ min: 100, max: 1500 }),
|
|
806
|
-
messages_summarized: chance.integer({ min: 10, max: 80 }),
|
|
807
|
-
monday_spike: false,
|
|
808
|
-
});
|
|
809
|
-
}
|
|
810
|
-
});
|
|
811
|
-
if (extraSummaries.length > 0) {
|
|
812
|
-
events.push(...extraSummaries);
|
|
813
|
-
}
|
|
814
|
-
}
|
|
815
|
-
|
|
816
|
-
// ═══════════════════════════════════════════════════════════
|
|
817
|
-
// HOOK 6 (clone pass): MONDAY PRODUCTIVITY SPIKE
|
|
818
|
-
// Clone each monday_spike=true summary once with a short offset
|
|
819
|
-
// to produce the observable 1.5x volume effect on Mondays.
|
|
820
|
-
// ═══════════════════════════════════════════════════════════
|
|
821
|
-
const mondaySummaries = events.filter(
|
|
822
|
-
e => e.event === "ai_summary_generated" && e.monday_spike === true
|
|
823
|
-
);
|
|
824
|
-
const mondayClones = [];
|
|
825
|
-
mondaySummaries.forEach(s => {
|
|
826
|
-
if (chance.bool({ likelihood: 50 })) {
|
|
827
|
-
mondayClones.push({
|
|
828
|
-
...s,
|
|
829
|
-
time: dayjs(s.time).add(chance.integer({ min: 15, max: 120 }), "minutes").toISOString(),
|
|
830
|
-
user_id: s.user_id,
|
|
831
|
-
tokens_saved: Math.round(s.tokens_saved * (0.8 + Math.random() * 0.4)),
|
|
832
|
-
messages_summarized: chance.integer({ min: 5, max: 50 }),
|
|
833
|
-
});
|
|
834
|
-
}
|
|
835
|
-
});
|
|
836
|
-
if (mondayClones.length > 0) {
|
|
837
|
-
events.push(...mondayClones);
|
|
838
|
-
}
|
|
839
|
-
|
|
840
|
-
return record;
|
|
841
|
-
}
|
|
842
|
-
|
|
843
|
-
return record;
|
|
844
|
-
},
|
|
845
|
-
};
|
|
846
|
-
|
|
847
|
-
export default config;
|