make-mp-data 2.0.21 → 2.0.23

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.
@@ -19,52 +19,52 @@ import os from "os";
19
19
  * @returns {Funnel[]} Array of inferred funnel configurations
20
20
  */
21
21
  function inferFunnels(events) {
22
- const createdFunnels = [];
23
- const firstEvents = events.filter((e) => e.isFirstEvent).map((e) => e.event);
24
- const usageEvents = events.filter((e) => !e.isFirstEvent).map((e) => e.event);
25
- const numFunnelsToCreate = Math.ceil(usageEvents.length);
26
-
27
- /** @type {import('../../types.js').Funnel} */
28
- const funnelTemplate = {
29
- sequence: [],
30
- conversionRate: 50,
31
- order: 'sequential',
32
- requireRepeats: false,
33
- props: {},
34
- timeToConvert: 1,
35
- isFirstFunnel: false,
36
- weight: 1
37
- };
38
-
39
- // Create funnels for first events
40
- if (firstEvents.length) {
41
- for (const event of firstEvents) {
42
- createdFunnels.push({
43
- ...u.deepClone(funnelTemplate),
44
- sequence: [event],
45
- isFirstFunnel: true,
46
- conversionRate: 100
47
- });
48
- }
49
- }
50
-
51
- // At least one funnel with all usage events
52
- createdFunnels.push({ ...u.deepClone(funnelTemplate), sequence: usageEvents });
53
-
54
- // Create random funnels for the rest
55
- for (let i = 1; i < numFunnelsToCreate; i++) {
56
- /** @type {import('../../types.js').Funnel} */
57
- const funnel = { ...u.deepClone(funnelTemplate) };
58
- funnel.conversionRate = u.integer(25, 75);
59
- funnel.timeToConvert = u.integer(1, 10);
60
- funnel.weight = u.integer(1, 10);
61
- const sequence = u.shuffleArray(usageEvents).slice(0, u.integer(2, usageEvents.length));
62
- funnel.sequence = sequence;
63
- funnel.order = 'random';
64
- createdFunnels.push(funnel);
65
- }
66
-
67
- return createdFunnels;
22
+ const createdFunnels = [];
23
+ const firstEvents = events.filter((e) => e.isFirstEvent).map((e) => e.event);
24
+ const usageEvents = events.filter((e) => !e.isFirstEvent).map((e) => e.event);
25
+ const numFunnelsToCreate = Math.ceil(usageEvents.length);
26
+
27
+ /** @type {import('../../types.js').Funnel} */
28
+ const funnelTemplate = {
29
+ sequence: [],
30
+ conversionRate: 50,
31
+ order: 'sequential',
32
+ requireRepeats: false,
33
+ props: {},
34
+ timeToConvert: 1,
35
+ isFirstFunnel: false,
36
+ weight: 1
37
+ };
38
+
39
+ // Create funnels for first events
40
+ if (firstEvents.length) {
41
+ for (const event of firstEvents) {
42
+ createdFunnels.push({
43
+ ...u.deepClone(funnelTemplate),
44
+ sequence: [event],
45
+ isFirstFunnel: true,
46
+ conversionRate: 100
47
+ });
48
+ }
49
+ }
50
+
51
+ // At least one funnel with all usage events
52
+ createdFunnels.push({ ...u.deepClone(funnelTemplate), sequence: usageEvents });
53
+
54
+ // Create random funnels for the rest
55
+ for (let i = 1; i < numFunnelsToCreate; i++) {
56
+ /** @type {import('../../types.js').Funnel} */
57
+ const funnel = { ...u.deepClone(funnelTemplate) };
58
+ funnel.conversionRate = u.integer(25, 75);
59
+ funnel.timeToConvert = u.integer(1, 10);
60
+ funnel.weight = u.integer(1, 10);
61
+ const sequence = u.shuffleArray(usageEvents).slice(0, u.integer(2, usageEvents.length));
62
+ funnel.sequence = sequence;
63
+ funnel.order = 'random';
64
+ createdFunnels.push(funnel);
65
+ }
66
+
67
+ return createdFunnels;
68
68
  }
69
69
 
70
70
  /**
@@ -73,169 +73,180 @@ function inferFunnels(events) {
73
73
  * @returns {Dungeon} Validated and enriched configuration
74
74
  */
