make-mp-data 1.5.56 → 2.0.1

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 (40) hide show
  1. package/.claude/settings.local.json +21 -0
  2. package/.gcloudignore +2 -1
  3. package/.vscode/launch.json +6 -17
  4. package/.vscode/settings.json +31 -2
  5. package/dungeons/media.js +371 -0
  6. package/index.js +353 -1766
  7. package/{components → lib/cli}/cli.js +25 -6
  8. package/lib/cloud-function.js +20 -0
  9. package/lib/core/config-validator.js +248 -0
  10. package/lib/core/context.js +180 -0
  11. package/lib/core/storage.js +268 -0
  12. package/{components → lib/data}/defaults.js +17 -14
  13. package/lib/generators/adspend.js +133 -0
  14. package/lib/generators/events.js +242 -0
  15. package/lib/generators/funnels.js +330 -0
  16. package/lib/generators/mirror.js +168 -0
  17. package/lib/generators/profiles.js +93 -0
  18. package/lib/generators/scd.js +102 -0
  19. package/lib/orchestrators/mixpanel-sender.js +222 -0
  20. package/lib/orchestrators/user-loop.js +194 -0
  21. package/lib/orchestrators/worker-manager.js +200 -0
  22. package/{components → lib/utils}/ai.js +8 -36
  23. package/{components → lib/utils}/chart.js +9 -9
  24. package/{components → lib/utils}/project.js +4 -4
  25. package/{components → lib/utils}/utils.js +35 -23
  26. package/package.json +15 -15
  27. package/scripts/dana.mjs +137 -0
  28. package/scripts/new-dungeon.sh +7 -6
  29. package/scripts/update-deps.sh +2 -1
  30. package/tests/cli.test.js +28 -25
  31. package/tests/e2e.test.js +38 -36
  32. package/tests/int.test.js +151 -56
  33. package/tests/testSoup.mjs +1 -1
  34. package/tests/unit.test.js +15 -14
  35. package/tsconfig.json +1 -1
  36. package/types.d.ts +68 -11
  37. package/vitest.config.js +47 -0
  38. package/log.json +0 -1678
  39. package/tests/jest.config.js +0 -47
  40. /package/{components → lib/utils}/prompt.txt +0 -0
@@ -1,5 +1,13 @@
1
- const yargs = require('yargs');
2
- const { version } = require('../package.json');
1
+ import yargs from 'yargs';
2
+ import fs from 'fs';
3
+ import path from 'path';
4
+ import { fileURLToPath } from 'url';
5
+
6
+ /** @typedef {import('../../types').Dungeon} Config */
7
+
8
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
9
+ const packageJsonPath = path.join(__dirname, '../../package.json');
10
+ const { version } = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'));
3
11
 
