make-mp-data 1.5.55 → 2.0.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.
Files changed (56) hide show
  1. package/.claude/settings.local.json +20 -0
  2. package/.gcloudignore +2 -1
  3. package/.vscode/launch.json +6 -3
  4. package/.vscode/settings.json +31 -2
  5. package/dungeons/media.js +371 -0
  6. package/index.js +354 -1757
  7. package/{components → lib/cli}/cli.js +21 -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 +19 -12
  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 +76 -18
  37. package/vitest.config.js +47 -0
  38. package/dungeons/adspend.js +0 -96
  39. package/dungeons/anon.js +0 -104
  40. package/dungeons/big.js +0 -224
  41. package/dungeons/business.js +0 -327
  42. package/dungeons/complex.js +0 -396
  43. package/dungeons/foobar.js +0 -241
  44. package/dungeons/funnels.js +0 -220
  45. package/dungeons/gaming-experiments.js +0 -323
  46. package/dungeons/gaming.js +0 -314
  47. package/dungeons/governance.js +0 -288
  48. package/dungeons/mirror.js +0 -129
  49. package/dungeons/sanity.js +0 -118
  50. package/dungeons/scd.js +0 -205
  51. package/dungeons/session-replay.js +0 -175
  52. package/dungeons/simple.js +0 -150
  53. package/dungeons/userAgent.js +0 -190
  54. package/log.json +0 -1067
  55. package/tests/jest.config.js +0 -47
  56. /package/{components → lib/utils}/prompt.txt +0 -0
@@ -1,5 +1,9 @@
1
- const yargs = require('yargs');
2
- const { version } = require('../package.json');
1
+ import yargs from 'yargs';
2
+ import fs from 'fs';
3
+
4
+ /** @typedef {import('../../types').Dungeon} Config */
5
+
6
+ const { version } = JSON.parse(fs.readFileSync('package.json', 'utf8'));
3
7
 
4
8
  const hero = String.raw`
5
9
 
@@ -47,7 +51,7 @@ DATA MODEL: https://github.com/ak--47/make-mp-data/blob/main/default.js
47
51
  type: 'string'
48
52
  })
49
53
  .option("format", {
50
- demandOption: false,
54
+ demandOption: false,
51
55
  alias: 'f',
52
56
  describe: 'csv or json',
53
57
  type: 'string'
@@ -104,6 +108,14 @@ DATA MODEL: https://github.com/ak--47/make-mp-data/blob/main/default.js
104
108
  type: 'boolean',
105
109
  coerce: boolCoerce
106
110
  })
111
+ .options("simple", {
112
+ demandOption: false,
113
+ default: false,
114
+ describe: 'use simple data model (basic events and users)',
115
+ alias: 'simp',
116
+ type: 'boolean',
117
+ coerce: boolCoerce
118
+ })
107
119
  .option("writeToDisk", {
108
120
  demandOption: false,
109
121
  default: true,
@@ -141,7 +153,7 @@ DATA MODEL: https://github.com/ak--47/make-mp-data/blob/main/default.js
141
153
  describe: 'create a PNG chart from data',
142
154
  type: 'boolean',
143
155
  coerce: boolCoerce
144
- })
156
+ })
145
157
  .option("hasAdSpend", {
146
158
  alias: 'ads',
147
159
  demandOption: false,
@@ -198,11 +210,14 @@ DATA MODEL: https://github.com/ak--47/make-mp-data/blob/main/default.js
198
210
  type: 'boolean',
199
211
  coerce: boolCoerce
200
212
  })
201
-
213
+
202
214
  .help()
203
215
  .wrap(null)
204
216
  .argv;
205
217
 
218
+ //cli is always verbose mode:
219
+ args.verbose = true;
220
+
206
221
  return args;
207
222
 
208
223
  }
@@ -218,4 +233,4 @@ function boolCoerce(value, foo) {
218
233
  }
219
234
 
220
235
 
221
- module.exports = cliParams;
236
+ 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
+ }