make-mp-data 2.0.1 → 2.0.2

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 (64) hide show
  1. package/dungeons/adspend.js +96 -0
  2. package/dungeons/anon.js +104 -0
  3. package/dungeons/big.js +225 -0
  4. package/dungeons/business.js +345 -0
  5. package/dungeons/complex.js +396 -0
  6. package/dungeons/experiments.js +125 -0
  7. package/dungeons/foobar.js +241 -0
  8. package/dungeons/funnels.js +272 -0
  9. package/dungeons/gaming.js +315 -0
  10. package/dungeons/media.js +7 -7
  11. package/dungeons/mirror.js +129 -0
  12. package/dungeons/sanity.js +113 -0
  13. package/dungeons/scd.js +205 -0
  14. package/dungeons/simple.js +195 -0
  15. package/dungeons/userAgent.js +190 -0
  16. package/entry.js +57 -0
  17. package/index.js +96 -68
  18. package/lib/cli/cli.js +5 -4
  19. package/lib/core/config-validator.js +28 -12
  20. package/lib/core/context.js +147 -130
  21. package/lib/core/storage.js +45 -31
  22. package/lib/data/defaults.js +2 -2
  23. package/lib/generators/adspend.js +1 -2
  24. package/lib/generators/events.js +35 -24
  25. package/lib/generators/funnels.js +1 -2
  26. package/lib/generators/mirror.js +1 -2
  27. package/lib/orchestrators/mixpanel-sender.js +15 -7
  28. package/lib/orchestrators/user-loop.js +21 -10
  29. package/lib/orchestrators/worker-manager.js +5 -2
  30. package/lib/utils/ai.js +36 -63
  31. package/lib/utils/chart.js +5 -0
  32. package/lib/utils/instructions.txt +593 -0
  33. package/lib/utils/utils.js +162 -38
  34. package/package.json +23 -9
  35. package/types.d.ts +376 -376
  36. package/.claude/settings.local.json +0 -21
  37. package/.gcloudignore +0 -18
  38. package/.gitattributes +0 -2
  39. package/.prettierrc +0 -0
  40. package/.vscode/launch.json +0 -80
  41. package/.vscode/settings.json +0 -69
  42. package/.vscode/tasks.json +0 -12
  43. package/dungeons/customers/.gitkeep +0 -0
  44. package/env.yaml +0 -1
  45. package/lib/cloud-function.js +0 -20
  46. package/scratch.mjs +0 -62
  47. package/scripts/dana.mjs +0 -137
  48. package/scripts/deploy.sh +0 -15
  49. package/scripts/jsdoctest.js +0 -5
  50. package/scripts/new-dungeon.sh +0 -98
  51. package/scripts/new-project.mjs +0 -14
  52. package/scripts/run-index.sh +0 -2
  53. package/scripts/update-deps.sh +0 -5
  54. package/tests/benchmark/concurrency.mjs +0 -52
  55. package/tests/cli.test.js +0 -124
  56. package/tests/coverage/.gitkeep +0 -0
  57. package/tests/e2e.test.js +0 -379
  58. package/tests/int.test.js +0 -715
  59. package/tests/testCases.mjs +0 -229
  60. package/tests/testSoup.mjs +0 -28
  61. package/tests/unit.test.js +0 -910
  62. package/tmp/.gitkeep +0 -0
  63. package/tsconfig.json +0 -18
  64. package/vitest.config.js +0 -47
package/index.js CHANGED
@@ -29,8 +29,6 @@ import { makeMirror } from './lib/generators/mirror.js';
29
29
  import { makeGroupProfile, makeProfile } from './lib/generators/profiles.js';
30
30
 
31
31
  // Utilities
32
- import getCliParams from './lib/cli/cli.js';
33
- import * as u from './lib/utils/utils.js';
34
32
  import { generateLineChart } from './lib/utils/chart.js';
35
33
 
36
34
  // External dependencies
@@ -39,6 +37,8 @@ import utc from "dayjs/plugin/utc.js";
39
37
  import functions from '@google-cloud/functions-framework';