4
12
  const hero = String.raw`
5
13
 
@@ -47,7 +55,7 @@ DATA MODEL: https://github.com/ak--47/make-mp-data/blob/main/default.js
47
55
  type: 'string'
48
56
  })
49
57
  .option("format", {
50
- demandOption: false,
58
+ demandOption: false,
51
59
  alias: 'f',
52
60
  describe: 'csv or json',
53
61
  type: 'string'
@@ -104,6 +112,14 @@ DATA MODEL: https://github.com/ak--47/make-mp-data/blob/main/default.js
104
112
  type: 'boolean',
105
113
  coerce: boolCoerce
106
114
  })
115
+ .options("simple", {
116
+ demandOption: false,
117
+ default: false,
118
+ describe: 'use simple data model (basic events and users)',
119
+ alias: 'simp',
120
+ type: 'boolean',
121
+ coerce: boolCoerce
122
+ })
107
123
  .option("writeToDisk", {
108
124
  demandOption: false,
109
125
  default: true,
@@ -141,7 +157,7 @@ DATA MODEL: https://github.com/ak--47/make-mp-data/blob/main/default.js
141
157
  describe: 'create a PNG chart from data',
142
158
  type: 'boolean',
143
159
  coerce: boolCoerce
144
- })
160
+ })
145
161
  .option("hasAdSpend", {
146
162
  alias: 'ads',
147
163
  demandOption: false,
@@ -198,11 +214,14 @@ DATA MODEL: https://github.com/ak--47/make-mp-data/blob/main/default.js
198
214
  type: 'boolean',
199
215
  coerce: boolCoerce
200
216
  })
201
-
217
+
202
218
  .help()
203
219
  .wrap(null)
204
220
  .argv;
205
221
 
222
+ //cli is always verbose mode:
223
+ args.verbose = true;
224
+
206
225
  return args;
207
226
 
208
227
  }
@@ -218,4 +237,4 @@ function boolCoerce(value, foo) {
218
237
  }
219
238
 
220
239
 
221
- module.exports = cliParams;
240
+ export default cliParams;
@@ -0,0 +1,20 @@
1
+ /**
2
+ * Cloud Function Entry Point
3
+ * Provides a clean interface for Google Cloud Functions deployment
4
+ */
5
+
6
+ /** @typedef {import('../types').Dungeon} Config */
7
+
8
+ import functions from '@google-cloud/functions-framework';
9
+ import { handleCloudFunctionEntry } from './orchestrators/worker-manager.js';
10
+ import main from '../index.js';
11
+
12
+ /**
13
+ * Cloud Function HTTP entry point
14
+ * Handles distributed data generation across multiple workers
15
+ */
16
+ functions.http('entry', async (req, res) => {
17
+ await handleCloudFunctionEntry(req, res, main);
18
+ });
19
+
20
+ export { handleCloudFunctionEntry, main };
@@ -0,0 +1,248 @@
1
+ /**
2
+ * Configuration validation and enrichment module
3
+ * Extracted from index.js validateDungeonConfig function
4
+ */
5
+
6
+ import dayjs from "dayjs";
7
+ import { makeName, clone } from "ak-tools";
8
+ import * as u from "../utils/utils.js";
9
+
10
+ /**
11
+ * Infers funnels from the provided events
12
+ * @param {Array} events - Array of event configurations
13
+ * @returns {Array} Array of inferred funnel configurations
14
+ */
15
+ function inferFunnels(events) {
16
+ const createdFunnels = [];
17
+ const firstEvents = events.filter((e) => e.isFirstEvent).map((e) => e.event);
18
+ const usageEvents = events.filter((e) => !e.isFirstEvent).map((e) => e.event);
19
+ const numFunnelsToCreate = Math.ceil(usageEvents.length);
20
+
21
+ /** @type {import('../../types.js').Funnel} */
22
+ const funnelTemplate = {
23
+ sequence: [],
24
+ conversionRate: 50,
25
+ order: 'sequential',
26
+ requireRepeats: false,
27
+ props: {},
28
+ timeToConvert: 1,
29
+ isFirstFunnel: false,
30
+ weight: 1
31
+ };
32
+
33
+ // Create funnels for first events
34
+ if (firstEvents.length) {
35
+ for (const event of firstEvents) {
36
+ createdFunnels.push({
37
+ ...clone(funnelTemplate),
38
+ sequence: [event],
39
+ isFirstFunnel: true,
40
+ conversionRate: 100
41
+ });
42
+ }
43
+ }
44
+
45
+ // At least one funnel with all usage events
46
+ createdFunnels.push({ ...clone(funnelTemplate), sequence: usageEvents });
47
+
48
+ // Create random funnels for the rest
49
+ for (let i = 1; i < numFunnelsToCreate; i++) {
50
+ /** @type {import('../../types.js').Funnel} */
51
+ const funnel = { ...clone(funnelTemplate) };
52
+ funnel.conversionRate = u.integer(25, 75);
53
+ funnel.timeToConvert = u.integer(1, 10);
54
+ funnel.weight = u.integer(1, 10);
55
+ const sequence = u.shuffleArray(usageEvents).slice(0, u.integer(2, usageEvents.length));
56
+ funnel.sequence = sequence;
57
+ funnel.order = 'random';
58
+ createdFunnels.push(funnel);
59
+ }
60
+
61
+ return createdFunnels;
62
+ }
63
+
64
+ /**
65
+ * Validates and enriches a dungeon configuration object
66
+ * @param {Object} config - Raw configuration object
67
+ * @returns {Object} Validated and enriched configuration
68
+ */
69
+ export function validateDungeonConfig(config) {
70
+ const chance = u.getChance();
71
+
72
+ // Extract configuration with defaults
73
+ let {
74
+ seed,
75
+ numEvents = 100_000,
76
+ numUsers = 1000,
77
+ numDays = 30,
78
+ epochStart = 0,
79
+ epochEnd = dayjs().unix(),
80
+ events = [{ event: "foo" }, { event: "bar" }, { event: "baz" }],
81
+ superProps = { luckyNumber: [2, 2, 4, 4, 42, 42, 42, 2, 2, 4, 4, 42, 42, 42, 420] },
82
+ funnels = [],
83
+ userProps = {
84
+ spiritAnimal: chance.animal.bind(chance),
85
+ },
86
+ scdProps = {},
87
+ mirrorProps = {},
88
+ groupKeys = [],
89
+ groupProps = {},
90
+ lookupTables = [],
91
+ hasAnonIds = false,
92
+ hasSessionIds = false,
93
+ format = "csv",
94
+ token = null,
95
+ region = "US",
96
+ writeToDisk = false,
97
+ verbose = false,
98
+ makeChart = false,
99
+ soup = {},
100
+ hook = (record) => record,
101
+ hasAdSpend = false,
102
+ hasCampaigns = false,
103
+ hasLocation = false,
104
+ hasAvatar = false,
105
+ isAnonymous = false,
106
+ hasBrowser = false,
107
+ hasAndroidDevices = false,
108
+ hasDesktopDevices = false,
109
+ hasIOSDevices = false,
110
+ alsoInferFunnels = false,
111
+ name = "",
112
+ batchSize = 500_000,
113
+ concurrency = 500
114
+ } = config;
115
+
116
+ // Ensure defaults for deep objects
117
+ if (!config.superProps) config.superProps = superProps;
118
+ if (!config.userProps || Object.keys(config?.userProps || {})) config.userProps = userProps;
119
+
120
+ // Setting up "TIME"
121
+ if (epochStart && !numDays) numDays = dayjs.unix(epochEnd).diff(dayjs.unix(epochStart), "day");
122
+ if (!epochStart && numDays) epochStart = dayjs.unix(epochEnd).subtract(numDays, "day").unix();
123
+ if (epochStart && numDays) { } // noop
124
+ if (!epochStart && !numDays) {
125
+ throw new Error("Either epochStart or numDays must be provided");
126
+ }
127
+
128
+ // Generate simulation name
129
+ config.simulationName = name || makeName();
130
+ config.name = config.simulationName;
131
+
132
+ // Validate events
133
+ if (!events || !events.length) events = [{ event: "foo" }, { event: "bar" }, { event: "baz" }];
134
+
135
+ // Convert string events to objects
136
+ if (typeof events[0] === "string") {
137
+ events = events.map(e => ({ event: e }));
138
+ }
139
+
140
+ // Handle funnel inference
141
+ if (alsoInferFunnels) {
142
+ const inferredFunnels = inferFunnels(events);
143
+ funnels = [...funnels, ...inferredFunnels];
144
+ }
145
+
146
+ // Create funnel for events not in other funnels
147
+ const eventContainedInFunnels = Array.from(funnels.reduce((acc, f) => {
148
+ const events = f.sequence;
149
+ events.forEach(event => acc.add(event));
150
+ return acc;
151
+ }, new Set()));
152
+
153
+ const eventsNotInFunnels = events
154
+ .filter(e => !e.isFirstEvent)
155
+ .filter(e => !eventContainedInFunnels.includes(e.event))
156
+ .map(e => e.event);
157
+
158
+ if (eventsNotInFunnels.length) {
159
+ const sequence = u.shuffleArray(eventsNotInFunnels.flatMap(event => {
160
+ let evWeight;
161
+ // First check the config
162
+ if (config.events) {
163
+ evWeight = config.events.find(e => e.event === event)?.weight || 1;
164
+ }
165
+ // Fallback on default
166
+ else {
167
+ evWeight = 1;
168
+ }
169
+ return Array(evWeight).fill(event);
170
+ }));
171
+
172
+ funnels.push({
173
+ sequence,
174
+ conversionRate: 50,
175
+ order: 'random',
176
+ timeToConvert: 24 * 14,
177
+ requireRepeats: false,
178
+ });
179
+ }
180
+
181
+ // Event validation
182
+ const validatedEvents = u.validateEventConfig(events);
183
+
184
+ // Build final config object
185
+ const validatedConfig = {
186
+ ...config,
187
+ concurrency,
188
+ funnels,
189
+ batchSize,
190
+ seed,
191
+ numEvents,
192
+ numUsers,
193
+ numDays,
194
+ epochStart,
195
+ epochEnd,
196
+ events: validatedEvents,
197
+ superProps,
198
+ userProps,
199
+ scdProps,
200
+ mirrorProps,
201
+ groupKeys,
202
+ groupProps,
203
+ lookupTables,
204
+ hasAnonIds,
205
+ hasSessionIds,
206
+ format,
207
+ token,
208
+ region,
209
+ writeToDisk,
210
+ verbose,
211
+ makeChart,
212
+ soup,
213
+ hook,
214
+ hasAdSpend,
215
+ hasCampaigns,
216
+ hasLocation,
217
+ hasAvatar,
218
+ isAnonymous,
219
+ hasBrowser,
220
+ hasAndroidDevices,
221
+ hasDesktopDevices,
222
+ hasIOSDevices,
223
+ simulationName: config.simulationName,
224
+ name: config.name
225
+ };
226
+
227
+ return validatedConfig;
228
+ }
229
+
230
+ /**
231
+ * Validates configuration for required fields
232
+ * @param {Object} config - Configuration to validate
233
+ * @throws {Error} If required fields are missing
234
+ */
235
+ export function validateRequiredConfig(config) {
236
+ if (!config) {
237
+ throw new Error("Configuration is required");
238
+ }
239
+
240
+ if (typeof config !== 'object') {
241
+ throw new Error("Configuration must be an object");
242
+ }
243
+
244
+ // Could add more specific validation here
245
+ return true;
246
+ }
247
+
248
+ export { inferFunnels };
@@ -0,0 +1,180 @@
1
+ /**
2
+ * Context module - replaces global variables with a context object
3
+ * Provides centralized state management and dependency injection
4
+ */
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 */
11
+
12
+ import dayjs from "dayjs";
13
+ import { campaigns, devices, locations } from '../data/defaults.js';
14
+ import * as u from '../utils/utils.js';
15
+
16
+ /**
17
+ * Creates a defaults factory function that computes weighted defaults
18
+ * @param {Dungeon} config - Configuration object
19
+ * @param {Array} campaignData - Campaign data array
20
+ * @returns {Defaults} Defaults object with factory functions
21
+ */
22
+ function createDefaults(config, campaignData) {
23
+ const { singleCountry } = config;
24
+
25
+ // Pre-compute weighted arrays based on configuration
26
+ const locationsUsers = singleCountry ?
27
+ locations.filter(l => l.country === singleCountry) :
28
+ locations;
29
+
30
+ const locationsEvents = singleCountry ?
31
+ locations.filter(l => l.country === singleCountry) :
32
+ locations;
33
+
34
+ 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)
42
+ };
43
+ }
44
+
45
+ /**
46
+ * Creates a runtime state object for tracking execution state
47
+ * @returns {RuntimeState} Runtime state with counters and flags
48
+ */
49
+ function createRuntimeState() {
50
+ return {
51
+ operations: 0,
52
+ eventCount: 0,
53
+ userCount: 0,
54
+ isBatchMode: false,
55
+ verbose: false,
56
+ isCLI: false
57
+ };
58
+ }
59
+
60
+ /**
61
+ * Context factory that creates a complete context object for data generation
62
+ * @param {Dungeon} config - Validated configuration object
63
+ * @param {Storage|null} storage - Storage containers (optional, can be set later)
64
+ * @returns {Context} Context object containing all state and dependencies
65
+ */
66
+ export function createContext(config, storage = null) {
67
+ // Import campaign data (could be made configurable)
68
+ const campaignData = campaigns;
69
+
70
+ // Create computed defaults based on config
71
+ const defaults = createDefaults(config, campaignData);
72
+
73
+ // Create runtime state
74
+ const runtime = createRuntimeState();
75
+
76
+ // Set runtime flags from config
77
+ runtime.verbose = config.verbose || false;
78
+ runtime.isBatchMode = config.batchSize && config.batchSize < config.numEvents;
79
+ runtime.isCLI = process.argv[1].endsWith('index.js') || process.argv[1].endsWith('cli.js');
80
+
81
+ const context = {
82
+ config,
83
+ storage,
84
+ defaults,
85
+ campaigns: campaignData,
86
+ runtime,
87
+
88
+ // Helper methods for updating state
89
+ incrementOperations() {
90
+ runtime.operations++;
91
+ },
92
+
93
+ incrementEvents() {
94
+ runtime.eventCount++;
95
+ },
96
+
97
+ incrementUsers() {
98
+ runtime.userCount++;
99
+ },
100
+
101
+ setStorage(storageObj) {
102
+ this.storage = storageObj;
103
+ },
104
+
105
+ // Getter methods for runtime state
106
+ getOperations() {
107
+ return runtime.operations;
108
+ },
109
+
110
+ getEventCount() {
111
+ return runtime.eventCount;
112
+ },
113
+
114
+ getUserCount() {
115
+ return runtime.userCount;
116
+ },
117
+
118
+ incrementUserCount() {
119
+ runtime.userCount++;
120
+ },
121
+
122
+ incrementEventCount() {
123
+ runtime.eventCount++;
124
+ },
125
+
126
+ isBatchMode() {
127
+ return runtime.isBatchMode;
128
+ },
129
+
130
+ isCLI() {
131
+ return runtime.isCLI;
132
+ },
133
+
134
+ // Time helper methods
135
+ getTimeShift() {
136
+ const actualNow = dayjs().add(2, "day");
137
+ return actualNow.diff(dayjs.unix(global.FIXED_NOW), "seconds");
138
+ },
139
+
140
+ getDaysShift() {
141
+ const actualNow = dayjs().add(2, "day");
142
+ return actualNow.diff(dayjs.unix(global.FIXED_NOW), "days");
143
+ },
144
+
145
+ // Time constants (previously globals)
146
+ FIXED_NOW: global.FIXED_NOW,
147
+ FIXED_BEGIN: global.FIXED_BEGIN
148
+ };
149
+
150
+ return context;
151
+ }
152
+
153
+ /**
154
+ * Updates an existing context with new storage containers
155
+ * @param {Context} context - Existing context object
156
+ * @param {Storage} storage - New storage containers
157
+ * @returns {Context} Updated context object
158
+ */
159
+ export function updateContextWithStorage(context, storage) {
160
+ context.storage = storage;
161
+ return context;
162
+ }
163
+
164
+ /**
165
+ * Validates that a context object has all required properties
166
+ * @param {Context} context - Context to validate
167
+ * @throws {Error} If context is missing required properties
168
+ */
169
+ export function validateContext(context) {
170
+ const required = ['config', 'defaults', 'campaigns', 'runtime'];
171
+ const missing = required.filter(prop => !context[prop]);
172
+
173
+ if (missing.length > 0) {
174
+ throw new Error(`Context is missing required properties: ${missing.join(', ')}`);
175
+ }
176
+
177
+ if (!context.config.numUsers || !context.config.numEvents) {
178
+ throw new Error('Context config must have numUsers and numEvents');
179
+ }
180
+ }