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.
- package/dungeons/ai-chat-analytics-ed.js +274 -0
- package/dungeons/business.js +0 -1
- package/dungeons/complex.js +0 -1
- package/dungeons/experiments.js +0 -1
- package/dungeons/gaming.js +47 -14
- package/dungeons/media.js +5 -6
- package/dungeons/mil.js +296 -0
- package/dungeons/money2020-ed-also.js +277 -0
- package/dungeons/money2020-ed.js +579 -0
- package/dungeons/sanity.js +0 -1
- package/dungeons/scd.js +0 -1
- package/dungeons/simple.js +57 -18
- package/dungeons/student-teacher.js +0 -1
- package/dungeons/text-generation.js +706 -0
- package/dungeons/userAgent.js +1 -2
- package/entry.js +4 -0
- package/index.js +63 -38
- package/lib/cli/cli.js +7 -8
- package/lib/core/config-validator.js +11 -13
- package/lib/core/context.js +13 -1
- package/lib/core/storage.js +45 -13
- package/lib/generators/adspend.js +1 -1
- package/lib/generators/events.js +18 -17
- package/lib/generators/funnels.js +293 -240
- package/lib/generators/text-bak-old.js +1121 -0
- package/lib/generators/text.js +1173 -0
- package/lib/orchestrators/mixpanel-sender.js +1 -1
- package/lib/templates/abbreviated.d.ts +13 -3
- package/lib/templates/defaults.js +311 -169
- package/lib/templates/hooks-instructions.txt +434 -0
- package/lib/templates/phrases-bak.js +925 -0
- package/lib/templates/phrases.js +2066 -0
- package/lib/templates/{instructions.txt → schema-instructions.txt} +78 -1
- package/lib/templates/scratch-dungeon-template.js +1 -1
- package/lib/templates/textQuickTest.js +172 -0
- package/lib/utils/ai.js +51 -2
- package/lib/utils/utils.js +145 -7
- package/package.json +8 -5
- package/types.d.ts +322 -7
- 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
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
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
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
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
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
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
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
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
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
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
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
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
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
340
|
+
context,
|
|
341
|
+
eventsWithTiming,
|
|
342
|
+
distinct_id,
|
|
343
|
+
earliestTime,
|
|
344
|
+
anonymousIds,
|
|
345
|
+
sessionIds,
|
|
346
|
+
groupKeys
|
|
295
347
|
) {
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
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
|
}
|