make-mp-data 2.0.22 → 2.1.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.
Files changed (40) hide show
  1. package/dungeons/ai-chat-analytics-ed.js +274 -0
  2. package/dungeons/business.js +0 -1
  3. package/dungeons/complex.js +0 -1
  4. package/dungeons/experiments.js +0 -1
  5. package/dungeons/gaming.js +47 -14
  6. package/dungeons/media.js +5 -6
  7. package/dungeons/mil.js +296 -0
  8. package/dungeons/money2020-ed-also.js +277 -0
  9. package/dungeons/money2020-ed.js +579 -0
  10. package/dungeons/sanity.js +0 -1
  11. package/dungeons/scd.js +0 -1
  12. package/dungeons/simple.js +57 -18
  13. package/dungeons/student-teacher.js +0 -1
  14. package/dungeons/text-generation.js +706 -0
  15. package/dungeons/userAgent.js +1 -2
  16. package/entry.js +4 -0
  17. package/index.js +63 -38
  18. package/lib/cli/cli.js +7 -8
  19. package/lib/core/config-validator.js +11 -13
  20. package/lib/core/context.js +13 -1
  21. package/lib/core/storage.js +45 -13
  22. package/lib/generators/adspend.js +1 -1
  23. package/lib/generators/events.js +18 -17
  24. package/lib/generators/funnels.js +293 -240
  25. package/lib/generators/text-bak-old.js +1121 -0
  26. package/lib/generators/text.js +1173 -0
  27. package/lib/orchestrators/mixpanel-sender.js +1 -1
  28. package/lib/templates/abbreviated.d.ts +13 -3
  29. package/lib/templates/defaults.js +311 -169
  30. package/lib/templates/hooks-instructions.txt +434 -0
  31. package/lib/templates/phrases-bak.js +925 -0
  32. package/lib/templates/phrases.js +2066 -0
  33. package/lib/templates/{instructions.txt → schema-instructions.txt} +78 -1
  34. package/lib/templates/scratch-dungeon-template.js +1 -1
  35. package/lib/templates/textQuickTest.js +172 -0
  36. package/lib/utils/ai.js +51 -2
  37. package/lib/utils/utils.js +145 -7
  38. package/package.json +8 -5
  39. package/types.d.ts +322 -7
  40. package/lib/utils/chart.js +0 -206
@@ -20,100 +20,137 @@ import { makeEvent } from "./events.js";
20
20
  * @returns {Promise<[Array, boolean]>} Tuple of [events, didConvert]
21
21
  */
