make-mp-data 2.0.18 → 2.0.21
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.
- package/dungeons/big.js +7 -6
- package/dungeons/business.js +21 -3
- package/dungeons/complex.js +1 -1
- package/dungeons/experiments.js +8 -7
- package/dungeons/media.js +7 -7
- package/dungeons/sanity.js +8 -13
- package/dungeons/simple.js +2 -0
- package/dungeons/student-teacher.js +475 -0
- package/dungeons/userAgent.js +7 -7
- package/entry.js +13 -2
- package/index.js +74 -3
- package/lib/cli/cli.js +1 -1
- package/lib/core/config-validator.js +22 -7
- package/lib/core/context.js +31 -16
- package/lib/core/storage.js +20 -19
- package/lib/generators/events.js +41 -18
- package/lib/orchestrators/worker-manager.js +5 -2
- package/lib/templates/abbreviated.d.ts +158 -0
- package/lib/{data → templates}/defaults.js +2 -2
- package/lib/templates/dungeon-template.js +110 -0
- package/lib/templates/instructions.txt +77 -0
- package/lib/templates/verbose-schema.js +311 -0
- package/lib/utils/ai.js +42 -64
- package/lib/utils/chart.js +5 -0
- package/lib/utils/utils.js +69 -45
- package/package.json +10 -11
- package/types.d.ts +134 -126
- package/lib/cloud-function.js +0 -20
- /package/lib/{utils/prompt.txt → templates/prompt (old).txt} +0 -0
package/entry.js
CHANGED
|
@@ -27,14 +27,25 @@ import getCliParams from './lib/cli/cli.js';
|
|
|
27
27
|
const result = await main(finalConfig);
|
|
28
28
|
console.log(`📊 Generated ${(result.eventCount || 0).toLocaleString()} events for ${(result.userCount || 0).toLocaleString()} users`);
|
|
29
29
|
console.log(`⏱️ Total time: ${result.time?.human || 'unknown'}`);
|
|
30
|
-
|
|
30
|
+
const recordsPerSecond = result.eventCount / result.time.delta * 1000;
|
|
31
|
+
console.log(`⚡ Records per second: ${recordsPerSecond.toFixed(2)}`);
|
|
32
|
+
|
|
33
|
+
if (result.errors?.length) {
|
|
34
|
+
console.error(`\n❗ Errors encountered: ${result.errors.length}`);
|
|
35
|
+
if (cliConfig.verbose) {
|
|
36
|
+
result.errors.forEach(err => console.error(` ${err}`));
|
|
37
|
+
}
|
|
38
|
+
} else {
|
|
39
|
+
console.log(`\n🙌 No errors encountered.`);
|
|
40
|
+
}
|
|
41
|
+
|
|
31
42
|
if (result.files?.length) {
|
|
32
43
|
console.log(`📁 Files written: ${result.files.length}`);
|
|
33
44
|
if (cliConfig.verbose) {
|
|
34
45
|
result.files.forEach(file => console.log(` ${file}`));
|
|
35
46
|
}
|
|
36
47
|
}
|
|
37
|
-
console.log(`\n
|
|
48
|
+
console.log(`\n👋 Job completed successfully!\n`);
|
|
38
49
|
process.exit(0);
|
|
39
50
|
} catch (error) {
|
|
40
51
|
console.error(`\n❌ Job failed: ${error.message}`);
|
package/index.js
CHANGED
|
@@ -47,6 +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
|
+
/**
|
|
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
|
+
}
|
|
50
109
|
|
|
51
110
|
/**
|
|
52
111
|
* Main data generation function
|
|
@@ -81,6 +140,11 @@ async function main(config) {
|
|
|
81
140
|
try {
|
|
82
141
|
// Step 1: Validate and enrich configuration
|
|
83
142
|
validatedConfig = validateDungeonConfig(config);
|
|
143
|
+
|
|
144
|
+
// Step 1.5: Display configuration summary (CLI mode only)
|
|
145
|
+
if (isCLI && validatedConfig.verbose) {
|
|
146
|
+
displayConfigurationSummary(validatedConfig);
|
|
147
|
+
}
|
|
84
148
|
|
|
85
149
|
// Step 2: Create context with validated config
|
|
86
150
|
const context = createContext(validatedConfig, null, isCLI);
|
|
@@ -90,6 +154,8 @@ async function main(config) {
|
|
|
90
154
|
const storage = await storageManager.initializeContainers();
|
|
91
155
|
updateContextWithStorage(context, storage);
|
|
92
156
|
|
|
157
|
+
// ! DATA GENERATION STARTS HERE
|
|
158
|
+
|
|
93
159
|
// Step 4: Generate ad spend data (if enabled)
|
|
94
160
|
if (validatedConfig.hasAdSpend) {
|
|
95
161
|
await generateAdSpendData(context);
|
|
@@ -119,6 +185,10 @@ async function main(config) {
|
|
|
119
185
|
await makeMirror(context);
|
|
120
186
|
}
|
|
121
187
|
|
|
188
|
+
if (context.config.verbose) console.log(`\n✅ Data generation completed successfully!\n`);
|
|
189
|
+
|
|
190
|
+
// ! DATA GENERATION ENDS HERE
|
|
191
|
+
|
|
122
192
|
// Step 10: Generate charts (if enabled)
|
|
123
193
|
if (validatedConfig.makeChart) {
|
|
124
194
|
await generateCharts(context);
|
|
@@ -252,7 +322,8 @@ async function generateLookupTables(context) {
|
|
|
252
322
|
|
|
253
323
|
for (let j = 0; j < entries; j++) {
|
|
254
324
|
const lookupEntry = await makeProfile(context, attributes, {
|
|
255
|
-
|
|
325
|
+
id: j + 1 //primary key is always a number so it joins simply with events
|
|
326
|
+
// [key]: `${key}_${j + 1}` // we don't want to use the lookup name as a prefix here
|
|
256
327
|
});
|
|
257
328
|
|
|
258
329
|
await lookupContainer.hookPush(lookupEntry);
|
|
@@ -352,7 +423,7 @@ async function generateCharts(context) {
|
|
|
352
423
|
if (config.makeChart && storage.eventData?.length > 0) {
|
|
353
424
|
const chartPath = typeof config.makeChart === 'string'
|
|
354
425
|
? config.makeChart
|
|
355
|
-
:
|
|
426
|
+
: `./${config.simulationName}-timeline`;
|
|
356
427
|
|
|
357
428
|
await generateLineChart(storage.eventData, undefined, chartPath);
|
|
358
429
|
|
|
@@ -395,7 +466,7 @@ async function flushStorageToDisk(storage, config) {
|
|
|
395
466
|
await Promise.all(flushPromises);
|
|
396
467
|
|
|
397
468
|
if (config.verbose) {
|
|
398
|
-
console.log('
|
|
469
|
+
console.log('🙏 Data flushed to disk successfully');
|
|
399
470
|
}
|
|
400
471
|
}
|
|
401
472
|
|
package/lib/cli/cli.js
CHANGED
|
@@ -100,7 +100,7 @@ DATA MODEL: https://github.com/ak--47/make-mp-data/blob/main/default.js
|
|
|
100
100
|
})
|
|
101
101
|
.option('concurrency', {
|
|
102
102
|
alias: 'conn',
|
|
103
|
-
default:
|
|
103
|
+
default: 10,
|
|
104
104
|
demandOption: false,
|
|
105
105
|
describe: 'concurrency level for data generation',
|
|
106
106
|
type: 'number'
|
|
@@ -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,8 +15,8 @@ import os from "os";
|
|
|
10
15
|
|
|
11
16
|
/**
|
|
12
17
|
* Infers funnels from the provided events
|
|
13
|
-
* @param {
|
|
14
|
-
* @returns {
|
|
18
|
+
* @param {EventConfig[]} events - Array of event configurations
|
|
19
|
+
* @returns {Funnel[]} Array of inferred funnel configurations
|
|
15
20
|
*/
|
|
16
21
|
function inferFunnels(events) {
|
|
17
22
|
const createdFunnels = [];
|
|
@@ -64,8 +69,8 @@ function inferFunnels(events) {
|
|
|
64
69
|
|
|
65
70
|
/**
|
|
66
71
|
* Validates and enriches a dungeon configuration object
|
|
67
|
-
* @param {
|
|
68
|
-
* @returns {
|
|
72
|
+
* @param {Partial<Dungeon>} config - Raw configuration object
|
|
73
|
+
* @returns {Dungeon} Validated and enriched configuration
|
|
69
74
|
*/
|
|
70
75
|
export function validateDungeonConfig(config) {
|
|
71
76
|
const chance = u.getChance();
|
|
@@ -111,9 +116,14 @@ export function validateDungeonConfig(config) {
|
|
|
111
116
|
alsoInferFunnels = false,
|
|
112
117
|
name = "",
|
|
113
118
|
batchSize = 500_000,
|
|
114
|
-
concurrency
|
|
119
|
+
concurrency
|
|
115
120
|
} = config;
|
|
116
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
|
+
|
|
117
127
|
// Ensure defaults for deep objects
|
|
118
128
|
if (!config.superProps) config.superProps = superProps;
|
|
119
129
|
if (!config.userProps || Object.keys(config?.userProps || {})) config.userProps = userProps;
|
|
@@ -133,9 +143,9 @@ export function validateDungeonConfig(config) {
|
|
|
133
143
|
// Validate events
|
|
134
144
|
if (!events || !events.length) events = [{ event: "foo" }, { event: "bar" }, { event: "baz" }];
|
|
135
145
|
|
|
136
|
-
// Convert string events to objects
|
|
146
|
+
// Convert string events to objects
|
|
137
147
|
if (typeof events[0] === "string") {
|
|
138
|
-
events = events.map(e => ({ event: e }));
|
|
148
|
+
events = events.map(e => ({ event: /** @type {string} */ (e) }));
|
|
139
149
|
}
|
|
140
150
|
|
|
141
151
|
// Handle funnel inference
|
|
@@ -233,6 +243,11 @@ export function validateDungeonConfig(config) {
|
|
|
233
243
|
* @param {Object} config - Configuration to validate
|
|
234
244
|
* @throws {Error} If required fields are missing
|
|
235
245
|
*/
|
|
246
|
+
/**
|
|
247
|
+
* Validates required configuration parameters
|
|
248
|
+
* @param {Dungeon} config - Configuration object to validate
|
|
249
|
+
* @returns {boolean} True if validation passes
|
|
250
|
+
*/
|
|
236
251
|
export function validateRequiredConfig(config) {
|
|
237
252
|
if (!config) {
|
|
238
253
|
throw new Error("Configuration is required");
|
package/lib/core/context.js
CHANGED
|
@@ -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 '../
|
|
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: () =>
|
|
36
|
-
locationsEvents: () =>
|
|
37
|
-
iOSDevices: () =>
|
|
38
|
-
androidDevices: () =>
|
|
39
|
-
desktopDevices: () =>
|
|
40
|
-
browsers: () =>
|
|
41
|
-
campaigns: () =>
|
|
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(
|
|
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(
|
|
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;
|
package/lib/core/storage.js
CHANGED
|
@@ -3,7 +3,10 @@
|
|
|
3
3
|
* Extracted from index.js to eliminate global dependencies
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
/** @typedef {import('../../types').Context} Context */
|
|
6
|
+
/** @typedef {import('../../types.js').Context} Context */
|
|
7
|
+
/** @typedef {import('../../types.js').HookedArray<any>} HookedArray */
|
|
8
|
+
/** @typedef {import('../../types.js').Storage} Storage */
|
|
9
|
+
/** @typedef {import('../../types.js').hookArrayOptions<any>} hookArrayOptions */
|
|
7
10
|
|
|
8
11
|
import { existsSync } from "fs";
|
|
9
12
|
import pLimit from 'p-limit';
|
|
@@ -14,25 +17,19 @@ import * as u from "../utils/utils.js";
|
|
|
14
17
|
/**
|
|
15
18
|
* Creates a hooked array that transforms data on push and handles batching/disk writes
|
|
16
19
|
* @param {Array} arr - Base array to enhance
|
|
17
|
-
* @param {
|
|
18
|
-
* @
|
|
19
|
-
* @param {string} opts.type - Type identifier for the hook function
|
|
20
|
-
* @param {string} opts.filepath - Base filename for disk writes
|
|
21
|
-
* @param {string} opts.format - Output format ('csv' or 'json')
|
|
22
|
-
* @param {number} opts.concurrency - Max concurrent file operations
|
|
23
|
-
* @param {Context} opts.context - Context object with config, batchSize, etc.
|
|
24
|
-
* @returns {Promise<Array>} Enhanced array with hookPush and flush methods
|
|
20
|
+
* @param {hookArrayOptions} opts - Configuration options
|
|
21
|
+
* @returns {Promise<HookedArray>} Enhanced array with hookPush and flush methods
|
|
25
22
|
*/
|
|
26
|
-
export async function createHookArray(arr = [], opts
|
|
23
|
+
export async function createHookArray(arr = [], opts) {
|
|
27
24
|
const {
|
|
28
25
|
hook = a => a,
|
|
29
26
|
type = "",
|
|
30
27
|
filepath = "./defaultFile",
|
|
31
28
|
format = "csv",
|
|
32
29
|
concurrency = 1,
|
|
33
|
-
context = {},
|
|
30
|
+
context = /** @type {Context} */ ({}),
|
|
34
31
|
...rest
|
|
35
|
-
} = opts;
|
|
32
|
+
} = opts || {};
|
|
36
33
|
|
|
37
34
|
const FILE_CONN = pLimit(concurrency);
|
|
38
35
|
const { config = {}, runtime = {} } = context;
|
|
@@ -77,7 +74,7 @@ export async function createHookArray(arr = [], opts = {}) {
|
|
|
77
74
|
|
|
78
75
|
// Performance optimization: skip hook overhead for passthrough hooks
|
|
79
76
|
const isPassthroughHook = hook.toString().includes('return record') || hook.length === 1;
|
|
80
|
-
|
|
77
|
+
|
|
81
78
|
if (isPassthroughHook) {
|
|
82
79
|
// Fast path for passthrough hooks - no transformation needed
|
|
83
80
|
if (Array.isArray(item)) {
|
|
@@ -118,6 +115,8 @@ export async function createHookArray(arr = [], opts = {}) {
|
|
|
118
115
|
batch++;
|
|
119
116
|
const writePath = getWritePath();
|
|
120
117
|
const writeResult = await FILE_CONN(() => writeToDisk(arr, { writePath }));
|
|
118
|
+
// Ensure array is cleared after successful write
|
|
119
|
+
arr.length = 0;
|
|
121
120
|
return writeResult;
|
|
122
121
|
} else {
|
|
123
122
|
return Promise.resolve(false);
|
|
@@ -129,7 +128,7 @@ export async function createHookArray(arr = [], opts = {}) {
|
|
|
129
128
|
let writeResult;
|
|
130
129
|
|
|
131
130
|
if (config.verbose) {
|
|
132
|
-
console.log(`\n\
|
|
131
|
+
console.log(`\n\twriting ${writePath}\n`);
|
|
133
132
|
}
|
|
134
133
|
|
|
135
134
|
switch (format) {
|
|
@@ -143,7 +142,7 @@ export async function createHookArray(arr = [], opts = {}) {
|
|
|
143
142
|
throw new Error(`format ${format} is not supported`);
|
|
144
143
|
}
|
|
145
144
|
|
|
146
|
-
|
|
145
|
+
// Array clearing now handled in transformThenPush to ensure proper timing
|
|
147
146
|
return writeResult;
|
|
148
147
|
}
|
|
149
148
|
|
|
@@ -157,7 +156,8 @@ export async function createHookArray(arr = [], opts = {}) {
|
|
|
157
156
|
}
|
|
158
157
|
|
|
159
158
|
// Enhance the array with our methods
|
|
160
|
-
|
|
159
|
+
/** @type {HookedArray} */
|
|
160
|
+
const enrichedArray = /** @type {any} */ (arr);
|
|
161
161
|
enrichedArray.hookPush = transformThenPush;
|
|
162
162
|
enrichedArray.flush = flush;
|
|
163
163
|
enrichedArray.getWriteDir = getWriteDir;
|
|
@@ -181,11 +181,12 @@ export class StorageManager {
|
|
|
181
181
|
|
|
182
182
|
/**
|
|
183
183
|
* Initialize all storage containers for the data generation process
|
|
184
|
-
* @returns {
|
|
184
|
+
* @returns {Promise<Storage>} Storage containers object
|
|
185
185
|
*/
|
|
186
186
|
async initializeContainers() {
|
|
187
187
|
const { config } = this.context;
|
|
188
188
|
|
|
189
|
+
/** @type {Storage} */
|
|
189
190
|
const storage = {
|
|
190
191
|
eventData: await createHookArray([], {
|
|
191
192
|
hook: config.hook,
|
|
@@ -207,7 +208,7 @@ export class StorageManager {
|
|
|
207
208
|
|
|
208
209
|
adSpendData: await createHookArray([], {
|
|
209
210
|
hook: config.hook,
|
|
210
|
-
type: "
|
|
211
|
+
type: "ad-spend",
|
|
211
212
|
filepath: `${config.simulationName || 'adspend'}-ADSPEND`,
|
|
212
213
|
format: config.format || "csv",
|
|
213
214
|
concurrency: config.concurrency || 1,
|
|
@@ -267,7 +268,7 @@ export class StorageManager {
|
|
|
267
268
|
hook: config.hook,
|
|
268
269
|
type: "lookup",
|
|
269
270
|
filepath: `${config.simulationName || 'lookup'}-${lookupConfig.key}-LOOKUP`,
|
|
270
|
-
format:
|
|
271
|
+
format: "csv", // Always force CSV for lookup tables
|
|
271
272
|
concurrency: config.concurrency || 1,
|
|
272
273
|
context: this.context
|
|
273
274
|
});
|
package/lib/generators/events.js
CHANGED
|
@@ -102,19 +102,22 @@ export async function makeEvent(
|
|
|
102
102
|
// Set event time using TimeSoup for realistic distribution
|
|
103
103
|
if (earliestTime) {
|
|
104
104
|
if (isFirstEvent) {
|
|
105
|
-
|
|
105
|
+
// Apply time shift to move to present day using precomputed value
|
|
106
|
+
eventTemplate.time = dayjs.unix(earliestTime).add(context.TIME_SHIFT_SECONDS, 'seconds').toISOString();
|
|
106
107
|
} else {
|
|
107
|
-
|
|
108
|
+
// Get time from TimeSoup and apply precomputed time shift
|
|
109
|
+
const soupTime = u.TimeSoup(earliestTime, context.FIXED_NOW, peaks, deviation, mean);
|
|
110
|
+
eventTemplate.time = dayjs(soupTime).add(context.TIME_SHIFT_SECONDS, 'seconds').toISOString();
|
|
108
111
|
}
|
|
109
112
|
}
|
|
110
113
|
|
|
111
114
|
// Add anonymous and session identifiers
|
|
112
115
|
if (anonymousIds.length) {
|
|
113
|
-
eventTemplate.device_id =
|
|
116
|
+
eventTemplate.device_id = u.pickRandom(anonymousIds);
|
|
114
117
|
}
|
|
115
118
|
|
|
116
119
|
if (sessionIds.length) {
|
|
117
|
-
eventTemplate.session_id =
|
|
120
|
+
eventTemplate.session_id = u.pickRandom(sessionIds);
|
|
118
121
|
}
|
|
119
122
|
|
|
120
123
|
// Sometimes add user_id (for attribution modeling)
|
|
@@ -127,16 +130,28 @@ export async function makeEvent(
|
|
|
127
130
|
eventTemplate.user_id = distinct_id;
|
|
128
131
|
}
|
|
129
132
|
|
|
130
|
-
//
|
|
131
|
-
const props = Object.assign({}, chosenEvent.properties, superProps);
|
|
132
|
-
|
|
133
|
+
// PERFORMANCE: Process properties directly without creating intermediate object
|
|
133
134
|
// Add custom properties from event configuration
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
135
|
+
if (chosenEvent.properties) {
|
|
136
|
+
for (const key in chosenEvent.properties) {
|
|
137
|
+
try {
|
|
138
|
+
eventTemplate[key] = u.choose(chosenEvent.properties[key]);
|
|
139
|
+
} catch (e) {
|
|
140
|
+
console.error(`error with ${key} in ${chosenEvent.event} event`, e);
|
|
141
|
+
// Continue processing other properties
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// Add super properties (override event properties if needed)
|
|
147
|
+
if (superProps) {
|
|
148
|
+
for (const key in superProps) {
|
|
149
|
+
try {
|
|
150
|
+
eventTemplate[key] = u.choose(superProps[key]);
|
|
151
|
+
} catch (e) {
|
|
152
|
+
console.error(`error with ${key} in super props`, e);
|
|
153
|
+
// Continue processing other properties
|
|
154
|
+
}
|
|
140
155
|
}
|
|
141
156
|
}
|
|
142
157
|
|
|
@@ -153,13 +168,21 @@ export async function makeEvent(
|
|
|
153
168
|
const tuple = `${eventTemplate.event}-${eventTemplate.time}-${distinctId}`;
|
|
154
169
|
eventTemplate.insert_id = u.quickHash(tuple);
|
|
155
170
|
|
|
156
|
-
//
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
const
|
|
160
|
-
|
|
171
|
+
// Call hook if configured (before returning the event)
|
|
172
|
+
const { hook } = config;
|
|
173
|
+
if (hook) {
|
|
174
|
+
const hookedEvent = await hook(eventTemplate, "event", {
|
|
175
|
+
user: { distinct_id },
|
|
176
|
+
config
|
|
177
|
+
});
|
|
178
|
+
// If hook returns a modified event, use it; otherwise use original
|
|
179
|
+
if (hookedEvent && typeof hookedEvent === 'object') {
|
|
180
|
+
return hookedEvent;
|
|
181
|
+
}
|
|
161
182
|
}
|
|
162
183
|
|
|
184
|
+
// Note: Time shift already applied above during timestamp calculation
|
|
185
|
+
|
|
163
186
|
return eventTemplate;
|
|
164
187
|
}
|
|
165
188
|
|
|
@@ -3,6 +3,9 @@
|
|
|
3
3
|
* Handles distributed processing across multiple cloud function workers
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
+
/** @typedef {import('../../types.js').Context} Context */
|
|
7
|
+
/** @typedef {import('../../types.js').Dungeon} Dungeon */
|
|
8
|
+
|
|
6
9
|
import pLimit from 'p-limit';
|
|
7
10
|
import { GoogleAuth } from 'google-auth-library';
|
|
8
11
|
import { timer, uid, sLog } from 'ak-tools';
|
|
@@ -138,7 +141,7 @@ export async function handleCloudFunctionEntry(req, res, mainFunction) {
|
|
|
138
141
|
try {
|
|
139
142
|
if (!script) throw new Error("no script");
|
|
140
143
|
|
|
141
|
-
/** @type {
|
|
144
|
+
/** @type {Dungeon} */
|
|
142
145
|
const config = eval(script);
|
|
143
146
|
|
|
144
147
|
if (isReplica) {
|
|
@@ -148,7 +151,7 @@ export async function handleCloudFunctionEntry(req, res, mainFunction) {
|
|
|
148
151
|
params.seed = newSeed;
|
|
149
152
|
}
|
|
150
153
|
|
|
151
|
-
/** @type {
|
|
154
|
+
/** @type {Dungeon} */
|
|
152
155
|
const optionsYouCantChange = {
|
|
153
156
|
verbose: false
|
|
154
157
|
};
|