make-mp-data 1.5.56 → 2.0.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.
Files changed (40) hide show
  1. package/.claude/settings.local.json +21 -0
  2. package/.gcloudignore +2 -1
  3. package/.vscode/launch.json +6 -17
  4. package/.vscode/settings.json +31 -2
  5. package/dungeons/media.js +371 -0
  6. package/index.js +353 -1766
  7. package/{components → lib/cli}/cli.js +25 -6
  8. package/lib/cloud-function.js +20 -0
  9. package/lib/core/config-validator.js +248 -0
  10. package/lib/core/context.js +180 -0
  11. package/lib/core/storage.js +268 -0
  12. package/{components → lib/data}/defaults.js +17 -14
  13. package/lib/generators/adspend.js +133 -0
  14. package/lib/generators/events.js +242 -0
  15. package/lib/generators/funnels.js +330 -0
  16. package/lib/generators/mirror.js +168 -0
  17. package/lib/generators/profiles.js +93 -0
  18. package/lib/generators/scd.js +102 -0
  19. package/lib/orchestrators/mixpanel-sender.js +222 -0
  20. package/lib/orchestrators/user-loop.js +194 -0
  21. package/lib/orchestrators/worker-manager.js +200 -0
  22. package/{components → lib/utils}/ai.js +8 -36
  23. package/{components → lib/utils}/chart.js +9 -9
  24. package/{components → lib/utils}/project.js +4 -4
  25. package/{components → lib/utils}/utils.js +35 -23
  26. package/package.json +15 -15
  27. package/scripts/dana.mjs +137 -0
  28. package/scripts/new-dungeon.sh +7 -6
  29. package/scripts/update-deps.sh +2 -1
  30. package/tests/cli.test.js +28 -25
  31. package/tests/e2e.test.js +38 -36
  32. package/tests/int.test.js +151 -56
  33. package/tests/testSoup.mjs +1 -1
  34. package/tests/unit.test.js +15 -14
  35. package/tsconfig.json +1 -1
  36. package/types.d.ts +68 -11
  37. package/vitest.config.js +47 -0
  38. package/log.json +0 -1678
  39. package/tests/jest.config.js +0 -47
  40. /package/{components → lib/utils}/prompt.txt +0 -0