40
38
  import { timer, sLog } from 'ak-tools';
41
39
  import fs, { existsSync } from 'fs';
40
+ import path from 'path';
41
+ import { fileURLToPath } from 'url';
42
42
 
43
43
  // Initialize dayjs and time constants
44
44
  dayjs.extend(utc);
@@ -47,12 +47,65 @@ global.FIXED_NOW = FIXED_NOW;
47
47
  let FIXED_BEGIN = dayjs.unix(FIXED_NOW).subtract(90, 'd').unix();
48
48
  global.FIXED_BEGIN = FIXED_BEGIN;
49
49
 
50
- // Package version
51
- const { version } = JSON.parse(fs.readFileSync('./package.json', 'utf8'));
52
-
53
- // Environment
54
- const { NODE_ENV = "unknown" } = process.env;
55
- const isCLI = process.argv[1].endsWith('index.js') || process.argv[1].endsWith('cli.js');
50
+ /**
51
+ * Display configuration summary for CLI mode
52
+ * @param {Config} config - Validated configuration object
53
+ */
54
+ function displayConfigurationSummary(config) {
55
+ console.log('\nšŸ“‹ Configuration Summary');
56
+ console.log('─'.repeat(40));
57
+
58
+ // Core parameters
59
+ console.log(`šŸŽÆ Target: ${config.numUsers?.toLocaleString()} users, ${config.numEvents?.toLocaleString()} events`);
60
+ console.log(`šŸ“… Timeline: ${config.numDays} days (${config.seed ? config.seed : 'random seed'})`);
61
+ console.log(`šŸ’¾ Output: ${config.format} format${config.region ? ` (${config.region})` : ''}`);
62
+ console.log(`⚔ Performance: ${config.concurrency || 'auto'} threads`);
63
+
64
+ // Feature flags
65
+ const features = [];
66
+ if (config.hasAnonIds) features.push('anonymous IDs');
67
+ if (config.hasSessionIds) features.push('session IDs');
68
+ if (config.alsoInferFunnels) features.push('funnel inference');
69
+ if (config.makeChart) features.push('chart generation');
70
+ if (config.writeToDisk) features.push('disk output');
71
+
72
+ if (features.length > 0) {
73
+ console.log(`šŸ”§ Features: ${features.join(', ')}`);
74
+ }
75
+
76
+ // Schema preview
77
+ if (config.events && config.events.length > 0) {
78
+ console.log('\nšŸŽ­ Event Schema');
79
+ console.log('─'.repeat(40));
80
+ const eventNames = config.events.slice(0, 6).map(e => e.event || e).join(', ');
81
+ const more = config.events.length > 6 ? ` (+${config.events.length - 6} more)` : '';
82
+ console.log(`šŸ“Š Events: ${eventNames}${more}`);
83
+ }
84
+
85
+ // Funnels preview
86
+ if (config.funnels && config.funnels.length > 0) {
87
+ console.log(`šŸ”„ Funnels: ${config.funnels.length} funnel${config.funnels.length > 1 ? 's' : ''} configured`);
88
+ config.funnels.slice(0, 4).forEach((funnel, i) => {
89
+ if (funnel.sequence) {
90
+ const arrow = ' → ';
91
+ const sequence = funnel.sequence.join(arrow);
92
+ const rate = funnel.conversionRate ? ` (${(funnel.conversionRate * 100).toFixed(0)}% conversion)` : '';
93
+ console.log(` ${i + 1}. ${sequence}${rate}`);
94
+ }
95
+ });
96
+ if (config.funnels.length > 4) {
97
+ console.log(` ...and ${config.funnels.length - 4} more funnels`);
98
+ }
99
+ }
100
+
101
+ // Group analytics
102
+ if (config.groupKeys && config.groupKeys.length > 0) {
103
+ const groups = config.groupKeys.map(([key, count]) => `${count} ${key}s`).join(', ');
104
+ console.log(`šŸ‘„ Groups: ${groups}`);
105
+ }
106
+
107
+ console.log(''); // Extra spacing before generation starts
108
+ }
56
109
 
