make-mp-data 2.0.19 → 2.0.22

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