make-mp-data 2.0.23 → 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 (38) 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 +3 -0
  17. package/index.js +8 -36
  18. package/lib/cli/cli.js +0 -7
  19. package/lib/core/config-validator.js +6 -8
  20. package/lib/generators/adspend.js +1 -1
  21. package/lib/generators/events.js +1 -1
  22. package/lib/generators/funnels.js +293 -242
  23. package/lib/generators/text-bak-old.js +1121 -0
  24. package/lib/generators/text.js +1173 -0
  25. package/lib/orchestrators/mixpanel-sender.js +1 -1
  26. package/lib/templates/abbreviated.d.ts +13 -3
  27. package/lib/templates/defaults.js +311 -169
  28. package/lib/templates/hooks-instructions.txt +434 -0
  29. package/lib/templates/phrases-bak.js +925 -0
  30. package/lib/templates/phrases.js +2066 -0
  31. package/lib/templates/{instructions.txt → schema-instructions.txt} +78 -1
  32. package/lib/templates/scratch-dungeon-template.js +1 -1
  33. package/lib/templates/textQuickTest.js +172 -0
  34. package/lib/utils/ai.js +51 -2
  35. package/lib/utils/utils.js +29 -18
  36. package/package.json +7 -5
  37. package/types.d.ts +319 -4
  38. 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,35 +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
-
132
- // PERFORMANCE: Shallow copy instead of deepClone for better performance
133
- // We only need to copy the top-level structure since we're rebuilding properties anyway
134
- const eventSpec = foundEvent ? {
135
- event: foundEvent.event,
136
- properties: { ...foundEvent.properties }
137
- } : { event: eventName, properties: {} };
138
-
139
- // Process event properties
140
- for (const key in eventSpec.properties) {
141
- try {
142
- eventSpec.properties[key] = u.choose(eventSpec.properties[key]);
143
- } catch (e) {
144
- console.error(`error with ${key} in ${eventSpec.event} event`, e);
145
- }
146
- }
147
-
148
- // Merge funnel properties (no need to delete properties since we're creating a new object)
149
- eventSpec.properties = { ...eventSpec.properties, ...chosenFunnelProps };
150
-
151
- return eventSpec;
152
- });
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
+ });
153
203
  }
154
204
 
155
205
  /**
@@ -161,32 +211,32 @@ function buildFunnelEvents(context, sequence, chosenFunnelProps) {
161
211
  * @returns {Object} Object with processedEvents and adjustedConversionRate
162
212
  */
163
213
  function processEventRepeats(events, requireRepeats, conversionRate, chance) {
164
- let adjustedConversionRate = conversionRate;
165
-
166
- const processedEvents = events.reduce((acc, step) => {
167
- if (!requireRepeats) {
168
- if (acc.find(e => e.event === step.event)) {
169
- if (chance.bool({ likelihood: 50 })) {
170
- adjustedConversionRate = Math.floor(adjustedConversionRate * 1.35); // Increase conversion rate
171
- acc.push(step);
172
- } else {
173
- adjustedConversionRate = Math.floor(adjustedConversionRate * 0.70); // Reduce conversion rate
174
- return acc; // Skip the step
175
- }
176
- } else {
177
- acc.push(step);
178
- }
179
- } else {
180
- acc.push(step);
181
- }
182
- return acc;
183
- }, []);
184
-
185
- // Clamp conversion rate
186
- if (adjustedConversionRate > 100) adjustedConversionRate = 100;
187
- if (adjustedConversionRate < 0) adjustedConversionRate = 0;
188
-
189
- 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 };
190
240
  }
191
241
 
192
242
  /**
@@ -197,12 +247,12 @@ function processEventRepeats(events, requireRepeats, conversionRate, chance) {
197
247
  * @returns {Object} Object with doesUserConvert and numStepsUserWillTake
198
248
  */
199
249
  function determineConversion(conversionRate, totalSteps, chance) {
200
- const doesUserConvert = chance.bool({ likelihood: conversionRate });
201
- const numStepsUserWillTake = doesUserConvert ?
202
- totalSteps :
203
- u.integer(1, totalSteps - 1);
204
-
205
- 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 };
206
256
  }