57
110
  /**
58
111
  * Main data generation function
@@ -64,8 +117,9 @@ async function main(config) {
64
117
  jobTimer.start();
65
118
 
66
119
  //cli mode check for positional dungeon config
120
+ const isCLI = config._ && Array.isArray(config._);
67
121
  if (isCLI) {
68
- const firstArg = config._.slice().pop()
122
+ const firstArg = config._.slice().pop();
69
123
  if (firstArg?.endsWith('.js') && existsSync(firstArg)) {
70
124
  if (config.verbose) {
71
125
  console.log(`\nšŸ” Loading dungeon config from: ${firstArg}`);
@@ -78,27 +132,36 @@ async function main(config) {
78
132
  throw error;
79
133
  }
80
134
  }
81
-
135
+
82
136
  }
83
137
 
138
+ if (config.verbose) console.log(`\nšŸ”§ Configuring dungeon with seed: ${config.seed}`);
84
139
  let validatedConfig;
85
140
  try {
86
141
  // Step 1: Validate and enrich configuration
87
142
  validatedConfig = validateDungeonConfig(config);
143
+
144
+ // Step 1.5: Display configuration summary (CLI mode only)
145
+ if (isCLI && validatedConfig.verbose) {
146
+ displayConfigurationSummary(validatedConfig);
147
+ }
88
148
 
89
149
  // Step 2: Create context with validated config
90
- const context = createContext(validatedConfig);
150
+ const context = createContext(validatedConfig, null, isCLI);
91
151
 
92
152
  // Step 3: Initialize storage containers
93
153
  const storageManager = new StorageManager(context);
94
154
  const storage = await storageManager.initializeContainers();
95
155
  updateContextWithStorage(context, storage);
96
156
 
157
+ // ! DATA GENERATION STARTS HERE
158
+
97
159
  // Step 4: Generate ad spend data (if enabled)
98
160
  if (validatedConfig.hasAdSpend) {
99
161
  await generateAdSpendData(context);
100
162
  }
101
163
 
164
+ if (context.config.verbose) console.log(`\nšŸ”„ Starting user and event generation...\n`);
102
165
  // Step 5: Main user and event generation
103
166
  await userLoop(context);
104
167
 
@@ -122,6 +185,10 @@ async function main(config) {
122
185
  await makeMirror(context);
123
186
  }
124
187
 
188
+ if (context.config.verbose) console.log(`\nāœ… Data generation completed successfully!\n`);
189
+
190
+ // ! DATA GENERATION ENDS HERE
191
+
125
192
  // Step 10: Generate charts (if enabled)
126
193
  if (validatedConfig.makeChart) {
127
194
  await generateCharts(context);
@@ -155,11 +222,9 @@ async function main(config) {
155
222
  };
156
223
 
157
224
  } catch (error) {
158
- if (isCLI || validatedConfig.verbose) {
225
+ if (validatedConfig?.verbose) {
159
226
  console.error(`\nāŒ Error: ${error.message}\n`);
160
- if (validatedConfig.verbose) {
161
- console.error(error.stack);
162
- }
227
+ console.error(error.stack);
163
228
  } else {
164
229
  sLog("Main execution error", { error: error.message, stack: error.stack }, "ERROR");
165
230
  }
@@ -195,7 +260,7 @@ async function generateGroupProfiles(context) {
195
260
  const { config, storage } = context;
196
261
  const { groupKeys, groupProps = {} } = config;
197
262
 
198
- if (isCLI || config.verbose) {
263
+ if (context.isCLI() || config.verbose) {
199
264
  console.log('\nšŸ‘„ Generating group profiles...');
200
265
  }
201
266
 
@@ -208,7 +273,7 @@ async function generateGroupProfiles(context) {
208
273
  continue;
209
274
  }
210
275
 
211
- if (isCLI || config.verbose) {
276
+ if (context.isCLI() || config.verbose) {
212
277
  console.log(` Creating ${groupCount.toLocaleString()} ${groupKey} profiles...`);
213
278
  }
214
279
 
@@ -224,7 +289,7 @@ async function generateGroupProfiles(context) {
224
289
  }
225
290
  }
226
291
 
227
- if (isCLI || config.verbose) {
292
+ if (context.isCLI() || config.verbose) {
228
293
  console.log('āœ… Group profiles generated successfully');
229
294
  }
230
295
  }
@@ -237,7 +302,7 @@ async function generateLookupTables(context) {
237
302
  const { config, storage } = context;
238
303
  const { lookupTables } = config;
239
304
 
240
- if (isCLI || config.verbose) {
305
+ if (context.isCLI() || config.verbose) {
241
306
  console.log('\nšŸ” Generating lookup tables...');
242
307
  }
243
308
 
@@ -251,7 +316,7 @@ async function generateLookupTables(context) {
251
316
  continue;
252
317
  }
253
318
 
254
- if (isCLI || config.verbose) {
319
+ if (context.isCLI() || config.verbose) {
255
320
  console.log(` Creating ${entries.toLocaleString()} ${key} lookup entries...`);
256
321
  }
257
322
 
@@ -264,7 +329,7 @@ async function generateLookupTables(context) {
264
329
  }
265
330
  }
266
331
 
267
- if (isCLI || config.verbose) {
332
+ if (context.isCLI() || config.verbose) {
268
333
  console.log('āœ… Lookup tables generated successfully');
269
334
  }
270
335
  }
@@ -277,7 +342,7 @@ async function generateGroupSCDs(context) {
277
342
  const { config, storage } = context;
278
343
  const { scdProps, groupKeys } = config;
279
344
 
280
- if (isCLI || config.verbose) {
345
+ if (context.isCLI() || config.verbose) {
281
346
  console.log('\nšŸ“Š Generating group SCDs...');
282
347
  }
283
348
 
@@ -300,7 +365,7 @@ async function generateGroupSCDs(context) {
300
365
  continue; // No SCDs for this group type
301
366
  }
302
367
 
303
- if (isCLI || config.verbose) {
368
+ if (context.isCLI() || config.verbose) {
304
369
  console.log(` Generating SCDs for ${groupCount.toLocaleString()} ${groupKey} entities...`);
305
370
  }
306
371
 
@@ -342,7 +407,7 @@ async function generateGroupSCDs(context) {
342
407
  }
343
408
  }
344
409
 
345
- if (isCLI || config.verbose) {
410
+ if (context.isCLI() || config.verbose) {
346
411
  console.log('āœ… Group SCDs generated successfully');
347
412
  }
348
413
  }
@@ -357,11 +422,11 @@ async function generateCharts(context) {
357
422
  if (config.makeChart && storage.eventData?.length > 0) {
358
423
  const chartPath = typeof config.makeChart === 'string'
359
424
  ? config.makeChart
360
- : `./charts/${config.simulationName}-timeline.png`;
425
+ : `./${config.simulationName}-timeline`;
361
426
 
362
427
  await generateLineChart(storage.eventData, undefined, chartPath);
363
428
 
364
- if (isCLI || config.verbose) {
429
+ if (context.isCLI() || config.verbose) {
365
430
  console.log(`šŸ“Š Chart generated: ${chartPath}`);
366
431
  } else {
367
432
  sLog("Chart generated", { path: chartPath });
@@ -375,7 +440,7 @@ async function generateCharts(context) {
375
440
  * @param {import('./types').Dungeon} config - Configuration object
376
441
  */
