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.
- 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 +3 -0
- package/index.js +8 -36
- package/lib/cli/cli.js +0 -7
- package/lib/core/config-validator.js +6 -8
- package/lib/generators/adspend.js +1 -1
- package/lib/generators/events.js +1 -1
- package/lib/generators/funnels.js +293 -242
- 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 +29 -18
- package/package.json +7 -5
- package/types.d.ts +319 -4
- 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,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
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
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
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
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
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
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
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
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
|
-
|
|
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
|
-
|
|
274
|
-
|
|
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
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
340
|
+
context,
|
|
341
|
+
eventsWithTiming,
|
|
342
|
+
distinct_id,
|
|
343
|
+
earliestTime,
|
|
344
|
+
anonymousIds,
|
|
345
|
+
sessionIds,
|
|
346
|
+
groupKeys
|
|
297
347
|
) {
|
|
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
|
-
|
|
329
|
-
|
|
330
|
-
|
|
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
|
}
|