@@ -0,0 +1,242 @@
1
+ /**
2
+ * Event generator module
3
+ * Creates individual Mixpanel events with realistic properties and timing
4
+ */
5
+
6
+ /** @typedef {import('../../types').Dungeon} Config */
7
+ /** @typedef {import('../../types').EventConfig} EventConfig */
8
+ /** @typedef {import('../../types').ValueValid} ValueValid */
9
+ /** @typedef {import('../../types').EventSchema} EventSchema */
10
+ /** @typedef {import('../../types').Context} Context */
11
+
12
+ import dayjs from "dayjs";
13
+ import { md5 } from "ak-tools";
14
+ import * as u from "../utils/utils.js";
15
+
16
+ /**
17
+ * Creates a Mixpanel event with a flat shape
18
+ * @param {Context} context - Context object containing config, defaults, etc.
19
+ * @param {string} distinct_id - User identifier
20
+ * @param {number} earliestTime - Unix timestamp for earliest possible event time
21
+ * @param {Object} chosenEvent - Event configuration object
22
+ * @param {string[]} [anonymousIds] - Array of anonymous/device IDs
23
+ * @param {string[]} [sessionIds] - Array of session IDs
24
+ * @param {Object} [superProps] - Super properties to add to event
25
+ * @param {Array} [groupKeys] - Group key configurations
26
+ * @param {boolean} [isFirstEvent] - Whether this is the user's first event
27
+ * @param {boolean} [skipDefaults] - Whether to skip adding default properties
28
+ * @returns {Promise<Object>} Generated event object
29
+ */
30
+ export async function makeEvent(
31
+ context,
32
+ distinct_id,
33
+ earliestTime,
34
+ chosenEvent,
35
+ anonymousIds = [],
36
+ sessionIds = [],
37
+ superProps = {},
38
+ groupKeys = [],
39
+ isFirstEvent = false,
40
+ skipDefaults = false
41
+ ) {
42
+ // Validate required parameters
43
+ if (!distinct_id) throw new Error("no distinct_id");
44
+ if (!earliestTime) throw new Error("no earliestTime");
45
+ if (!chosenEvent) throw new Error("no chosenEvent");
46
+
47
+ // Update context metrics
48
+ context.incrementOperations();
49
+ context.incrementEvents();
50
+
51
+ const { config, defaults } = context;
52
+ const chance = u.getChance();
53
+
54
+ // Extract soup configuration for time distribution
55
+ const { mean = 0, deviation = 2, peaks = 5 } = config.soup || {};
56
+
57
+ // Extract feature flags from config
58
+ const {
59
+ hasAndroidDevices = false,
60
+ hasBrowser = false,
61
+ hasCampaigns = false,
62
+ hasDesktopDevices = false,
63
+ hasIOSDevices = false,
64
+ hasLocation = false
65
+ } = config;
66
+
67
+ // Create base event template
68
+ const eventTemplate = {
69
+ event: chosenEvent.event,
70
+ source: "dm4",
71
+ time: "",
72
+ insert_id: "",
73
+ };
74
+
75
+ let defaultProps = {};
76
+ let devicePool = [];
77
+
78
+ // Add default properties based on configuration
79
+ if (hasLocation) {
80
+ defaultProps.location = u.shuffleArray(defaults.locationsEvents()).pop();
81
+ }
82
+
83
+ if (hasBrowser) {
84
+ defaultProps.browser = u.choose(defaults.browsers());
85
+ }
86
+
87
+ // Build device pool based on enabled device types
88
+ if (hasAndroidDevices) devicePool.push(defaults.androidDevices());
89
+ if (hasIOSDevices) devicePool.push(defaults.iOSDevices());
90
+ if (hasDesktopDevices) devicePool.push(defaults.desktopDevices());
91
+
92
+ // Add campaigns with attribution likelihood
93
+ if (hasCampaigns && chance.bool({ likelihood: 25 })) {
94
+ defaultProps.campaigns = u.shuffleArray(defaults.campaigns()).pop();
95
+ }
96
+
97
+ // Select device from pool
98
+ const devices = devicePool.flat();
99
+ if (devices.length) {
100
+ defaultProps.device = u.shuffleArray(devices).pop();
101
+ }
102
+
103
+ // Set event time using TimeSoup for realistic distribution
104
+ if (earliestTime) {
105
+ if (isFirstEvent) {
106
+ eventTemplate.time = dayjs.unix(earliestTime).toISOString();
107
+ } else {
108
+ eventTemplate.time = u.TimeSoup(earliestTime, context.FIXED_NOW, peaks, deviation, mean);
109
+ }
110
+ }
111
+
112
+ // Add anonymous and session identifiers
113
+ if (anonymousIds.length) {
114
+ eventTemplate.device_id = chance.pickone(anonymousIds);
115
+ }
116
+
117
+ if (sessionIds.length) {
118
+ eventTemplate.session_id = chance.pickone(sessionIds);
119
+ }
120
+
121
+ // Sometimes add user_id (for attribution modeling)
122
+ if (!isFirstEvent && chance.bool({ likelihood: 42 })) {
123
+ eventTemplate.user_id = distinct_id;
124
+ }
125
+
126
+ // Ensure we have either user_id or device_id
127
+ if (!eventTemplate.user_id && !eventTemplate.device_id) {
128
+ eventTemplate.user_id = distinct_id;
129
+ }
130
+
131
+ // Merge custom properties with super properties
132
+ const props = { ...chosenEvent.properties, ...superProps };
133
+
134
+ // Add custom properties from event configuration
135
+ for (const key in props) {
136
+ try {
137
+ eventTemplate[key] = u.choose(props[key]);
138
+ } catch (e) {
139
+ console.error(`error with ${key} in ${chosenEvent.event} event`, e);
140
+ // Continue processing other properties
141
+ }
142
+ }
143
+
144
+ // Add default properties if not skipped
145
+ if (!skipDefaults) {
146
+ addDefaultProperties(eventTemplate, defaultProps);
147
+ }
148
+
149
+ // Add group properties
150
+ addGroupProperties(eventTemplate, groupKeys);
151
+
152
+ // Generate unique insert_id
153
+ eventTemplate.insert_id = md5(JSON.stringify(eventTemplate));
154
+
155
+ // Apply time shift to move events to current timeline
156
+ if (earliestTime) {
157
+ const timeShift = dayjs().add(2, "day").diff(dayjs.unix(context.FIXED_NOW), "seconds");
158
+ const timeShifted = dayjs(eventTemplate.time).add(timeShift, "seconds").toISOString();
159
+ eventTemplate.time = timeShifted;
160
+ }
161
+
162
+ return eventTemplate;
163
+ }
164
+
165
+ /**
166
+ * Adds default properties to an event template
167
+ * Handles complex nested property structures
168
+ * @param {Object} eventTemplate - Event object to modify
169
+ * @param {Object} defaultProps - Default properties to add
170
+ */
171
+ function addDefaultProperties(eventTemplate, defaultProps) {
172
+ for (const key in defaultProps) {
173
+ if (Array.isArray(defaultProps[key])) {
174
+ const choice = u.choose(defaultProps[key]);
175
+
176
+ if (typeof choice === "string") {
177
+ if (!eventTemplate[key]) eventTemplate[key] = choice;
178
+ }
179
+ else if (Array.isArray(choice)) {
180
+ for (const subChoice of choice) {
181
+ if (!eventTemplate[key]) eventTemplate[key] = subChoice;
182
+ }
183
+ }
184
+ else if (typeof choice === "object") {
185
+ addNestedObjectProperties(eventTemplate, choice);
186
+ }
187
+ }
188
+ else if (typeof defaultProps[key] === "object") {
189
+ addNestedObjectProperties(eventTemplate, defaultProps[key]);
190
+ }
191
+ else {
192
+ if (!eventTemplate[key]) eventTemplate[key] = defaultProps[key];
193
+ }
194
+ }
195
+ }
196
+
197
+ /**
198
+ * Adds nested object properties to event template
199
+ * @param {Object} eventTemplate - Event object to modify
200
+ * @param {Object} obj - Object with properties to add
201
+ */
202
+ function addNestedObjectProperties(eventTemplate, obj) {
203
+ for (const subKey in obj) {
204
+ if (typeof obj[subKey] === "string") {
205
+ if (!eventTemplate[subKey]) eventTemplate[subKey] = obj[subKey];
206
+ }
207
+ else if (Array.isArray(obj[subKey])) {
208
+ const subChoice = u.choose(obj[subKey]);
209
+ if (!eventTemplate[subKey]) eventTemplate[subKey] = subChoice;
210
+ }
211
+ else if (typeof obj[subKey] === "object") {
212
+ for (const subSubKey in obj[subKey]) {
213
+ if (!eventTemplate[subSubKey]) {
214
+ eventTemplate[subSubKey] = obj[subKey][subSubKey];
215
+ }
216
+ }
217
+ }
218
+ }
219
+ }
220
+
221
+ /**
222
+ * Adds group properties to event based on group key configuration
223
+ * @param {Object} eventTemplate - Event object to modify
224
+ * @param {Array} groupKeys - Array of group key configurations
225
+ */
226
+ function addGroupProperties(eventTemplate, groupKeys) {
227
+ for (const groupPair of groupKeys) {
228
+ const groupKey = groupPair[0];
229
+ const groupCardinality = groupPair[1];
230
+ const groupEvents = groupPair[2] || [];
231
+
232
+ // Empty array for group events means all events get the group property
233
+ if (!groupEvents.length) {
234
+ eventTemplate[groupKey] = u.pick(u.weighNumRange(1, groupCardinality));
235
+ }
236
+
237
+ // Only add group property if event is in the specified group events
238
+ if (groupEvents.includes(eventTemplate.event)) {
239
+ eventTemplate[groupKey] = u.pick(u.weighNumRange(1, groupCardinality));
240
+ }
241
+ }
242
+ }
@@ -0,0 +1,330 @@
1
+ /**
2
+ * Funnel generator module
3
+ * Creates conversion sequences with realistic timing and ordering
4
+ */
5
+
6
+ /** @typedef {import('../../types').Context} Context */
7
+
8
+ import dayjs from "dayjs";
9
+ import { clone } from "ak-tools";
10
+ import * as u from "../utils/utils.js";
11
+ import { makeEvent } from "./events.js";
12
+
13
+ /**
14
+ * Creates a funnel (sequence of events) for a user with conversion logic
15
+ * @param {Context} context - Context object containing config, defaults, etc.
16
+ * @param {Object} funnel - Funnel configuration
17
+ * @param {Object} user - User object with distinct_id, created, etc.
18
+ * @param {number} firstEventTime - Unix timestamp for first event
19
+ * @param {Object} profile - User profile object
20
+ * @param {Object} scd - Slowly changing dimensions object
21
+ * @returns {Promise<[Array, boolean]>} Tuple of [events, didConvert]
22
+ */
23
+ export async function makeFunnel(context, funnel, user, firstEventTime, profile = {}, scd = {}) {
24
+ if (!funnel) throw new Error("no funnel");
25
+ if (!user) throw new Error("no user");
26
+
27
+ const { config } = context;
28
+ const chance = u.getChance();
29
+ const { hook = async (a) => a } = config;
30
+
31
+ // Get session start events if configured
32
+ const sessionStartEvents = config.events?.filter(a => a.isSessionStartEvent) || [];
33
+
34
+ // Call pre-funnel hook
35
+ await hook(funnel, "funnel-pre", { user, profile, scd, funnel, config });
36
+
37
+ // Extract funnel configuration
38
+ let {
39
+ sequence,
40
+ conversionRate = 50,
41
+ order = 'sequential',
42
+ timeToConvert = 1,
43
+ props = {},
44
+ requireRepeats = false,
45
+ } = funnel;
46
+
47
+ const { distinct_id, created, anonymousIds = [], sessionIds = [] } = user;
48
+ const { superProps = {}, groupKeys = [] } = config;
49
+
50
+ // Choose properties for this funnel instance
51
+ const chosenFunnelProps = { ...props, ...superProps };
52
+ for (const key in props) {
53
+ try {
54
+ chosenFunnelProps[key] = u.choose(chosenFunnelProps[key]);
55
+ } catch (e) {
56
+ console.error(`error with ${key} in ${funnel.sequence.join(" > ")} funnel`, e);
57
+ }
58
+ }
59
+
60
+ // Build event specifications for funnel steps
61
+ const funnelPossibleEvents = buildFunnelEvents(context, sequence, chosenFunnelProps);
62
+
63
+ // Handle repeat logic and conversion rate adjustment
64
+ const { processedEvents, adjustedConversionRate } = processEventRepeats(
65
+ funnelPossibleEvents,
66
+ requireRepeats,
67
+ conversionRate,
68
+ chance
69
+ );
70
+
71
+ // Determine if user converts and how many steps they'll take
72
+ const { doesUserConvert, numStepsUserWillTake } = determineConversion(
73
+ adjustedConversionRate,
74
+ sequence.length,
75
+ chance
76
+ );
77
+
78
+ // Get steps user will actually take
79
+ const funnelStepsUserWillTake = processedEvents.slice(0, numStepsUserWillTake);
80
+
81
+ // Apply ordering strategy
82
+ const funnelActualOrder = applyOrderingStrategy(
83
+ funnelStepsUserWillTake,
84
+ order,
85
+ config,
86
+ sequence
87
+ );
88
+
89
+ // Add timing offsets to events
90
+ const funnelEventsWithTiming = addTimingOffsets(
91
+ funnelActualOrder,
92
+ timeToConvert,
93
+ numStepsUserWillTake
94
+ );
95
+
96
+ // Add session start event if configured
97
+ if (sessionStartEvents.length) {
98
+ const sessionStartEvent = chance.pickone(sessionStartEvents);
99
+ sessionStartEvent.relativeTimeMs = -15000; // 15 seconds before funnel
100
+ funnelEventsWithTiming.push(sessionStartEvent);
101
+ }
102
+
103
+ // Generate actual events with timing
104
+ const finalEvents = await generateFunnelEvents(
105
+ context,
106
+ funnelEventsWithTiming,
107
+ distinct_id,
108
+ firstEventTime || dayjs(created).unix(),
109
+ anonymousIds,
110
+ sessionIds,
111
+ groupKeys
112
+ );
113
+
114
+ // Call post-funnel hook
115
+ await hook(finalEvents, "funnel-post", { user, profile, scd, funnel, config });
116
+
117
+ return [finalEvents, doesUserConvert];
118
+ }
119
+
120
+ /**
121
+ * Builds event specifications for funnel steps
122
+ * @param {Context} context - Context object
123
+ * @param {Array} sequence - Array of event names
124
+ * @param {Object} chosenFunnelProps - Properties to apply to all events
125
+ * @returns {Array} Array of event specifications
126
+ */
127
+ function buildFunnelEvents(context, sequence, chosenFunnelProps) {
128
+ const { config } = context;
129
+
130
+ return sequence.map((eventName) => {
131
+ const foundEvent = config.events?.find((e) => e.event === eventName);
132
+ const eventSpec = clone(foundEvent) || { event: eventName, properties: {} };
133
+
134
+ // Process event properties
135
+ for (const key in eventSpec.properties) {
136
+ try {
137
+ eventSpec.properties[key] = u.choose(eventSpec.properties[key]);
138
+ } catch (e) {
139
+ console.error(`error with ${key} in ${eventSpec.event} event`, e);
140
+ }
141
+ }
142
+
143
+ // Clean up funnel-specific properties
144
+ delete eventSpec.isFirstEvent;
145
+ delete eventSpec.weight;
146
+
147
+ // Merge funnel properties
148
+ eventSpec.properties = { ...eventSpec.properties, ...chosenFunnelProps };
149
+
150
+ return eventSpec;
151
+ });
152
+ }
153
+
154
+ /**
155
+ * Processes event repeats and adjusts conversion rate
156
+ * @param {Array} events - Array of event specifications
157
+ * @param {boolean} requireRepeats - Whether repeats are required
158
+ * @param {number} conversionRate - Base conversion rate
159
+ * @param {Object} chance - Chance.js instance
160
+ * @returns {Object} Object with processedEvents and adjustedConversionRate
161
+ */
162
+ function processEventRepeats(events, requireRepeats, conversionRate, chance) {
163
+ let adjustedConversionRate = conversionRate;
164
+
165
+ const processedEvents = events.reduce((acc, step) => {
166
+ if (!requireRepeats) {
167
+ if (acc.find(e => e.event === step.event)) {
168
+ if (chance.bool({ likelihood: 50 })) {
169
+ adjustedConversionRate = Math.floor(adjustedConversionRate * 1.35); // Increase conversion rate
170
+ acc.push(step);
171
+ } else {
172
+ adjustedConversionRate = Math.floor(adjustedConversionRate * 0.70); // Reduce conversion rate
173
+ return acc; // Skip the step
174
+ }
175
+ } else {
176
+ acc.push(step);
177
+ }
178
+ } else {
179
+ acc.push(step);
180
+ }
181
+ return acc;
182
+ }, []);
183
+
184
+ // Clamp conversion rate
185
+ if (adjustedConversionRate > 100) adjustedConversionRate = 100;
186
+ if (adjustedConversionRate < 0) adjustedConversionRate = 0;
187
+
188
+ return { processedEvents, adjustedConversionRate };
189
+ }
190
+
191
+ /**
192
+ * Determines if user converts and how many steps they'll take
193
+ * @param {number} conversionRate - Adjusted conversion rate
194
+ * @param {number} totalSteps - Total number of steps in funnel
195
+ * @param {Object} chance - Chance.js instance
196
+ * @returns {Object} Object with doesUserConvert and numStepsUserWillTake
197
+ */
198
+ function determineConversion(conversionRate, totalSteps, chance) {
199
+ const doesUserConvert = chance.bool({ likelihood: conversionRate });
200
+ const numStepsUserWillTake = doesUserConvert ?
201
+ totalSteps :
202
+ u.integer(1, totalSteps - 1);
203
+
204
+ return { doesUserConvert, numStepsUserWillTake };
205
+ }
206
+
207
+ /**
208
+ * Applies ordering strategy to funnel steps
209
+ * @param {Array} steps - Funnel steps to order
210
+ * @param {string} order - Ordering strategy
211
+ * @param {Object} config - Configuration object
212
+ * @param {Array} sequence - Original sequence for interrupted mode
213
+ * @returns {Array} Ordered funnel steps
214
+ */
215
+ function applyOrderingStrategy(steps, order, config, sequence) {
216
+ switch (order) {
217
+ case "sequential":
218
+ return steps;
219
+ case "random":
220
+ return u.shuffleArray(steps);
221
+ case "first-fixed":
222
+ return u.shuffleExceptFirst(steps);
223
+ case "last-fixed":
224
+ return u.shuffleExceptLast(steps);
225
+ case "first-and-last-fixed":
226
+ return u.fixFirstAndLast(steps);
227
+ case "middle-fixed":
228
+ return u.shuffleOutside(steps);
229
+ case "interrupted":
230
+ const potentialSubstitutes = config.events
231
+ ?.filter(e => !e.isFirstEvent)
232
+ ?.filter(e => !sequence.includes(e.event)) || [];
233
+ return u.interruptArray(steps, potentialSubstitutes);
234
+ default:
235
+ return steps;
236
+ }
237
+ }
238
+
239
+ /**
240
+ * Adds timing offsets to funnel events
241
+ * @param {Array} events - Events to add timing to
242
+ * @param {number} timeToConvert - Total time to convert (in hours)
243
+ * @param {number} numSteps - Number of steps in funnel
244
+ * @returns {Array} Events with timing information
245
+ */
246
+ function addTimingOffsets(events, timeToConvert, numSteps) {
247
+ const msInHour = 60000 * 60;
248
+ let lastTimeJump = 0;
249
+
250
+ return events.map((event, index) => {
251
+ if (index === 0) {
252
+ event.relativeTimeMs = 0;
253
+ return event;
254
+ }
255
+
256
+ // Calculate base increment for each step
257
+ const baseIncrement = (timeToConvert * msInHour) / numSteps;
258
+
259
+ // Add random fluctuation
260
+ const fluctuation = u.integer(
261
+ -baseIncrement / u.integer(3, 5),
262
+ baseIncrement / u.integer(3, 5)
263
+ );
264
+
265
+ // Ensure increasing timestamps
266
+ const previousTime = lastTimeJump;
267
+ const currentTime = previousTime + baseIncrement + fluctuation;
268
+ const chosenTime = Math.max(currentTime, previousTime + 1);
269
+
270
+ lastTimeJump = chosenTime;
271
+ event.relativeTimeMs = chosenTime;
272
+
273
+ return event;
274
+ });
275
+ }
276
+
277
+ /**
278
+ * Generates actual events with proper timing
279
+ * @param {Context} context - Context object
280
+ * @param {Array} eventsWithTiming - Events with timing information
281
+ * @param {string} distinct_id - User ID
282
+ * @param {number} earliestTime - Base timestamp
283
+ * @param {Array} anonymousIds - Anonymous IDs
284
+ * @param {Array} sessionIds - Session IDs
285
+ * @param {Array} groupKeys - Group keys
286
+ * @returns {Promise<Array>} Generated events
287
+ */
288
+ async function generateFunnelEvents(
289
+ context,
290
+ eventsWithTiming,
291
+ distinct_id,
292
+ earliestTime,
293
+ anonymousIds,
294
+ sessionIds,
295
+ groupKeys
296
+ ) {
297
+ let funnelStartTime;
298
+
299
+ const finalEvents = await Promise.all(eventsWithTiming.map(async (event, index) => {
300
+ const newEvent = await makeEvent(
301
+ context,
302
+ distinct_id,
303
+ earliestTime,
304
+ event,
305
+ anonymousIds,
306
+ sessionIds,
307
+ {},
308
+ groupKeys
309
+ );
310
+
311
+ if (index === 0) {
312
+ funnelStartTime = dayjs(newEvent.time);
313
+ delete newEvent.relativeTimeMs;
314
+ return newEvent;
315
+ }
316
+
317
+ try {
318
+ newEvent.time = dayjs(funnelStartTime)
319
+ .add(event.relativeTimeMs, "milliseconds")
320
+ .toISOString();
321
+ delete newEvent.relativeTimeMs;
322
+ return newEvent;
323
+ } catch (e) {
324
+ console.error("Error setting funnel event time:", e);
325
+ return newEvent;
326
+ }
327
+ }));
328
+
329
+ return finalEvents;
330
+ }