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.
- package/.claude/settings.local.json +20 -0
- package/.gcloudignore +2 -1
- package/.vscode/launch.json +6 -3
- package/.vscode/settings.json +31 -2
- package/dungeons/media.js +371 -0
- package/index.js +354 -1757
- package/{components → lib/cli}/cli.js +21 -6
- package/lib/cloud-function.js +20 -0
- package/lib/core/config-validator.js +248 -0
- package/lib/core/context.js +180 -0
- package/lib/core/storage.js +268 -0
- package/{components → lib/data}/defaults.js +17 -14
- package/lib/generators/adspend.js +133 -0
- package/lib/generators/events.js +242 -0
- package/lib/generators/funnels.js +330 -0
- package/lib/generators/mirror.js +168 -0
- package/lib/generators/profiles.js +93 -0
- package/lib/generators/scd.js +102 -0
- package/lib/orchestrators/mixpanel-sender.js +222 -0
- package/lib/orchestrators/user-loop.js +194 -0
- package/lib/orchestrators/worker-manager.js +200 -0
- package/{components → lib/utils}/ai.js +8 -36
- package/{components → lib/utils}/chart.js +9 -9
- package/{components → lib/utils}/project.js +4 -4
- package/{components → lib/utils}/utils.js +35 -23
- package/package.json +19 -12
- package/scripts/dana.mjs +137 -0
- package/scripts/new-dungeon.sh +7 -6
- package/scripts/update-deps.sh +2 -1
- package/tests/cli.test.js +28 -25
- package/tests/e2e.test.js +38 -36
- package/tests/int.test.js +151 -56
- package/tests/testSoup.mjs +1 -1
- package/tests/unit.test.js +15 -14
- package/tsconfig.json +1 -1
- package/types.d.ts +76 -18
- package/vitest.config.js +47 -0
- package/dungeons/adspend.js +0 -96
- package/dungeons/anon.js +0 -104
- package/dungeons/big.js +0 -224
- package/dungeons/business.js +0 -327
- package/dungeons/complex.js +0 -396
- package/dungeons/foobar.js +0 -241
- package/dungeons/funnels.js +0 -220
- package/dungeons/gaming-experiments.js +0 -323
- package/dungeons/gaming.js +0 -314
- package/dungeons/governance.js +0 -288
- package/dungeons/mirror.js +0 -129
- package/dungeons/sanity.js +0 -118
- package/dungeons/scd.js +0 -205
- package/dungeons/session-replay.js +0 -175
- package/dungeons/simple.js +0 -150
- package/dungeons/userAgent.js +0 -190
- package/log.json +0 -1067
- package/tests/jest.config.js +0 -47
- /package/{components → lib/utils}/prompt.txt +0 -0
|
@@ -1,5 +1,9 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
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
|
-
|
|
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
|
+
}
|