22
22
  export async function makeFunnel(context, funnel, user, firstEventTime, profile = {}, scd = {}) {
23
- if (!funnel) throw new Error("no funnel");
24
- if (!user) throw new Error("no user");
25
-
26
- const { config } = context;
27
- const chance = u.getChance();
28
- const { hook = async (a) => a } = config;
29
-
30
- // Get session start events if configured
31
- const sessionStartEvents = config.events?.filter(a => a.isSessionStartEvent) || [];
32
-
33
- // Call pre-funnel hook
34
- await hook(funnel, "funnel-pre", { user, profile, scd, funnel, config });
35
-
36
- // Extract funnel configuration
37
- let {
38
- sequence,
39
- conversionRate = 50,
40
- order = 'sequential',
41
- timeToConvert = 1,
42
- props = {},
43
- requireRepeats = false,
44
- } = funnel;
45
-
46
- const { distinct_id, created, anonymousIds = [], sessionIds = [] } = user;
47
- const { superProps = {}, groupKeys = [] } = config;
48
-
49
- // Choose properties for this funnel instance
50
- const chosenFunnelProps = { ...props, ...superProps };
51
- for (const key in props) {
52
- try {
53
- chosenFunnelProps[key] = u.choose(chosenFunnelProps[key]);
54
- } catch (e) {
55
- console.error(`error with ${key} in ${funnel.sequence.join(" > ")} funnel`, e);
56
- }
57
- }
58
-
59
- // Build event specifications for funnel steps
60
- const funnelPossibleEvents = buildFunnelEvents(context, sequence, chosenFunnelProps);
61
-
62
- // Handle repeat logic and conversion rate adjustment
63
- const { processedEvents, adjustedConversionRate } = processEventRepeats(
64
- funnelPossibleEvents,
65
- requireRepeats,
66
- conversionRate,
67
- chance
68
- );
69
-
70
- // Determine if user converts and how many steps they'll take
71
- const { doesUserConvert, numStepsUserWillTake } = determineConversion(
72
- adjustedConversionRate,
73
- sequence.length,
74
- chance
75
- );
76
-
77
- // Get steps user will actually take
78
- const funnelStepsUserWillTake = processedEvents.slice(0, numStepsUserWillTake);
79
-
80
- // Apply ordering strategy
81
- const funnelActualOrder = applyOrderingStrategy(
82
- funnelStepsUserWillTake,
83
- order,
84
- config,
85
- sequence
86
- );
87
-
88
- // Add timing offsets to events
89
- const funnelEventsWithTiming = addTimingOffsets(
90
- funnelActualOrder,
91
- timeToConvert,
92
- numStepsUserWillTake
93
- );
94
-
95
- // Add session start event if configured
96
- if (sessionStartEvents.length) {
97
- const sessionStartEvent = chance.pickone(sessionStartEvents);
98
- sessionStartEvent.relativeTimeMs = -15000; // 15 seconds before funnel
99
- funnelEventsWithTiming.push(sessionStartEvent);
100
- }
101
-
102
- // Generate actual events with timing
103
- const finalEvents = await generateFunnelEvents(
104
- context,
105
- funnelEventsWithTiming,
106
- distinct_id,
107
- firstEventTime || dayjs(created).unix(),
108
- anonymousIds,
109
- sessionIds,
110
- groupKeys
111
- );
112
-
113
- // Call post-funnel hook
114
- await hook(finalEvents, "funnel-post", { user, profile, scd, funnel, config });
115
-
116
- return [finalEvents, doesUserConvert];
23
+ if (!funnel) throw new Error("no funnel");
24
+ if (!user) throw new Error("no user");
25
+
26
+ const { config } = context;
27
+ const chance = u.getChance();
28
+ const { hook = async (a) => a } = config;
29
+
30
+ // Get session start events if configured
31
+ const sessionStartEvents = config.events?.filter(a => a.isSessionStartEvent) || [];
32
+
33
+ // Clone funnel to avoid mutating the original object
34
+ funnel = { ...funnel };
35
+
36
+ // Experiment handling: if funnel.experiment === true, create 3 variants
37
+ let experimentVariant = null;
38
+ let experimentName = null;
39
+
40
+ if (funnel.experiment) {
41
+ experimentName = funnel.name + ` Experiment` || "Unnamed Funnel";
42
+
43
+ // Evenly distribute across 3 variants (33.33% each) using seeded chance
44
+ const randomValue = chance.floating({ min: 0, max: 1 });
45
+ if (randomValue < 0.333) {
46
+ // Variant A: WORSE conversion, slower
47
+ funnel.conversionRate = Math.max(1, Math.floor(funnel.conversionRate * 0.7));
48
+ funnel.timeToConvert = Math.max(0.1, funnel.timeToConvert * 1.5);
49
+ experimentVariant = "A";
50
+ } else if (randomValue < 0.666) {
51
+ // Variant B: BETTER conversion, faster
52
+ funnel.conversionRate = Math.min(100, Math.ceil(funnel.conversionRate * 1.3));
53
+ funnel.timeToConvert = Math.max(0.1, funnel.timeToConvert * 0.7);
54
+ experimentVariant = "B";
55
+ } else {
56
+ // Variant C: CONTROL - original values (no changes)
57
+ experimentVariant = "C";
58
+ }
59
+
60
+ // Mark that this funnel has experiment metadata (used later)
61
+ funnel._experimentName = experimentName;
62
+ funnel._experimentVariant = experimentVariant;
63
+
64
+ // Insert $experiment_started at beginning of sequence (clone array to avoid mutation)
65
+ funnel.sequence = ["$experiment_started", ...funnel.sequence];
66
+ }
67
+
68
+ // Call pre-funnel hook
69
+ await hook(funnel, "funnel-pre", { user, profile, scd, funnel, config, firstEventTime });
70
+
71
+ // Extract funnel configuration
72
+ let {
73
+ sequence,
74
+ conversionRate = 50,
75
+ order = 'sequential',
76
+ timeToConvert = 1,
77
+ props = {},
78
+ requireRepeats = false,
79
+ _experimentName: expName,
80
+ _experimentVariant: expVariant,
81
+ } = funnel;
82
+
83
+ const { distinct_id, created, anonymousIds = [], sessionIds = [] } = user;
84
+ const { superProps = {}, groupKeys = [] } = config;
85
+
86
+ // Choose properties for this funnel instance
87
+ const chosenFunnelProps = { ...props, ...superProps };
88
+ for (const key in props) {
89
+ try {
90
+ chosenFunnelProps[key] = u.choose(chosenFunnelProps[key]);
91
+ } catch (e) {
92
+ console.error(`error with ${key} in ${funnel.sequence.join(" > ")} funnel`, e);
93
+ }
94
+ }
95
+
96
+ // Build event specifications for funnel steps
97
+ const funnelPossibleEvents = buildFunnelEvents(context, sequence, chosenFunnelProps, expName, expVariant);
98
+
99
+ // Handle repeat logic and conversion rate adjustment
100
+ const { processedEvents, adjustedConversionRate } = processEventRepeats(
101
+ funnelPossibleEvents,
102
+ requireRepeats,
103
+ conversionRate,
104
+ chance
105
+ );
106
+
107
+ // Determine if user converts and how many steps they'll take
108
+ const { doesUserConvert, numStepsUserWillTake } = determineConversion(
109
+ adjustedConversionRate,
110
+ sequence.length,
111
+ chance
112
+ );
113
+
114
+ // Get steps user will actually take
115
+ const funnelStepsUserWillTake = processedEvents.slice(0, numStepsUserWillTake);
116
+
117
+ // Apply ordering strategy
118
+ const funnelActualOrder = applyOrderingStrategy(
119
+ funnelStepsUserWillTake,
120
+ order,
121
+ config,
122
+ sequence
123
+ );
124
+
125
+ // Add timing offsets to events
126
+ const funnelEventsWithTiming = addTimingOffsets(
127
+ funnelActualOrder,
128
+ timeToConvert,
129
+ numStepsUserWillTake
130
+ );
131
+
132
+ // Add session start event if configured
133
+ if (sessionStartEvents.length) {
134
+ const sessionStartEvent = chance.pickone(sessionStartEvents);
135
+ sessionStartEvent.relativeTimeMs = -15000; // 15 seconds before funnel
136
+ funnelEventsWithTiming.push(sessionStartEvent);
137
+ }
138
+
139
+ // Generate actual events with timing
140
+ const finalEvents = await generateFunnelEvents(
141
+ context,
142
+ funnelEventsWithTiming,
143
+ distinct_id,
144
+ firstEventTime || dayjs(created).unix(),
145
+ anonymousIds,
146
+ sessionIds,
147
+ groupKeys
148
+ );
149
+
150
+ // Call post-funnel hook
151
+ await hook(finalEvents, "funnel-post", { user, profile, scd, funnel, config });
152
+
153
+ return [finalEvents, doesUserConvert];
117
154
  }