377
442
  async function flushStorageToDisk(storage, config) {
378
- if (isCLI || config.verbose) {
443
+ if (config.verbose) {
379
444
  console.log('\nšŸ’¾ Writing data to disk...');
380
445
  }
381
446
 
@@ -399,8 +464,8 @@ async function flushStorageToDisk(storage, config) {
399
464
 
400
465
  await Promise.all(flushPromises);
401
466
 
402
- if (isCLI || config.verbose) {
403
- console.log('āœ… Data flushed to disk successfully');
467
+ if (config.verbose) {
468
+ console.log('šŸ™ Data flushed to disk successfully');
404
469
  }
405
470
  }
406
471
 
@@ -445,44 +510,6 @@ function extractStorageData(storage) {
445
510
  };
446
511
  }
447
512
 
448
- // CLI execution
449
- if (isCLI) {
450
- (async () => {
451
- const cliConfig = getCliParams();
452
-
453
- // Load dungeon config if --complex or --simple flags are used
454
- let finalConfig = cliConfig;
455
- if (cliConfig.complex) {
456
- const complexConfig = await import('./dungeons/complex.js');
457
- finalConfig = { ...complexConfig.default, ...cliConfig };
458
- } else if (cliConfig.simple) {
459
- const simpleConfig = await import('./dungeons/simple.js');
460
- finalConfig = { ...simpleConfig.default, ...cliConfig };
461
- }
462
-
463
- main(finalConfig)
464
- .then(result => {
465
- console.log(`šŸ“Š Generated ${(result.eventCount || 0).toLocaleString()} events for ${(result.userCount || 0).toLocaleString()} users`);
466
- console.log(`ā±ļø Total time: ${result.time?.human || 'unknown'}`);
467
- if (result.files?.length) {
468
- console.log(`šŸ“ Files written: ${result.files.length}`);
469
- if (cliConfig.verbose) {
470
- result.files.forEach(file => console.log(` ${file}`));
471
- }
472
- }
473
- console.log(`\nāœ… Job completed successfully!`);
474
- process.exit(0);
475
- })
476
- .catch(error => {
477
- console.error(`\nāŒ Job failed: ${error.message}`);
478
- if (cliConfig.verbose) {
479
- console.error(error.stack);
480
- }
481
- process.exit(1);
482
- });
483
- })();
484
- }
485
-
486
513
  // Cloud Functions setup
487
514
  functions.http('entry', async (req, res) => {
488
515
  await handleCloudFunctionEntry(req, res, main);
@@ -494,4 +521,5 @@ export default main;
494
521
  // CommonJS compatibility
495
522
  if (typeof module !== 'undefined' && module.exports) {
496
523
  module.exports = main;
497
- }
524
+ }
525
+
package/lib/cli/cli.js CHANGED
@@ -28,9 +28,10 @@ function cliParams() {
28
28
  .usage(`\nusage:\nnpx $0 [dataModel.js] [options]
29
29
 
30
30
  examples:
31
- npx $0
32
- npx $0 --token 1234 --u 100 --e 1000 --d 7 --w false
33
- npx $0 myDataConfig.js
31
+ npx $0 (uses simple data model by default)
32
+ npx $0 --complex (full enterprise data model)
33
+ npx $0 --token 1234 --u 100 --e 1000 (send data to mixpanel)
34
+ npx $0 myDataConfig.js (use custom config)
34
35
 
35
36
  DOCS: https://github.com/ak--47/make-mp-data
36
37
  DATA MODEL: https://github.com/ak--47/make-mp-data/blob/main/default.js
@@ -99,7 +100,7 @@ DATA MODEL: https://github.com/ak--47/make-mp-data/blob/main/default.js
99
100
  })
100
101
  .option('concurrency', {
101
102
  alias: 'conn',
102
- default: 500,
103
+ default: 10,
103
104
  demandOption: false,
104
105
  describe: 'concurrency level for data generation',
105
106
  type: 'number'
@@ -3,14 +3,20 @@
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
- import { makeName, clone } from "ak-tools";
12
+ import { makeName } from "ak-tools";
8
13
  import * as u from "../utils/utils.js";
14
+ import os from "os";
9
15
 
10
16
  /**
11
17
  * Infers funnels from the provided events
12
- * @param {Array} events - Array of event configurations
13
- * @returns {Array} Array of inferred funnel configurations
18
+ * @param {EventConfig[]} events - Array of event configurations
19
+ * @returns {Funnel[]} Array of inferred funnel configurations
14
20
  */
15
21
  function inferFunnels(events) {
16
22
  const createdFunnels = [];
@@ -34,7 +40,7 @@ function inferFunnels(events) {
34
40
  if (firstEvents.length) {
35
41
  for (const event of firstEvents) {
36
42
  createdFunnels.push({
37
- ...clone(funnelTemplate),
43
+ ...u.deepClone(funnelTemplate),
38
44
  sequence: [event],
39
45
  isFirstFunnel: true,
40
46
  conversionRate: 100
@@ -43,12 +49,12 @@ function inferFunnels(events) {
43
49
  }
44
50
 
45
51
  // At least one funnel with all usage events
46
- createdFunnels.push({ ...clone(funnelTemplate), sequence: usageEvents });
52
+ createdFunnels.push({ ...u.deepClone(funnelTemplate), sequence: usageEvents });
47
53
 
48
54
  // Create random funnels for the rest
49
55
  for (let i = 1; i < numFunnelsToCreate; i++) {
50
56
  /** @type {import('../../types.js').Funnel} */
51
- const funnel = { ...clone(funnelTemplate) };
57
+ const funnel = { ...u.deepClone(funnelTemplate) };
52
58
  funnel.conversionRate = u.integer(25, 75);
53
59
  funnel.timeToConvert = u.integer(1, 10);
54
60
  funnel.weight = u.integer(1, 10);
@@ -63,8 +69,8 @@ function inferFunnels(events) {
63
69
 
64
70
  /**
65
71
  * Validates and enriches a dungeon configuration object
66
- * @param {Object} config - Raw configuration object
67
- * @returns {Object} Validated and enriched configuration
72
+ * @param {Partial<Dungeon>} config - Raw configuration object
73
+ * @returns {Dungeon} Validated and enriched configuration
68
74
  */
69
75
  export function validateDungeonConfig(config) {
70
76
  const chance = u.getChance();
@@ -94,7 +100,7 @@ export function validateDungeonConfig(config) {
94
100
  token = null,
95
101
  region = "US",
96
102
  writeToDisk = false,
97
- verbose = false,
103
+ verbose = true,
98
104
  makeChart = false,
99
105
  soup = {},
100
106
  hook = (record) => record,
@@ -110,9 +116,14 @@ export function validateDungeonConfig(config) {
110
116
  alsoInferFunnels = false,
111
117
  name = "",
112
118
  batchSize = 500_000,
113
- concurrency = 500
119
+ concurrency
114
120
  } = config;
115
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
+
116
127
  // Ensure defaults for deep objects
117
128
  if (!config.superProps) config.superProps = superProps;
118
129
  if (!config.userProps || Object.keys(config?.userProps || {})) config.userProps = userProps;
@@ -132,9 +143,9 @@ export function validateDungeonConfig(config) {
132
143
  // Validate events
133
144
  if (!events || !events.length) events = [{ event: "foo" }, { event: "bar" }, { event: "baz" }];
134
145
 
135
- // Convert string events to objects
146
+ // Convert string events to objects
136
147
  if (typeof events[0] === "string") {
137
- events = events.map(e => ({ event: e }));
148
+ events = events.map(e => ({ event: /** @type {string} */ (e) }));
138
149
  }
139
150
 
140
151
  // Handle funnel inference
@@ -232,6 +243,11 @@ export function validateDungeonConfig(config) {
232
243
  * @param {Object} config - Configuration to validate
233
244
  * @throws {Error} If required fields are missing
234
245
  */
246
+ /**
247
+ * Validates required configuration parameters
248
+ * @param {Dungeon} config - Configuration object to validate
249
+ * @returns {boolean} True if validation passes
250
+ */
235
251
  export function validateRequiredConfig(config) {
236
252
  if (!config) {
237
253
  throw new Error("Configuration is required");