75
75
  export function validateDungeonConfig(config) {
76
- const chance = u.getChance();
77
-
78
- // Extract configuration with defaults
79
- let {
80
- seed,
81
- numEvents = 100_000,
82
- numUsers = 1000,
83
- numDays = 30,
84
- epochStart = 0,
85
- epochEnd = dayjs().unix(),
86
- events = [{ event: "foo" }, { event: "bar" }, { event: "baz" }],
87
- superProps = { luckyNumber: [2, 2, 4, 4, 42, 42, 42, 2, 2, 4, 4, 42, 42, 42, 420] },
88
- funnels = [],
89
- userProps = {
90
- spiritAnimal: chance.animal.bind(chance),
91
- },
92
- scdProps = {},
93
- mirrorProps = {},
94
- groupKeys = [],
95
- groupProps = {},
96
- lookupTables = [],
97
- hasAnonIds = false,
98
- hasSessionIds = false,
99
- format = "csv",
100
- token = null,
101
- region = "US",
102
- writeToDisk = false,
103
- verbose = true,
104
- makeChart = false,
105
- soup = {},
106
- hook = (record) => record,
107
- hasAdSpend = false,
108
- hasCampaigns = false,
109
- hasLocation = false,
110
- hasAvatar = false,
111
- isAnonymous = false,
112
- hasBrowser = false,
113
- hasAndroidDevices = false,
114
- hasDesktopDevices = false,
115
- hasIOSDevices = false,
116
- alsoInferFunnels = false,
117
- name = "",
118
- batchSize = 500_000,
119
- concurrency
120
- } = config;
121
-
122
- // Set concurrency default only if not provided
123
- if (concurrency === undefined || concurrency === null) {
124
- concurrency = Math.min(os.cpus().length * 2, 16);
125
- }
126
-
127
- // Ensure defaults for deep objects
128
- if (!config.superProps) config.superProps = superProps;
129
- if (!config.userProps || Object.keys(config?.userProps || {})) config.userProps = userProps;
130
-
131
- // Setting up "TIME"
132
- if (epochStart && !numDays) numDays = dayjs.unix(epochEnd).diff(dayjs.unix(epochStart), "day");
133
- if (!epochStart && numDays) epochStart = dayjs.unix(epochEnd).subtract(numDays, "day").unix();
134
- if (epochStart && numDays) { } // noop
135
- if (!epochStart && !numDays) {
136
- throw new Error("Either epochStart or numDays must be provided");
137
- }
138
-
139
- // Generate simulation name
140
- config.simulationName = name || makeName();
141
- config.name = config.simulationName;
142
-
143
- // Validate events
144
- if (!events || !events.length) events = [{ event: "foo" }, { event: "bar" }, { event: "baz" }];
145
-
146
- // Convert string events to objects
147
- if (typeof events[0] === "string") {
148
- events = events.map(e => ({ event: /** @type {string} */ (e) }));
149
- }
150
-
151
- // Handle funnel inference
152
- if (alsoInferFunnels) {
153
- const inferredFunnels = inferFunnels(events);
154
- funnels = [...funnels, ...inferredFunnels];
155
- }
156
-
157
- // Create funnel for events not in other funnels
158
- const eventContainedInFunnels = Array.from(funnels.reduce((acc, f) => {
159
- const events = f.sequence;
160
- events.forEach(event => acc.add(event));
161
- return acc;
162
- }, new Set()));
163
-
164
- const eventsNotInFunnels = events
165
- .filter(e => !e.isFirstEvent)
166
- .filter(e => !eventContainedInFunnels.includes(e.event))
167
- .map(e => e.event);
168
-
169
- if (eventsNotInFunnels.length) {
170
- const sequence = u.shuffleArray(eventsNotInFunnels.flatMap(event => {
171
- let evWeight;
172
- // First check the config
173
- if (config.events) {
174
- evWeight = config.events.find(e => e.event === event)?.weight || 1;
175
- }
176
- // Fallback on default
177
- else {
178
- evWeight = 1;
179
- }
180
- return Array(evWeight).fill(event);
181
- }));
182
-
183
- funnels.push({
184
- sequence,
185
- conversionRate: 50,
186
- order: 'random',
187
- timeToConvert: 24 * 14,
188
- requireRepeats: false,
189
- });
190
- }
191
-
192
- // Event validation
193
- const validatedEvents = u.validateEventConfig(events);
194
-
195
- // Build final config object
196
- const validatedConfig = {
197
- ...config,
198
- concurrency,
199
- funnels,
200
- batchSize,
201
- seed,
202
- numEvents,
203
- numUsers,
204
- numDays,
205
- epochStart,
206
- epochEnd,
207
- events: validatedEvents,
208
- superProps,
209
- userProps,
210
- scdProps,
211
- mirrorProps,
212
- groupKeys,
213
- groupProps,
214
- lookupTables,
215
- hasAnonIds,
216
- hasSessionIds,
217
- format,
218
- token,
219
- region,
220
- writeToDisk,
221
- verbose,
222
- makeChart,
223
- soup,
224
- hook,
225
- hasAdSpend,
226
- hasCampaigns,
227
- hasLocation,
228
- hasAvatar,
229
- isAnonymous,
230
- hasBrowser,
231
- hasAndroidDevices,
232
- hasDesktopDevices,
233
- hasIOSDevices,
234
- simulationName: config.simulationName,
235
- name: config.name
236
- };
237
-
238
- return validatedConfig;
76
+ const chance = u.getChance();
77
+
78
+ // Extract configuration with defaults
79
+ let {
80
+ seed,
81
+ numEvents = 100_000,
82
+ numUsers = 1000,
83
+ numDays = 30,
84
+ epochStart = 0,
85
+ epochEnd = dayjs().unix(),
86
+ events = [{ event: "foo" }, { event: "bar" }, { event: "baz" }],
87
+ superProps = { luckyNumber: [2, 2, 4, 4, 42, 42, 42, 2, 2, 4, 4, 42, 42, 42, 420] },
88
+ funnels = [],
89
+ userProps = {
90
+ spiritAnimal: chance.animal.bind(chance),
91
+ },
92
+ scdProps = {},
93
+ mirrorProps = {},
94
+ groupKeys = [],
95
+ groupProps = {},
96
+ lookupTables = [],
97
+ hasAnonIds = false,
98
+ hasSessionIds = false,
99
+ format = "csv",
100
+ token = null,
101
+ region = "US",
102
+ writeToDisk = false,
103
+ verbose = true,
104
+ makeChart = false,
105
+ soup = {},
106
+ hook = (record) => record,
107
+ hasAdSpend = false,
108
+ hasCampaigns = false,
109
+ hasLocation = false,
110
+ hasAvatar = false,
111
+ isAnonymous = false,
112
+ hasBrowser = false,
113
+ hasAndroidDevices = false,
114
+ hasDesktopDevices = false,
115
+ hasIOSDevices = false,
116
+ alsoInferFunnels = false,
117
+ name = "",
118
+ batchSize = 500_000,
119
+ concurrency
120
+ } = config;
121
+
122
+ // Set concurrency default only if not provided
123
+ if (concurrency === undefined || concurrency === null) {
124
+ concurrency = Math.min(os.cpus().length * 2, 16);
125
+ }
126
+
127
+ // Ensure defaults for deep objects
128
+ if (!config.superProps) config.superProps = superProps;
129
+ if (!config.userProps || Object.keys(config?.userProps || {})) config.userProps = userProps;
130
+
131
+ // Setting up "TIME"
132
+ if (epochStart && !numDays) numDays = dayjs.unix(epochEnd).diff(dayjs.unix(epochStart), "day");
133
+ if (!epochStart && numDays) epochStart = dayjs.unix(epochEnd).subtract(numDays, "day").unix();
134
+ if (epochStart && numDays) { } // noop
135
+ if (!epochStart && !numDays) {
136
+ throw new Error("Either epochStart or numDays must be provided");
137
+ }
138
+
139
+ // Use provided name if non-empty string, otherwise generate one
140
+ if (!name || name === "") {
141
+ name = makeName();
142
+ }
143
+
144
+ // Validate events
145
+ if (!events || !events.length) events = [{ event: "foo" }, { event: "bar" }, { event: "baz" }];
146
+
147
+ // Convert string events to objects
148
+ if (typeof events[0] === "string") {
149
+ events = events.map(e => ({ event: /** @type {string} */ (e) }));
150
+ }
151
+
152
+ // Handle funnel inference
153
+ if (alsoInferFunnels) {
154
+ const inferredFunnels = inferFunnels(events);
155
+ funnels = [...funnels, ...inferredFunnels];
156
+ }
157
+
158
+ // Create funnel for events not in other funnels
159
+ const eventContainedInFunnels = Array.from(funnels.reduce((acc, f) => {
160
+ const events = f.sequence;
161
+ events.forEach(event => acc.add(event));
162
+ return acc;
163
+ }, new Set()));
164
+
165
+ const eventsNotInFunnels = events
166
+ .filter(e => !e.isFirstEvent)
167
+ .filter(e => !eventContainedInFunnels.includes(e.event))
168
+ .map(e => e.event);
169
+
170
+ if (eventsNotInFunnels.length) {
171
+ const sequence = u.shuffleArray(eventsNotInFunnels.flatMap(event => {
172
+ let evWeight;
173
+ // First check the config
174
+ if (config.events) {
175
+ evWeight = config.events.find(e => e.event === event)?.weight || 1;
176
+ }
177
+ // Fallback on default
178
+ else {
179
+ evWeight = 1;
180
+ }
181
+ return Array(evWeight).fill(event);
182
+ }));
183
+
184
+ funnels.push({
185
+ sequence,
186
+ conversionRate: 50,
187
+ order: 'random',
188
+ timeToConvert: 24 * 14,
189
+ requireRepeats: false,
190
+ });
191
+ }
192
+
193
+ // ensure every event in funnel sequence exists in our eventConfig
194
+ const eventInFunnels = Array.from(new Set(funnels.map(funnel => funnel.sequence).flat()));
195
+
196
+ const definedEvents = events.map(e => e.event);
197
+ const missingEvents = eventInFunnels.filter(event => !definedEvents.includes(event));
198
+ if (missingEvents.length) {
199
+ throw new Error(`Funnel sequences contain events that are not defined in the events config:\n${missingEvents.join(', ')}\nPlease ensure all events in funnel sequences are defined in the events array.`);
200
+ }
201
+
202
+
203
+
204
+ // Event validation
205
+ const validatedEvents = u.validateEventConfig(events);
206
+
207
+ // Build final config object
208
+ const validatedConfig = {
209
+ ...config,
210
+ concurrency,
211
+ funnels,
212
+ batchSize,
213
+ seed,
214
+ numEvents,
215
+ numUsers,
216
+ numDays,
217
+ epochStart,
218
+ epochEnd,
219
+ events: validatedEvents,
220
+ superProps,
221
+ userProps,
222
+ scdProps,
223
+ mirrorProps,
224
+ groupKeys,
225
+ groupProps,
226
+ lookupTables,
227
+ hasAnonIds,
228
+ hasSessionIds,
229
+ format,
230
+ token,
231
+ region,
232
+ writeToDisk,
233
+ verbose,
234
+ makeChart,
235
+ soup,
236
+ hook,
237
+ hasAdSpend,
238
+ hasCampaigns,
239
+ hasLocation,
240
+ hasAvatar,
241
+ isAnonymous,
242
+ hasBrowser,
243
+ hasAndroidDevices,
244
+ hasDesktopDevices,
245
+ hasIOSDevices,
246
+ name
247
+ };
248
+
249
+ return validatedConfig;
239
250
  }