207
257
 
208
258
  /**
@@ -214,27 +264,27 @@ function determineConversion(conversionRate, totalSteps, chance) {
214
264
  * @returns {Array} Ordered funnel steps
215
265
  */
216
266
  function applyOrderingStrategy(steps, order, config, sequence) {
217
- switch (order) {
218
- case "sequential":
219
- return steps;
220
- case "random":
221
- return u.shuffleArray(steps);
222
- case "first-fixed":
223
- return u.shuffleExceptFirst(steps);
224
- case "last-fixed":
225
- return u.shuffleExceptLast(steps);
226
- case "first-and-last-fixed":
227
- return u.fixFirstAndLast(steps);
228
- case "middle-fixed":
229
- return u.shuffleOutside(steps);
230
- case "interrupted":
231
- const potentialSubstitutes = config.events
232
- ?.filter(e => !e.isFirstEvent)
233
- ?.filter(e => !sequence.includes(e.event)) || [];
234
- return u.interruptArray(steps, potentialSubstitutes);
235
- default:
236
- return steps;
237
- }
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
+ }
238
288
  }
239
289
 
240
290
  /**
@@ -245,34 +295,34 @@ function applyOrderingStrategy(steps, order, config, sequence) {
245
295
  * @returns {Array} Events with timing information
246
296
  */
247
297
  function addTimingOffsets(events, timeToConvert, numSteps) {
248
- const msInHour = 60000 * 60;
249
- let lastTimeJump = 0;
250
-
251
- return events.map((event, index) => {
252
- if (index === 0) {
253
- event.relativeTimeMs = 0;
254
- return event;
255
- }
256
-
257
- // Calculate base increment for each step
258
- const baseIncrement = (timeToConvert * msInHour) / numSteps;
259
-
260
- // Add random fluctuation
261
- const fluctuation = u.integer(
262
- -baseIncrement / u.integer(3, 5),
263
- baseIncrement / u.integer(3, 5)
264
- );
265
-
266
- // Ensure increasing timestamps
267
- const previousTime = lastTimeJump;
268
- const currentTime = previousTime + baseIncrement + fluctuation;
269
- const chosenTime = Math.max(currentTime, previousTime + 1);
270
-
271
- lastTimeJump = chosenTime;
272
- event.relativeTimeMs = chosenTime;
273
-
274
- return event;
275
- });
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
+ });
276
326
  }
277
327
 
278
328
  /**
@@ -287,45 +337,46 @@ function addTimingOffsets(events, timeToConvert, numSteps) {
287
337
  * @returns {Promise<Array>} Generated events
288
338
  */
289
339
  async function generateFunnelEvents(
290
- context,
291
- eventsWithTiming,
292
- distinct_id,
293
- earliestTime,
294
- anonymousIds,
295
- sessionIds,
296
- groupKeys
340
+ context,
341
+ eventsWithTiming,
342
+ distinct_id,
343
+ earliestTime,
344
+ anonymousIds,
345
+ sessionIds,
346
+ groupKeys
297
347
  ) {
298
- let funnelStartTime;
299
-
300
- const finalEvents = await Promise.all(eventsWithTiming.map(async (event, index) => {
301
- const newEvent = await makeEvent(
302
- context,
303
- distinct_id,
304
- earliestTime,
305
- event,
306
- anonymousIds,
307
- sessionIds,
308
- {},
309
- groupKeys
310
- );
311
-
312
- if (index === 0) {
313
- funnelStartTime = dayjs(newEvent.time);
314
- delete newEvent.relativeTimeMs;
315
- return newEvent;
316
- }
317
-
318
- try {
319
- newEvent.time = dayjs(funnelStartTime)
320
- .add(event.relativeTimeMs, "milliseconds")
321
- .toISOString();
322
- delete newEvent.relativeTimeMs;
323
- return newEvent;
324
- } catch (e) {
325
- console.error("Error setting funnel event time:", e);
326
- return newEvent;
327
- }
328
- }));
329
-
330
- 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;
331
382
  }