118
155
 
119
156
  /**
@@ -121,33 +158,48 @@ export async function makeFunnel(context, funnel, user, firstEventTime, profile
121
158
  * @param {Context} context - Context object
122
159
  * @param {Array} sequence - Array of event names
123
160
  * @param {Object} chosenFunnelProps - Properties to apply to all events
161
+ * @param {string} [experimentName] - Name of experiment (if experiment is enabled)
162
+ * @param {string} [experimentVariant] - Variant name (A, B, or C)
124
163
  * @returns {Array} Array of event specifications
125
164
  */
126
- function buildFunnelEvents(context, sequence, chosenFunnelProps) {
127
- const { config } = context;
128
-
129
- return sequence.map((eventName) => {
130
- const foundEvent = config.events?.find((e) => e.event === eventName);
131
- const eventSpec = u.deepClone(foundEvent) || { event: eventName, properties: {} };
132
-
133
- // Process event properties
134
- for (const key in eventSpec.properties) {
135
- try {
136
- eventSpec.properties[key] = u.choose(eventSpec.properties[key]);
137
- } catch (e) {
138
- console.error(`error with ${key} in ${eventSpec.event} event`, e);
139
- }
140
- }
141
-
142
- // Clean up funnel-specific properties
143
- delete eventSpec.isFirstEvent;
144
- delete eventSpec.weight;
145
-
146
- // Merge funnel properties
147
- eventSpec.properties = { ...eventSpec.properties, ...chosenFunnelProps };
148
-
149
- return eventSpec;
150
- });
165
+ function buildFunnelEvents(context, sequence, chosenFunnelProps, experimentName, experimentVariant) {
166
+ const { config } = context;
167
+
168
+ return sequence.map((eventName) => {
169
+ // Handle $experiment_started event specially
170
+ if (eventName === "$experiment_started" && experimentName && experimentVariant) {
171
+ return {
172
+ event: "$experiment_started",
173
+ properties: {
174
+ "Experiment name": experimentName,
175
+ "Variant name": experimentVariant
176
+ }
177
+ };
178
+ }
179
+
180
+ const foundEvent = config.events?.find((e) => e.event === eventName);
181
+
182
+ // PERFORMANCE: Shallow copy instead of deepClone for better performance
183
+ // We only need to copy the top-level structure since we're rebuilding properties anyway
184
+ const eventSpec = foundEvent ? {
185
+ event: foundEvent.event,
186
+ properties: { ...foundEvent.properties }
187
+ } : { event: eventName, properties: {} };
188
+
189
+ // Process event properties
190
+ for (const key in eventSpec.properties) {
191
+ try {
192
+ eventSpec.properties[key] = u.choose(eventSpec.properties[key]);
193
+ } catch (e) {
194
+ console.error(`error with ${key} in ${eventSpec.event} event`, e);
195
+ }
196
+ }
197
+
198
+ // Merge funnel properties (no need to delete properties since we're creating a new object)
199
+ eventSpec.properties = { ...eventSpec.properties, ...chosenFunnelProps };
200
+
201
+ return eventSpec;
202
+ });
151
203
  }
152
204
 
153
205
  /**
@@ -159,32 +211,32 @@ function buildFunnelEvents(context, sequence, chosenFunnelProps) {
159
211
  * @returns {Object} Object with processedEvents and adjustedConversionRate
160
212
  */
161
213
  function processEventRepeats(events, requireRepeats, conversionRate, chance) {
162
- let adjustedConversionRate = conversionRate;
163
-
164
- const processedEvents = events.reduce((acc, step) => {
165
- if (!requireRepeats) {
166
- if (acc.find(e => e.event === step.event)) {
167
- if (chance.bool({ likelihood: 50 })) {
168
- adjustedConversionRate = Math.floor(adjustedConversionRate * 1.35); // Increase conversion rate
169
- acc.push(step);
170
- } else {
171
- adjustedConversionRate = Math.floor(adjustedConversionRate * 0.70); // Reduce conversion rate
172
- return acc; // Skip the step
173
- }
174
- } else {
175
- acc.push(step);
176
- }
177
- } else {
178
- acc.push(step);
179
- }
180
- return acc;
181
- }, []);
182
-
183
- // Clamp conversion rate
184
- if (adjustedConversionRate > 100) adjustedConversionRate = 100;
185
- if (adjustedConversionRate < 0) adjustedConversionRate = 0;
186
-
187
- return { processedEvents, adjustedConversionRate };
214
+ let adjustedConversionRate = conversionRate;
215
+
216
+ const processedEvents = events.reduce((acc, step) => {
217
+ if (!requireRepeats) {
218
+ if (acc.find(e => e.event === step.event)) {
219
+ if (chance.bool({ likelihood: 50 })) {
220
+ adjustedConversionRate = Math.floor(adjustedConversionRate * 1.35); // Increase conversion rate
221
+ acc.push(step);
222
+ } else {
223
+ adjustedConversionRate = Math.floor(adjustedConversionRate * 0.70); // Reduce conversion rate
224
+ return acc; // Skip the step
225
+ }
226
+ } else {
227
+ acc.push(step);
228
+ }
229
+ } else {
230
+ acc.push(step);
231
+ }
232
+ return acc;
233
+ }, []);
234
+
235
+ // Clamp conversion rate
236
+ if (adjustedConversionRate > 100) adjustedConversionRate = 100;
237
+ if (adjustedConversionRate < 0) adjustedConversionRate = 0;
238
+
239
+ return { processedEvents, adjustedConversionRate };
188
240
  }
189
241
 
190
242
  /**
@@ -195,12 +247,12 @@ function processEventRepeats(events, requireRepeats, conversionRate, chance) {
195
247
  * @returns {Object} Object with doesUserConvert and numStepsUserWillTake
196
248
  */
197
249
  function determineConversion(conversionRate, totalSteps, chance) {
198
- const doesUserConvert = chance.bool({ likelihood: conversionRate });
199
- const numStepsUserWillTake = doesUserConvert ?
200
- totalSteps :
201
- u.integer(1, totalSteps - 1);
202
-
203
- return { doesUserConvert, numStepsUserWillTake };
250
+ const doesUserConvert = chance.bool({ likelihood: conversionRate });
251
+ const numStepsUserWillTake = doesUserConvert ?
252
+ totalSteps :
253
+ u.integer(1, totalSteps - 1);
254
+
255
+ return { doesUserConvert, numStepsUserWillTake };
204
256
  }
205
257
 
206
258
  /**
@@ -212,27 +264,27 @@ function determineConversion(conversionRate, totalSteps, chance) {
212
264
  * @returns {Array} Ordered funnel steps
213
265
  */
214
266
  function applyOrderingStrategy(steps, order, config, sequence) {
215
- switch (order) {
216
- case "sequential":
217
- return steps;
218
- case "random":
219
- return u.shuffleArray(steps);
220
- case "first-fixed":
221
- return u.shuffleExceptFirst(steps);
222
- case "last-fixed":
223
- return u.shuffleExceptLast(steps);
224
- case "first-and-last-fixed":
225
- return u.fixFirstAndLast(steps);
226
- case "middle-fixed":
227
- return u.shuffleOutside(steps);
228
- case "interrupted":
229
- const potentialSubstitutes = config.events
230
- ?.filter(e => !e.isFirstEvent)
231
- ?.filter(e => !sequence.includes(e.event)) || [];
232
- return u.interruptArray(steps, potentialSubstitutes);
233
- default:
234
- return steps;
235
- }
267
+ switch (order) {
268
+ case "sequential":
269
+ return steps;
270
+ case "random":
271
+ return u.shuffleArray(steps);
272
+ case "first-fixed":
273
+ return u.shuffleExceptFirst(steps);
274
+ case "last-fixed":
275
+ return u.shuffleExceptLast(steps);
276
+ case "first-and-last-fixed":
277
+ return u.fixFirstAndLast(steps);
278
+ case "middle-fixed":
279
+ return u.shuffleOutside(steps);
280
+ case "interrupted":
281
+ const potentialSubstitutes = config.events
282
+ ?.filter(e => !e.isFirstEvent)
283
+ ?.filter(e => !sequence.includes(e.event)) || [];
284
+ return u.interruptArray(steps, potentialSubstitutes);
285
+ default:
286
+ return steps;
287
+ }
236
288
  }
237
289
 
238
290
  /**
@@ -243,34 +295,34 @@ function applyOrderingStrategy(steps, order, config, sequence) {
243
295
  * @returns {Array} Events with timing information
244
296
  */
245
297
  function addTimingOffsets(events, timeToConvert, numSteps) {
246
- const msInHour = 60000 * 60;
247
- let lastTimeJump = 0;
248
-
249
- return events.map((event, index) => {
250
- if (index === 0) {
251
- event.relativeTimeMs = 0;
252
- return event;
253
- }
254
-
255
- // Calculate base increment for each step
256
- const baseIncrement = (timeToConvert * msInHour) / numSteps;
257
-
258
- // Add random fluctuation
259
- const fluctuation = u.integer(
260
- -baseIncrement / u.integer(3, 5),
261
- baseIncrement / u.integer(3, 5)
262
- );
263
-
264
- // Ensure increasing timestamps
265
- const previousTime = lastTimeJump;
266
- const currentTime = previousTime + baseIncrement + fluctuation;
267
- const chosenTime = Math.max(currentTime, previousTime + 1);
268
-
269
- lastTimeJump = chosenTime;
270
- event.relativeTimeMs = chosenTime;
271
-
272
- return event;
273
- });
298
+ const msInHour = 60000 * 60;
299
+ let lastTimeJump = 0;
300
+
301
+ return events.map((event, index) => {
302
+ if (index === 0) {
303
+ event.relativeTimeMs = 0;
304
+ return event;
305
+ }
306
+
307
+ // Calculate base increment for each step
308
+ const baseIncrement = (timeToConvert * msInHour) / numSteps;
309
+
310
+ // Add random fluctuation
311
+ const fluctuation = u.integer(
312
+ -baseIncrement / u.integer(3, 5),
313
+ baseIncrement / u.integer(3, 5)
314
+ );
315
+
316
+ // Ensure increasing timestamps
317
+ const previousTime = lastTimeJump;
318
+ const currentTime = previousTime + baseIncrement + fluctuation;
319
+ const chosenTime = Math.max(currentTime, previousTime + 1);
320
+
321
+ lastTimeJump = chosenTime;
322
+ event.relativeTimeMs = chosenTime;
323
+
324
+ return event;
325
+ });
274
326
  }
275
327
 
276
328
  /**
@@ -285,45 +337,46 @@ function addTimingOffsets(events, timeToConvert, numSteps) {
285
337
  * @returns {Promise<Array>} Generated events
286
338
  */
287
339
  async function generateFunnelEvents(
288
- context,
289
- eventsWithTiming,
290
- distinct_id,
291
- earliestTime,
292
- anonymousIds,
293
- sessionIds,
294
- groupKeys
340
+ context,
341
+ eventsWithTiming,
342
+ distinct_id,
343
+ earliestTime,
344
+ anonymousIds,
345
+ sessionIds,
346
+ groupKeys
295
347
  ) {
296
- let funnelStartTime;
297
-
298
- const finalEvents = await Promise.all(eventsWithTiming.map(async (event, index) => {
299
- const newEvent = await makeEvent(
300
- context,
301
- distinct_id,
302
- earliestTime,
303
- event,
304
- anonymousIds,
305
- sessionIds,
306
- {},
307
- groupKeys
308
- );
309
-
310
- if (index === 0) {
311
- funnelStartTime = dayjs(newEvent.time);
312
- delete newEvent.relativeTimeMs;
313
- return newEvent;
314
- }
315
-
316
- try {
317
- newEvent.time = dayjs(funnelStartTime)
318
- .add(event.relativeTimeMs, "milliseconds")
319
- .toISOString();
320
- delete newEvent.relativeTimeMs;
321
- return newEvent;
322
- } catch (e) {
323
- console.error("Error setting funnel event time:", e);
324
- return newEvent;
325
- }
326
- }));
327
-
328
- return finalEvents;
348
+ let funnelStartTime;
349
+
350
+ const finalEvents = await Promise.all(eventsWithTiming.map(async (event, index) => {
351
+ const newEvent = await makeEvent(
352
+ context,
353
+ distinct_id,
354
+ earliestTime,
355
+ event,
356
+ anonymousIds,
357
+ sessionIds,
358
+ {},
359
+ groupKeys,
360
+ false // Let all funnel events use TimeSoup for proper time distribution
361
+ );
362
+
363
+ if (index === 0) {
364
+ funnelStartTime = dayjs(newEvent.time);
365
+ delete newEvent.relativeTimeMs;
366
+ return newEvent;
367
+ }
368
+
369
+ try {
370
+ newEvent.time = dayjs(funnelStartTime)
371
+ .add(event.relativeTimeMs, "milliseconds")
372
+ .toISOString();
373
+ delete newEvent.relativeTimeMs;
374
+ return newEvent;
375
+ } catch (e) {
376
+ console.error("Error setting funnel event time:", e);
377
+ return newEvent;
378
+ }
379
+ }));
380
+
381
+ return finalEvents;
329
382
  }