240
251
 
241
252
  /**
@@ -249,16 +260,16 @@ export function validateDungeonConfig(config) {
249
260
  * @returns {boolean} True if validation passes
250
261
  */
251
262
  export function validateRequiredConfig(config) {
252
- if (!config) {
253
- throw new Error("Configuration is required");
254
- }
255
-
256
- if (typeof config !== 'object') {
257
- throw new Error("Configuration must be an object");
258
- }
259
-
260
- // Could add more specific validation here
261
- return true;
263
+ if (!config) {
264
+ throw new Error("Configuration is required");
265
+ }
266
+
267
+ if (typeof config !== 'object') {
268
+ throw new Error("Configuration must be an object");
269
+ }
270
+
271
+ // Could add more specific validation here
272
+ return true;
262
273
  }
263
274
 
264
275
  export { inferFunnels };
@@ -40,6 +40,14 @@ function createDefaults(config, campaignData) {
40
40
  const weighedBrowsers = u.weighArray(devices.browsers);
41
41
  const weighedCampaigns = u.weighArray(campaignData);
42
42
 
43
+ // PERFORMANCE: Pre-compute device pools based on config to avoid rebuilding in makeEvent
44
+ const devicePools = {
45
+ android: config.hasAndroidDevices ? weighedAndroidDevices : [],
46
+ ios: config.hasIOSDevices ? weighedIOSDevices : [],
47
+ desktop: config.hasDesktopDevices ? weighedDesktopDevices : []
48
+ };
49
+ const allDevices = [...devicePools.android, ...devicePools.ios, ...devicePools.desktop];
50
+
43
51
  return {
44
52
  locationsUsers: () => weighedLocationsUsers,
45
53
  locationsEvents: () => weighedLocationsEvents,
@@ -47,7 +55,11 @@ function createDefaults(config, campaignData) {
47
55
  androidDevices: () => weighedAndroidDevices,
48
56
  desktopDevices: () => weighedDesktopDevices,
49
57
  browsers: () => weighedBrowsers,
50
- campaigns: () => weighedCampaigns
58
+ campaigns: () => weighedCampaigns,
59
+
60
+ // PERFORMANCE: Pre-computed device pools
61
+ devicePools,
62
+ allDevices
51
63
  };
52
64
  }
53
65