make-mp-data 1.5.0 → 1.5.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.
- package/.gcloudignore +17 -0
- package/.vscode/launch.json +54 -19
- package/.vscode/settings.json +2 -0
- package/.vscode/tasks.json +12 -0
- package/components/ai.js +93 -0
- package/{src → components}/chart.js +14 -0
- package/{src → components}/cli.js +7 -1
- package/components/project.js +166 -0
- package/components/prompt.txt +98 -0
- package/{src → components}/utils.js +142 -41
- package/{schemas → dungeons}/adspend.js +2 -2
- package/{schemas → dungeons}/anon.js +2 -2
- package/{schemas → dungeons}/big.js +2 -2
- package/dungeons/business.js +327 -0
- package/{schemas → dungeons}/complex.js +10 -10
- package/dungeons/foobar.js +241 -0
- package/{schemas → dungeons}/funnels.js +3 -4
- package/dungeons/gaming.js +314 -0
- package/{schemas → dungeons}/mirror.js +2 -2
- package/{schemas/foobar.js → dungeons/sanity.js} +20 -27
- package/dungeons/scd.js +205 -0
- package/dungeons/session-replay.js +175 -0
- package/{schemas → dungeons}/simple.js +3 -3
- package/dungeons/userAgent.js +190 -0
- package/env.yaml +1 -0
- package/index.js +482 -167
- package/package.json +13 -6
- package/scripts/deploy.sh +11 -0
- package/scripts/jsdoctest.js +1 -1
- package/scripts/{new.sh → new-dungeon.sh} +39 -10
- package/scripts/new-project.mjs +14 -0
- package/scripts/update-deps.sh +4 -0
- package/tests/benchmark/concurrency.mjs +2 -2
- package/tests/cli.test.js +121 -0
- package/tests/e2e.test.js +134 -186
- package/tests/int.test.js +14 -12
- package/tests/jest.config.js +8 -0
- package/tests/testCases.mjs +1 -1
- package/tests/testSoup.mjs +4 -3
- package/tests/unit.test.js +16 -15
- package/tsconfig.json +1 -1
- package/types.d.ts +40 -8
- package/scripts/deps.sh +0 -3
- /package/{src → components}/defaults.js +0 -0
- /package/dungeons/{.gitkeep → customers/.gitkeep} +0 -0
- /package/scripts/{go.sh → run-index.sh} +0 -0
package/index.js
CHANGED
|
@@ -10,35 +10,44 @@ ak@mixpanel.com
|
|
|
10
10
|
//todo: regular interval events (like 'card charged')
|
|
11
11
|
//todo: SCDs send to mixpanel
|
|
12
12
|
//todo: decent 'new dungeon' workflow
|
|
13
|
+
//todo: validation that funnel events exist
|
|
14
|
+
//todo: ability to catch events not in funnels and make them random...
|
|
13
15
|
|
|
14
16
|
|
|
15
17
|
//TIME
|
|
16
18
|
const dayjs = require("dayjs");
|
|
17
19
|
const utc = require("dayjs/plugin/utc");
|
|
18
20
|
dayjs.extend(utc);
|
|
19
|
-
const
|
|
20
|
-
global.
|
|
21
|
+
const FIXED_NOW = dayjs('2024-02-02').unix();
|
|
22
|
+
global.FIXED_NOW = FIXED_NOW;
|
|
21
23
|
// ^ this creates a FIXED POINT in time; we will shift it later
|
|
24
|
+
let FIXED_BEGIN = dayjs.unix(FIXED_NOW).subtract(90, 'd').unix();
|
|
25
|
+
global.FIXED_BEGIN = FIXED_BEGIN;
|
|
22
26
|
const actualNow = dayjs();
|
|
23
|
-
const
|
|
24
|
-
const
|
|
27
|
+
const timeShift = actualNow.diff(dayjs.unix(FIXED_NOW), "seconds");
|
|
28
|
+
const daysShift = actualNow.diff(dayjs.unix(FIXED_NOW), "days");
|
|
25
29
|
|
|
26
30
|
// UTILS
|
|
27
31
|
const { existsSync } = require("fs");
|
|
28
32
|
const pLimit = require('p-limit');
|
|
29
33
|
const os = require("os");
|
|
30
34
|
const path = require("path");
|
|
31
|
-
const { comma, bytesHuman, makeName, md5, clone, tracker, uid, timer, ls, rm } = require("ak-tools");
|
|
35
|
+
const { comma, bytesHuman, makeName, md5, clone, tracker, uid, timer, ls, rm, touch, load, sLog } = require("ak-tools");
|
|
32
36
|
const jobTimer = timer('job');
|
|
33
|
-
const { generateLineChart } = require('./
|
|
37
|
+
const { generateLineChart } = require('./components/chart.js');
|
|
34
38
|
const { version } = require('./package.json');
|
|
35
39
|
const mp = require("mixpanel-import");
|
|
36
|
-
const u = require("./
|
|
37
|
-
const getCliParams = require("./
|
|
40
|
+
const u = require("./components/utils.js");
|
|
41
|
+
const getCliParams = require("./components/cli.js");
|
|
38
42
|
const metrics = tracker("make-mp-data", "db99eb8f67ae50949a13c27cacf57d41", os.userInfo().username);
|
|
43
|
+
const t = require('ak-tools');
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
//CLOUD
|
|
47
|
+
const functions = require('@google-cloud/functions-framework');
|
|
39
48
|
|
|
40
49
|
// DEFAULTS
|
|
41
|
-
const { campaigns, devices, locations } = require('./
|
|
50
|
+
const { campaigns, devices, locations } = require('./components/defaults.js');
|
|
42
51
|
let CAMPAIGNS;
|
|
43
52
|
let DEFAULTS;
|
|
44
53
|
/** @type {Storage} */
|
|
@@ -47,13 +56,17 @@ let STORAGE;
|
|
|
47
56
|
let CONFIG;
|
|
48
57
|
require('dotenv').config();
|
|
49
58
|
|
|
59
|
+
const { NODE_ENV = "unknown" } = process.env;
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
|
|
50
63
|
|
|
51
64
|
// RUN STATE
|
|
52
65
|
let VERBOSE = false;
|
|
53
66
|
let isCLI = false;
|
|
54
67
|
// if we are running in batch mode, we MUST write to disk before we can send to mixpanel
|
|
55
|
-
let isBATCH_MODE = false;
|
|
56
|
-
let BATCH_SIZE =
|
|
68
|
+
let isBATCH_MODE = false;
|
|
69
|
+
let BATCH_SIZE = 1_000_000;
|
|
57
70
|
|
|
58
71
|
//todo: these should be moved into the hookedArrays
|
|
59
72
|
let operations = 0;
|
|
@@ -75,6 +88,7 @@ async function main(config) {
|
|
|
75
88
|
// ^ this is critical; same seed = same data;
|
|
76
89
|
// ^ seed can be passed in as an env var or in the config
|
|
77
90
|
validateDungeonConfig(config);
|
|
91
|
+
global.FIXED_BEGIN = dayjs.unix(FIXED_NOW).subtract(config.numDays, 'd').unix();
|
|
78
92
|
|
|
79
93
|
//GLOBALS
|
|
80
94
|
CONFIG = config;
|
|
@@ -93,21 +107,25 @@ async function main(config) {
|
|
|
93
107
|
|
|
94
108
|
//TRACKING
|
|
95
109
|
const runId = uid(42);
|
|
96
|
-
const { events, superProps, userProps, scdProps, groupKeys, groupProps, lookupTables, soup, hook, mirrorProps, ...trackingParams } = config;
|
|
110
|
+
const { events, superProps, userProps, scdProps, groupKeys, groupProps, lookupTables, soup, hook, mirrorProps, token: source_proj_token, ...trackingParams } = config;
|
|
97
111
|
let { funnels } = config;
|
|
98
112
|
trackingParams.runId = runId;
|
|
99
113
|
trackingParams.version = version;
|
|
114
|
+
delete trackingParams.funnels;
|
|
100
115
|
|
|
101
116
|
//STORAGE
|
|
102
117
|
const { simulationName, format } = config;
|
|
103
118
|
const eventData = await makeHookArray([], { hook, type: "event", config, format, filepath: `${simulationName}-EVENTS` });
|
|
104
119
|
const userProfilesData = await makeHookArray([], { hook, type: "user", config, format, filepath: `${simulationName}-USERS` });
|
|
105
120
|
const adSpendData = await makeHookArray([], { hook, type: "ad-spend", config, format, filepath: `${simulationName}-AD-SPEND` });
|
|
121
|
+
const groupEventData = await makeHookArray([], { hook, type: "group-event", config, format, filepath: `${simulationName}-GROUP-EVENTS` });
|
|
106
122
|
|
|
107
123
|
// SCDs, Groups, + Lookups may have multiple tables
|
|
108
124
|
const scdTableKeys = Object.keys(scdProps);
|
|
109
125
|
const scdTableData = await Promise.all(scdTableKeys.map(async (key) =>
|
|
110
|
-
|
|
126
|
+
//todo don't assume everything is a string... lol
|
|
127
|
+
// @ts-ignore
|
|
128
|
+
await makeHookArray([], { hook, type: "scd", config, format, scdKey: key, entityType: config.scdProps[key].type, dataType: "string", filepath: `${simulationName}-${scdProps[key]?.type || "user"}-SCD-${key}` })
|
|
111
129
|
));
|
|
112
130
|
const groupTableKeys = Object.keys(groupKeys);
|
|
113
131
|
const groupProfilesData = await Promise.all(groupTableKeys.map(async (key, index) => {
|
|
@@ -123,7 +141,17 @@ async function main(config) {
|
|
|
123
141
|
|
|
124
142
|
const mirrorEventData = await makeHookArray([], { hook, type: "mirror", config, format, filepath: `${simulationName}-MIRROR` });
|
|
125
143
|
|
|
126
|
-
STORAGE = {
|
|
144
|
+
STORAGE = {
|
|
145
|
+
eventData,
|
|
146
|
+
userProfilesData,
|
|
147
|
+
scdTableData,
|
|
148
|
+
groupProfilesData,
|
|
149
|
+
lookupTableData,
|
|
150
|
+
mirrorEventData,
|
|
151
|
+
adSpendData,
|
|
152
|
+
groupEventData
|
|
153
|
+
|
|
154
|
+
};
|
|
127
155
|
|
|
128
156
|
|
|
129
157
|
track('start simulation', trackingParams);
|
|
@@ -156,22 +184,72 @@ async function main(config) {
|
|
|
156
184
|
log("\n");
|
|
157
185
|
|
|
158
186
|
//GROUP PROFILES
|
|
187
|
+
const groupSCDs = t.objFilter(scdProps, (scd) => scd.type !== 'user');
|
|
159
188
|
for (const [index, groupPair] of groupKeys.entries()) {
|
|
160
189
|
const groupKey = groupPair[0];
|
|
161
190
|
const groupCardinality = groupPair[1];
|
|
162
191
|
for (let i = 1; i < groupCardinality + 1; i++) {
|
|
163
192
|
if (VERBOSE) u.progress([["groups", i]]);
|
|
164
|
-
|
|
193
|
+
|
|
194
|
+
const props = await makeProfile(groupProps[groupKey], { created: () => { return dayjs().subtract(u.integer(0, CONFIG.numDays || 30), 'd').toISOString(); } });
|
|
165
195
|
const group = {
|
|
166
196
|
[groupKey]: i,
|
|
167
197
|
...props,
|
|
168
198
|
};
|
|
169
199
|
group["distinct_id"] = i.toString();
|
|
170
200
|
await groupProfilesData[index].hookPush(group);
|
|
201
|
+
|
|
202
|
+
//SCDs
|
|
203
|
+
const thisGroupSCD = t.objFilter(groupSCDs, (scd) => scd.type === groupKey);
|
|
204
|
+
const groupSCDKeys = Object.keys(thisGroupSCD);
|
|
205
|
+
const groupSCD = {};
|
|
206
|
+
for (const [index, key] of groupSCDKeys.entries()) {
|
|
207
|
+
const { max = 100 } = groupSCDs[key];
|
|
208
|
+
const mutations = chance.integer({ min: 2, max });
|
|
209
|
+
const changes = await makeSCD(scdProps[key], key, i.toString(), mutations, group.created);
|
|
210
|
+
groupSCD[key] = changes;
|
|
211
|
+
const scdTable = scdTableData
|
|
212
|
+
.filter(hookArr => hookArr.scdKey === key);
|
|
213
|
+
|
|
214
|
+
await config.hook(changes, 'scd-pre', { profile: group, type: groupKey, scd: { [key]: groupSCDs[key] }, config, allSCDs: groupSCD });
|
|
215
|
+
await scdTable[0].hookPush(changes, { profile: group, type: groupKey });
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
|
|
171
219
|
}
|
|
172
220
|
}
|
|
173
221
|
log("\n");
|
|
174
222
|
|
|
223
|
+
//GROUP EVENTS
|
|
224
|
+
if (config.groupEvents) {
|
|
225
|
+
for (const groupEvent of config.groupEvents) {
|
|
226
|
+
const { frequency, group_key, attribute_to_user, group_size, ...normalEvent } = groupEvent;
|
|
227
|
+
for (const group_num of Array.from({ length: group_size }, (_, i) => i + 1)) {
|
|
228
|
+
const groupProfile = groupProfilesData.find(groups => groups.groupKey === group_key).find(group => group[group_key] === group_num);
|
|
229
|
+
const { created, distinct_id } = groupProfile;
|
|
230
|
+
normalEvent[group_key] = distinct_id;
|
|
231
|
+
const random_user_id = chance.pick(eventData.filter(a => a.user_id)).user_id;
|
|
232
|
+
if (!random_user_id) debugger;
|
|
233
|
+
const deltaDays = actualNow.diff(dayjs(created), "day");
|
|
234
|
+
const numIntervals = Math.floor(deltaDays / frequency);
|
|
235
|
+
const eventsForThisGroup = [];
|
|
236
|
+
for (let i = 0; i < numIntervals; i++) {
|
|
237
|
+
const event = await makeEvent(random_user_id, null, normalEvent, [], [], {}, [], false, true);
|
|
238
|
+
if (!attribute_to_user) delete event.user_id;
|
|
239
|
+
event[group_key] = distinct_id;
|
|
240
|
+
event.time = dayjs(created).add(i * frequency, "day").toISOString();
|
|
241
|
+
delete event.distinct_id;
|
|
242
|
+
//always skip the first event
|
|
243
|
+
if (i !== 0) {
|
|
244
|
+
eventsForThisGroup.push(event);
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
await groupEventData.hookPush(eventsForThisGroup, { profile: groupProfile });
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
|
|
175
253
|
//LOOKUP TABLES
|
|
176
254
|
for (const [index, lookupTable] of lookupTables.entries()) {
|
|
177
255
|
const { key, entries, attributes } = lookupTable;
|
|
@@ -204,7 +282,7 @@ async function main(config) {
|
|
|
204
282
|
const bornBehaviors = [...bornEvents, ...bornFunnels];
|
|
205
283
|
const chart = await generateLineChart(eventData, bornBehaviors, makeChart);
|
|
206
284
|
}
|
|
207
|
-
const { writeToDisk, token } = config;
|
|
285
|
+
const { writeToDisk = true, token } = config;
|
|
208
286
|
if (!writeToDisk && !token) {
|
|
209
287
|
jobTimer.stop(false);
|
|
210
288
|
const { start, end, delta, human } = jobTimer.report(false);
|
|
@@ -243,6 +321,7 @@ async function main(config) {
|
|
|
243
321
|
jobTimer.stop(false);
|
|
244
322
|
const { start, end, delta, human } = jobTimer.report(false);
|
|
245
323
|
|
|
324
|
+
if (process.env.NODE_ENV === 'dev') debugger;
|
|
246
325
|
return {
|
|
247
326
|
...STORAGE,
|
|
248
327
|
importResults,
|
|
@@ -253,6 +332,53 @@ async function main(config) {
|
|
|
253
332
|
|
|
254
333
|
|
|
255
334
|
|
|
335
|
+
functions.http('entry', async (req, res) => {
|
|
336
|
+
const reqTimer = timer('request');
|
|
337
|
+
reqTimer.start();
|
|
338
|
+
let response = {};
|
|
339
|
+
let script = req.body || "";
|
|
340
|
+
let writePath;
|
|
341
|
+
try {
|
|
342
|
+
sLog("DM4: start");
|
|
343
|
+
if (!script) throw new Error("no script");
|
|
344
|
+
|
|
345
|
+
// Replace require("../ with require("./
|
|
346
|
+
script = script.replace(/require\("\.\.\//g, 'require("./');
|
|
347
|
+
// ^ need to replace this because of the way the script is passed in... this is sketch
|
|
348
|
+
|
|
349
|
+
/** @type {Config} */
|
|
350
|
+
const config = eval(script);
|
|
351
|
+
sLog("DM4: eval ok");
|
|
352
|
+
|
|
353
|
+
const { token } = config;
|
|
354
|
+
if (!token) throw new Error("no token");
|
|
355
|
+
|
|
356
|
+
/** @type {Config} */
|
|
357
|
+
const optionsYouCantChange = {
|
|
358
|
+
verbose: false,
|
|
359
|
+
writeToDisk: false,
|
|
360
|
+
|
|
361
|
+
};
|
|
362
|
+
const result = await main({
|
|
363
|
+
...config,
|
|
364
|
+
...optionsYouCantChange,
|
|
365
|
+
});
|
|
366
|
+
await rm(writePath);
|
|
367
|
+
reqTimer.stop(false);
|
|
368
|
+
const { start, end, delta, human } = jobTimer.report(false);
|
|
369
|
+
sLog(`DM4: end (${human})`, { ms: delta });
|
|
370
|
+
}
|
|
371
|
+
catch (e) {
|
|
372
|
+
sLog("DM4: error", { error: e.message });
|
|
373
|
+
response = { error: e.message };
|
|
374
|
+
res.status(500);
|
|
375
|
+
await rm(writePath);
|
|
376
|
+
}
|
|
377
|
+
finally {
|
|
378
|
+
res.send(response);
|
|
379
|
+
}
|
|
380
|
+
});
|
|
381
|
+
|
|
256
382
|
|
|
257
383
|
/*
|
|
258
384
|
------
|
|
@@ -272,7 +398,7 @@ MODELS
|
|
|
272
398
|
* @param {Boolean} [isFirstEvent]
|
|
273
399
|
* @return {Promise<EventSchema>}
|
|
274
400
|
*/
|
|
275
|
-
async function makeEvent(distinct_id, earliestTime, chosenEvent, anonymousIds, sessionIds, superProps, groupKeys, isFirstEvent) {
|
|
401
|
+
async function makeEvent(distinct_id, earliestTime, chosenEvent, anonymousIds, sessionIds, superProps, groupKeys, isFirstEvent, skipDefaults = false) {
|
|
276
402
|
operations++;
|
|
277
403
|
eventCount++;
|
|
278
404
|
if (!distinct_id) throw new Error("no distinct_id");
|
|
@@ -304,11 +430,13 @@ async function makeEvent(distinct_id, earliestTime, chosenEvent, anonymousIds, s
|
|
|
304
430
|
|
|
305
431
|
let defaultProps = {};
|
|
306
432
|
let devicePool = [];
|
|
433
|
+
|
|
307
434
|
if (hasLocation) defaultProps.location = DEFAULTS.locationsEvents();
|
|
308
435
|
if (hasBrowser) defaultProps.browser = DEFAULTS.browsers();
|
|
309
436
|
if (hasAndroidDevices) devicePool.push(DEFAULTS.androidDevices());
|
|
310
437
|
if (hasIOSDevices) devicePool.push(DEFAULTS.iOSDevices());
|
|
311
438
|
if (hasDesktopDevices) devicePool.push(DEFAULTS.desktopDevices());
|
|
439
|
+
|
|
312
440
|
// we don't always have campaigns, because of attribution
|
|
313
441
|
if (hasCampaigns && chance.bool({ likelihood: 25 })) defaultProps.campaigns = DEFAULTS.campaigns();
|
|
314
442
|
const devices = devicePool.flat();
|
|
@@ -316,12 +444,10 @@ async function makeEvent(distinct_id, earliestTime, chosenEvent, anonymousIds, s
|
|
|
316
444
|
|
|
317
445
|
|
|
318
446
|
//event time
|
|
319
|
-
if (earliestTime
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
if (isFirstEvent) eventTemplate.time = dayjs.unix(earliestTime).toISOString();
|
|
324
|
-
if (!isFirstEvent) eventTemplate.time = u.TimeSoup(earliestTime, NOW, peaks, deviation, mean);
|
|
447
|
+
if (earliestTime) {
|
|
448
|
+
if (isFirstEvent) eventTemplate.time = dayjs.unix(earliestTime).toISOString();
|
|
449
|
+
if (!isFirstEvent) eventTemplate.time = u.TimeSoup(earliestTime, FIXED_NOW, peaks, deviation, mean);
|
|
450
|
+
}
|
|
325
451
|
|
|
326
452
|
// anonymous and session ids
|
|
327
453
|
if (anonymousIds.length) eventTemplate.device_id = chance.pickone(anonymousIds);
|
|
@@ -346,39 +472,39 @@ async function makeEvent(distinct_id, earliestTime, chosenEvent, anonymousIds, s
|
|
|
346
472
|
}
|
|
347
473
|
|
|
348
474
|
//iterate through default properties
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
if (
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
else if (Array.isArray(choice)) {
|
|
357
|
-
for (const subChoice of choice) {
|
|
358
|
-
if (!eventTemplate[key]) eventTemplate[key] = subChoice;
|
|
475
|
+
if (!skipDefaults) {
|
|
476
|
+
for (const key in defaultProps) {
|
|
477
|
+
if (Array.isArray(defaultProps[key])) {
|
|
478
|
+
const choice = u.choose(defaultProps[key]);
|
|
479
|
+
if (typeof choice === "string") {
|
|
480
|
+
if (!eventTemplate[key]) eventTemplate[key] = choice;
|
|
359
481
|
}
|
|
360
|
-
}
|
|
361
482
|
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
if (!eventTemplate[subKey]) eventTemplate[subKey] = choice[subKey];
|
|
366
|
-
}
|
|
367
|
-
else if (Array.isArray(choice[subKey])) {
|
|
368
|
-
const subChoice = u.choose(choice[subKey]);
|
|
369
|
-
if (!eventTemplate[subKey]) eventTemplate[subKey] = subChoice;
|
|
483
|
+
else if (Array.isArray(choice)) {
|
|
484
|
+
for (const subChoice of choice) {
|
|
485
|
+
if (!eventTemplate[key]) eventTemplate[key] = subChoice;
|
|
370
486
|
}
|
|
487
|
+
}
|
|
371
488
|
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
489
|
+
else if (typeof choice === "object") {
|
|
490
|
+
for (const subKey in choice) {
|
|
491
|
+
if (typeof choice[subKey] === "string") {
|
|
492
|
+
if (!eventTemplate[subKey]) eventTemplate[subKey] = choice[subKey];
|
|
493
|
+
}
|
|
494
|
+
else if (Array.isArray(choice[subKey])) {
|
|
495
|
+
const subChoice = u.choose(choice[subKey]);
|
|
496
|
+
if (!eventTemplate[subKey]) eventTemplate[subKey] = subChoice;
|
|
375
497
|
}
|
|
376
|
-
}
|
|
377
498
|
|
|
499
|
+
else if (typeof choice[subKey] === "object") {
|
|
500
|
+
for (const subSubKey in choice[subKey]) {
|
|
501
|
+
if (!eventTemplate[subSubKey]) eventTemplate[subSubKey] = choice[subKey][subSubKey];
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
}
|
|
378
506
|
}
|
|
379
507
|
}
|
|
380
|
-
|
|
381
|
-
|
|
382
508
|
}
|
|
383
509
|
}
|
|
384
510
|
|
|
@@ -396,10 +522,11 @@ async function makeEvent(distinct_id, earliestTime, chosenEvent, anonymousIds, s
|
|
|
396
522
|
//make $insert_id
|
|
397
523
|
eventTemplate.insert_id = md5(JSON.stringify(eventTemplate));
|
|
398
524
|
|
|
399
|
-
//
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
525
|
+
// move time forward
|
|
526
|
+
if (earliestTime) {
|
|
527
|
+
const timeShifted = dayjs(eventTemplate.time).add(timeShift, "seconds").toISOString();
|
|
528
|
+
eventTemplate.time = timeShifted;
|
|
529
|
+
}
|
|
403
530
|
|
|
404
531
|
|
|
405
532
|
return eventTemplate;
|
|
@@ -489,6 +616,8 @@ async function makeFunnel(funnel, user, firstEventTime, profile, scd, config) {
|
|
|
489
616
|
return acc;
|
|
490
617
|
}, []);
|
|
491
618
|
|
|
619
|
+
if (conversionRate > 100) conversionRate = 100;
|
|
620
|
+
if (conversionRate < 0) conversionRate = 0;
|
|
492
621
|
let doesUserConvert = chance.bool({ likelihood: conversionRate });
|
|
493
622
|
let numStepsUserWillTake = sequence.length;
|
|
494
623
|
if (!doesUserConvert) numStepsUserWillTake = u.integer(1, sequence.length - 1);
|
|
@@ -601,12 +730,23 @@ async function makeProfile(props, defaults) {
|
|
|
601
730
|
...defaults,
|
|
602
731
|
};
|
|
603
732
|
|
|
733
|
+
for (const key in profile) {
|
|
734
|
+
try {
|
|
735
|
+
profile[key] = u.choose(profile[key]);
|
|
736
|
+
}
|
|
737
|
+
catch (e) {
|
|
738
|
+
// never gets here
|
|
739
|
+
debugger;
|
|
740
|
+
}
|
|
741
|
+
}
|
|
742
|
+
|
|
743
|
+
|
|
604
744
|
for (const key in props) {
|
|
605
745
|
try {
|
|
606
746
|
profile[key] = u.choose(props[key]);
|
|
607
747
|
} catch (e) {
|
|
608
748
|
// never gets here
|
|
609
|
-
|
|
749
|
+
debugger;
|
|
610
750
|
}
|
|
611
751
|
}
|
|
612
752
|
|
|
@@ -614,42 +754,67 @@ async function makeProfile(props, defaults) {
|
|
|
614
754
|
}
|
|
615
755
|
|
|
616
756
|
/**
|
|
617
|
-
* @param {
|
|
757
|
+
* @param {SCDProp} scdProp
|
|
618
758
|
* @param {string} scdKey
|
|
619
759
|
* @param {string} distinct_id
|
|
620
760
|
* @param {number} mutations
|
|
621
761
|
* @param {string} created
|
|
622
762
|
* @return {Promise<SCDSchema[]>}
|
|
623
763
|
*/
|
|
624
|
-
async function makeSCD(
|
|
625
|
-
if (
|
|
764
|
+
async function makeSCD(scdProp, scdKey, distinct_id, mutations, created) {
|
|
765
|
+
if (Array.isArray(scdProp)) scdProp = { values: scdProp, frequency: 'week', max: 10, timing: 'fuzzy', type: 'user' };
|
|
766
|
+
const { frequency, max, timing, values, type } = scdProp;
|
|
767
|
+
if (JSON.stringify(values) === "{}" || JSON.stringify(values) === "[]") return [];
|
|
626
768
|
const scdEntries = [];
|
|
627
769
|
let lastInserted = dayjs(created);
|
|
628
770
|
const deltaDays = dayjs().diff(lastInserted, "day");
|
|
771
|
+
const uuidKeyName = type === 'user' ? 'distinct_id' : type;
|
|
629
772
|
|
|
630
773
|
for (let i = 0; i < mutations; i++) {
|
|
631
774
|
if (lastInserted.isAfter(dayjs())) break;
|
|
632
|
-
let scd = await makeProfile({ [scdKey]:
|
|
775
|
+
let scd = await makeProfile({ [scdKey]: values }, { [uuidKeyName]: distinct_id });
|
|
633
776
|
|
|
634
777
|
// Explicitly constructing SCDSchema object with all required properties
|
|
635
778
|
const scdEntry = {
|
|
636
779
|
...scd, // spread existing properties
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
780
|
+
[uuidKeyName]: scd.distinct_id || distinct_id, // ensure distinct_id is set
|
|
781
|
+
startTime: null,
|
|
782
|
+
insertTime: null
|
|
640
783
|
};
|
|
641
784
|
|
|
785
|
+
if (timing === 'fixed') {
|
|
786
|
+
if (frequency === "day") scdEntry.startTime = lastInserted.add(1, "day").startOf('day').toISOString();
|
|
787
|
+
if (frequency === "week") scdEntry.startTime = lastInserted.add(1, "week").startOf('week').toISOString();
|
|
788
|
+
if (frequency === "month") scdEntry.startTime = lastInserted.add(1, "month").startOf('month').toISOString();
|
|
789
|
+
}
|
|
790
|
+
|
|
791
|
+
if (timing === 'fuzzy') {
|
|
792
|
+
scdEntry.startTime = lastInserted.toISOString();
|
|
793
|
+
}
|
|
794
|
+
|
|
795
|
+
const insertTime = lastInserted.add(u.integer(1, 9000), "seconds");
|
|
796
|
+
scdEntry.insertTime = insertTime.toISOString();
|
|
797
|
+
|
|
798
|
+
|
|
799
|
+
|
|
642
800
|
// Ensure TypeScript sees all required properties are set
|
|
643
801
|
if (scdEntry.hasOwnProperty('insertTime') && scdEntry.hasOwnProperty('startTime')) {
|
|
644
802
|
scdEntries.push(scdEntry);
|
|
645
803
|
}
|
|
646
804
|
|
|
805
|
+
//advance time for next entry
|
|
647
806
|
lastInserted = lastInserted
|
|
648
807
|
.add(u.integer(0, deltaDays), "day")
|
|
649
|
-
.subtract(u.integer(1,
|
|
808
|
+
.subtract(u.integer(1, 9000), "seconds");
|
|
650
809
|
}
|
|
651
810
|
|
|
652
|
-
|
|
811
|
+
//de-dupe on startTime
|
|
812
|
+
const deduped = scdEntries.filter((entry, index, self) =>
|
|
813
|
+
index === self.findIndex((t) => (
|
|
814
|
+
t.startTime === entry.startTime
|
|
815
|
+
))
|
|
816
|
+
);
|
|
817
|
+
return deduped;
|
|
653
818
|
}
|
|
654
819
|
|
|
655
820
|
|
|
@@ -803,18 +968,24 @@ async function userLoop(config, storage, concurrency = 1) {
|
|
|
803
968
|
userProps,
|
|
804
969
|
scdProps,
|
|
805
970
|
numDays,
|
|
971
|
+
percentUsersBornInDataset = 5,
|
|
806
972
|
} = config;
|
|
807
973
|
const { eventData, userProfilesData, scdTableData } = storage;
|
|
808
974
|
const avgEvPerUser = numEvents / numUsers;
|
|
975
|
+
const startTime = Date.now();
|
|
809
976
|
|
|
810
|
-
for (let i =
|
|
977
|
+
for (let i = 0; i < numUsers; i++) {
|
|
811
978
|
await USER_CONN(async () => {
|
|
812
979
|
userCount++;
|
|
813
|
-
|
|
980
|
+
const eps = Math.floor(eventCount / ((Date.now() - startTime) / 1000));
|
|
981
|
+
if (verbose) u.progress([["users", userCount], ["events", eventCount], ["eps", eps]]);
|
|
814
982
|
const userId = chance.guid();
|
|
815
983
|
const user = u.generateUser(userId, { numDays, isAnonymous, hasAvatar, hasAnonIds, hasSessionIds });
|
|
816
984
|
const { distinct_id, created } = user;
|
|
985
|
+
const userIsBornInDataset = chance.bool({ likelihood: percentUsersBornInDataset });
|
|
817
986
|
let numEventsPreformed = 0;
|
|
987
|
+
if (!userIsBornInDataset) delete user.created;
|
|
988
|
+
const adjustedCreated = userIsBornInDataset ? dayjs(created).subtract(daysShift, 'd') : dayjs.unix(global.FIXED_BEGIN);
|
|
818
989
|
|
|
819
990
|
if (hasLocation) {
|
|
820
991
|
const location = u.choose(DEFAULTS.locationsUsers);
|
|
@@ -822,19 +993,24 @@ async function userLoop(config, storage, concurrency = 1) {
|
|
|
822
993
|
user[key] = location[key];
|
|
823
994
|
}
|
|
824
995
|
}
|
|
825
|
-
|
|
996
|
+
|
|
826
997
|
// Profile creation
|
|
827
998
|
const profile = await makeProfile(userProps, user);
|
|
828
|
-
|
|
999
|
+
|
|
829
1000
|
|
|
830
1001
|
// SCD creation
|
|
831
|
-
const
|
|
1002
|
+
const scdUserTables = t.objFilter(scdProps, (scd) => scd.type === 'user');
|
|
1003
|
+
const scdTableKeys = Object.keys(scdUserTables);
|
|
1004
|
+
|
|
1005
|
+
|
|
832
1006
|
const userSCD = {};
|
|
833
1007
|
for (const [index, key] of scdTableKeys.entries()) {
|
|
834
|
-
|
|
1008
|
+
// @ts-ignore
|
|
1009
|
+
const { max = 100 } = scdProps[key];
|
|
1010
|
+
const mutations = chance.integer({ min: 1, max });
|
|
835
1011
|
const changes = await makeSCD(scdProps[key], key, distinct_id, mutations, created);
|
|
836
1012
|
userSCD[key] = changes;
|
|
837
|
-
await
|
|
1013
|
+
await config.hook(changes, "scd-pre", { profile, type: 'user', scd: { [key]: scdProps[key] }, config, allSCDs: userSCD });
|
|
838
1014
|
}
|
|
839
1015
|
|
|
840
1016
|
let numEventsThisUserWillPreform = Math.floor(chance.normal({
|
|
@@ -849,23 +1025,30 @@ async function userLoop(config, storage, concurrency = 1) {
|
|
|
849
1025
|
|
|
850
1026
|
let userFirstEventTime;
|
|
851
1027
|
|
|
852
|
-
// First funnel logic...
|
|
853
1028
|
const firstFunnels = funnels.filter((f) => f.isFirstFunnel).reduce(u.weighFunnels, []);
|
|
854
1029
|
const usageFunnels = funnels.filter((f) => !f.isFirstFunnel).reduce(u.weighFunnels, []);
|
|
855
|
-
|
|
1030
|
+
|
|
1031
|
+
const secondsInDay = 86400;
|
|
1032
|
+
const noise = () => chance.integer({ min: 0, max: secondsInDay });
|
|
1033
|
+
let usersEvents = [];
|
|
856
1034
|
|
|
857
1035
|
if (firstFunnels.length && userIsBornInDataset) {
|
|
858
1036
|
const firstFunnel = chance.pickone(firstFunnels, user);
|
|
859
|
-
|
|
860
|
-
|
|
1037
|
+
|
|
1038
|
+
const firstTime = adjustedCreated.subtract(noise(), 'seconds').unix();
|
|
1039
|
+
const [data, userConverted] = await makeFunnel(firstFunnel, user, firstTime, profile, userSCD, config);
|
|
1040
|
+
userFirstEventTime = dayjs(data[0].time).subtract(timeShift, 'seconds').unix();
|
|
861
1041
|
numEventsPreformed += data.length;
|
|
862
|
-
await eventData.hookPush(data);
|
|
1042
|
+
// await eventData.hookPush(data, { profile });
|
|
1043
|
+
usersEvents.push(...data);
|
|
863
1044
|
if (!userConverted) {
|
|
864
1045
|
if (verbose) u.progress([["users", userCount], ["events", eventCount]]);
|
|
865
1046
|
return;
|
|
866
1047
|
}
|
|
867
1048
|
} else {
|
|
868
|
-
userFirstEventTime = dayjs(created).unix();
|
|
1049
|
+
// userFirstEventTime = dayjs(created).unix();
|
|
1050
|
+
// userFirstEventTime = global.FIXED_BEGIN;
|
|
1051
|
+
userFirstEventTime = adjustedCreated.subtract(noise(), 'seconds').unix();
|
|
869
1052
|
}
|
|
870
1053
|
|
|
871
1054
|
while (numEventsPreformed < numEventsThisUserWillPreform) {
|
|
@@ -873,14 +1056,35 @@ async function userLoop(config, storage, concurrency = 1) {
|
|
|
873
1056
|
const currentFunnel = chance.pickone(usageFunnels);
|
|
874
1057
|
const [data, userConverted] = await makeFunnel(currentFunnel, user, userFirstEventTime, profile, userSCD, config);
|
|
875
1058
|
numEventsPreformed += data.length;
|
|
876
|
-
|
|
1059
|
+
usersEvents.push(...data);
|
|
1060
|
+
// await eventData.hookPush(data, { profile });
|
|
877
1061
|
} else {
|
|
878
1062
|
const data = await makeEvent(distinct_id, userFirstEventTime, u.choose(config.events), user.anonymousIds, user.sessionIds, {}, config.groupKeys, true);
|
|
879
1063
|
numEventsPreformed++;
|
|
880
|
-
|
|
1064
|
+
usersEvents.push(data);
|
|
1065
|
+
// await eventData.hookPush(data);
|
|
881
1066
|
}
|
|
882
1067
|
}
|
|
883
1068
|
|
|
1069
|
+
// NOW ADD ALL OUR DATA FOR THIS USER
|
|
1070
|
+
if (config.hook) {
|
|
1071
|
+
const newEvents = await config.hook(usersEvents, "everything", { profile, scd: userSCD, config, userIsBornInDataset });
|
|
1072
|
+
if (Array.isArray(newEvents)) usersEvents = newEvents;
|
|
1073
|
+
}
|
|
1074
|
+
|
|
1075
|
+
await userProfilesData.hookPush(profile);
|
|
1076
|
+
|
|
1077
|
+
if (Object.keys(userSCD).length) {
|
|
1078
|
+
for (const [key, changesArray] of Object.entries(userSCD)) {
|
|
1079
|
+
for (const changes of changesArray) {
|
|
1080
|
+
const target = scdTableData.filter(arr => arr.scdKey === key).pop();
|
|
1081
|
+
await target.hookPush(changes, { profile, type: 'user' });
|
|
1082
|
+
}
|
|
1083
|
+
}
|
|
1084
|
+
}
|
|
1085
|
+
await eventData.hookPush(usersEvents, { profile });
|
|
1086
|
+
|
|
1087
|
+
|
|
884
1088
|
if (verbose) u.progress([["users", userCount], ["events", eventCount]]);
|
|
885
1089
|
});
|
|
886
1090
|
}
|
|
@@ -895,8 +1099,18 @@ async function userLoop(config, storage, concurrency = 1) {
|
|
|
895
1099
|
* @param {Storage} storage
|
|
896
1100
|
*/
|
|
897
1101
|
async function sendToMixpanel(config, storage) {
|
|
898
|
-
const {
|
|
899
|
-
|
|
1102
|
+
const {
|
|
1103
|
+
adSpendData,
|
|
1104
|
+
eventData,
|
|
1105
|
+
groupProfilesData,
|
|
1106
|
+
lookupTableData,
|
|
1107
|
+
mirrorEventData,
|
|
1108
|
+
scdTableData,
|
|
1109
|
+
userProfilesData,
|
|
1110
|
+
groupEventData
|
|
1111
|
+
|
|
1112
|
+
} = storage;
|
|
1113
|
+
const { token, region, writeToDisk = true } = config;
|
|
900
1114
|
const importResults = { events: {}, users: {}, groups: [] };
|
|
901
1115
|
|
|
902
1116
|
/** @type {import('mixpanel-import').Creds} */
|
|
@@ -913,7 +1127,7 @@ async function sendToMixpanel(config, storage) {
|
|
|
913
1127
|
dryRun: false,
|
|
914
1128
|
abridged: false,
|
|
915
1129
|
fixJson: true,
|
|
916
|
-
showProgress: true,
|
|
1130
|
+
showProgress: NODE_ENV === "dev" ? true : false,
|
|
917
1131
|
streamFormat: mpImportFormat
|
|
918
1132
|
};
|
|
919
1133
|
|
|
@@ -949,7 +1163,7 @@ async function sendToMixpanel(config, storage) {
|
|
|
949
1163
|
log(`\tsent ${comma(imported.success)} user profiles\n`);
|
|
950
1164
|
importResults.users = imported;
|
|
951
1165
|
}
|
|
952
|
-
if (
|
|
1166
|
+
if (groupEventData || isBATCH_MODE) {
|
|
953
1167
|
log(`importing ad spend data to mixpanel...\n`);
|
|
954
1168
|
let adSpendDataToImport = clone(adSpendData);
|
|
955
1169
|
if (isBATCH_MODE) {
|
|
@@ -985,11 +1199,65 @@ async function sendToMixpanel(config, storage) {
|
|
|
985
1199
|
}
|
|
986
1200
|
}
|
|
987
1201
|
|
|
1202
|
+
if (groupEventData || isBATCH_MODE) {
|
|
1203
|
+
log(`importing group events to mixpanel...\n`);
|
|
1204
|
+
let groupEventDataToImport = clone(groupEventData);
|
|
1205
|
+
if (isBATCH_MODE) {
|
|
1206
|
+
const writeDir = groupEventData.getWriteDir();
|
|
1207
|
+
const files = await ls(writeDir.split(path.basename(writeDir)).join(""));
|
|
1208
|
+
groupEventDataToImport = files.filter(f => f.includes('-GROUP-EVENTS-'));
|
|
1209
|
+
}
|
|
1210
|
+
const imported = await mp(creds, groupEventDataToImport, {
|
|
1211
|
+
recordType: "event",
|
|
1212
|
+
...commonOpts,
|
|
1213
|
+
strict: false
|
|
1214
|
+
});
|
|
1215
|
+
log(`\tsent ${comma(imported.success)} group events\n`);
|
|
1216
|
+
importResults.groupEvents = imported;
|
|
1217
|
+
}
|
|
1218
|
+
const { serviceAccount, projectId, serviceSecret } = config;
|
|
1219
|
+
if (serviceAccount && projectId && serviceSecret) {
|
|
1220
|
+
if (scdTableData || isBATCH_MODE) {
|
|
1221
|
+
log(`importing SCD data to mixpanel...\n`);
|
|
1222
|
+
for (const scdEntity of scdTableData) {
|
|
1223
|
+
const scdKey = scdEntity?.scdKey;
|
|
1224
|
+
log(`importing ${scdKey} SCD data to mixpanel...\n`);
|
|
1225
|
+
let scdDataToImport = clone(scdEntity);
|
|
1226
|
+
if (isBATCH_MODE) {
|
|
1227
|
+
const writeDir = scdEntity.getWriteDir();
|
|
1228
|
+
const files = await ls(writeDir.split(path.basename(writeDir)).join(""));
|
|
1229
|
+
scdDataToImport = files.filter(f => f.includes(`-SCD-${scdKey}`));
|
|
1230
|
+
}
|
|
1231
|
+
|
|
1232
|
+
const options = {
|
|
1233
|
+
recordType: "scd",
|
|
1234
|
+
scdKey,
|
|
1235
|
+
scdType: scdEntity.dataType,
|
|
1236
|
+
scdLabel: `${scdKey}-scd`,
|
|
1237
|
+
...commonOpts,
|
|
1238
|
+
};
|
|
1239
|
+
if (scdEntity.entityType !== "user") options.groupKey = scdEntity.entityType
|
|
1240
|
+
const imported = await mp(
|
|
1241
|
+
{
|
|
1242
|
+
token,
|
|
1243
|
+
acct: serviceAccount,
|
|
1244
|
+
pass: serviceSecret,
|
|
1245
|
+
project: projectId
|
|
1246
|
+
},
|
|
1247
|
+
scdDataToImport,
|
|
1248
|
+
// @ts-ignore
|
|
1249
|
+
options);
|
|
1250
|
+
log(`\tsent ${comma(imported.success)} ${scdKey} SCD data\n`);
|
|
1251
|
+
importResults[`${scdKey}_scd`] = imported;
|
|
1252
|
+
}
|
|
1253
|
+
}
|
|
1254
|
+
}
|
|
1255
|
+
|
|
988
1256
|
//if we are in batch mode, we need to delete the files
|
|
989
1257
|
if (!writeToDisk && isBATCH_MODE) {
|
|
990
1258
|
const writeDir = eventData?.getWriteDir() || userProfilesData?.getWriteDir();
|
|
991
1259
|
const listDir = await ls(writeDir.split(path.basename(writeDir)).join(""));
|
|
992
|
-
const files = listDir.filter(f => f.includes('-EVENTS-') || f.includes('-USERS-') || f.includes('-AD-SPEND-') || f.includes('-GROUPS-'));
|
|
1260
|
+
const files = listDir.filter(f => f.includes('-EVENTS-') || f.includes('-USERS-') || f.includes('-AD-SPEND-') || f.includes('-GROUPS-') || f.includes('-GROUP-EVENTS-'));
|
|
993
1261
|
for (const file of files) {
|
|
994
1262
|
await rm(file);
|
|
995
1263
|
}
|
|
@@ -1048,6 +1316,7 @@ function validateDungeonConfig(config) {
|
|
|
1048
1316
|
hasAndroidDevices = false,
|
|
1049
1317
|
hasDesktopDevices = false,
|
|
1050
1318
|
hasIOSDevices = false,
|
|
1319
|
+
alsoInferFunnels = false,
|
|
1051
1320
|
name = "",
|
|
1052
1321
|
batchSize = 500_000,
|
|
1053
1322
|
concurrency = 500
|
|
@@ -1076,6 +1345,48 @@ function validateDungeonConfig(config) {
|
|
|
1076
1345
|
funnels = inferFunnels(events);
|
|
1077
1346
|
}
|
|
1078
1347
|
|
|
1348
|
+
if (alsoInferFunnels) {
|
|
1349
|
+
const inferredFunnels = inferFunnels(events);
|
|
1350
|
+
funnels = [...funnels, ...inferredFunnels];
|
|
1351
|
+
}
|
|
1352
|
+
|
|
1353
|
+
|
|
1354
|
+
const eventContainedInFunnels = Array.from(funnels.reduce((acc, f) => {
|
|
1355
|
+
const events = f.sequence;
|
|
1356
|
+
events.forEach(event => acc.add(event));
|
|
1357
|
+
return acc;
|
|
1358
|
+
}, new Set()));
|
|
1359
|
+
|
|
1360
|
+
const eventsNotInFunnels = events
|
|
1361
|
+
.filter(e => !e.isFirstEvent)
|
|
1362
|
+
.filter(e => !eventContainedInFunnels.includes(e.event)).map(e => e.event);
|
|
1363
|
+
if (eventsNotInFunnels.length) {
|
|
1364
|
+
// const biggestWeight = funnels.reduce((acc, f) => {
|
|
1365
|
+
// if (f.weight > acc) return f.weight;
|
|
1366
|
+
// return acc;
|
|
1367
|
+
// }, 0);
|
|
1368
|
+
// const smallestWeight = funnels.reduce((acc, f) => {
|
|
1369
|
+
// if (f.weight < acc) return f.weight;
|
|
1370
|
+
// return acc;
|
|
1371
|
+
// }, 0);
|
|
1372
|
+
// const weight = u.integer(smallestWeight, biggestWeight) * 2;
|
|
1373
|
+
|
|
1374
|
+
const sequence = u.shuffleArray(eventsNotInFunnels.flatMap(event => {
|
|
1375
|
+
const evWeight = config.events.find(e => e.event === event)?.weight || 1;
|
|
1376
|
+
return Array(evWeight).fill(event);
|
|
1377
|
+
}));
|
|
1378
|
+
|
|
1379
|
+
|
|
1380
|
+
|
|
1381
|
+
funnels.push({
|
|
1382
|
+
sequence,
|
|
1383
|
+
conversionRate: 50,
|
|
1384
|
+
order: 'random',
|
|
1385
|
+
timeToConvert: 24 * 14,
|
|
1386
|
+
requireRepeats: false,
|
|
1387
|
+
});
|
|
1388
|
+
}
|
|
1389
|
+
|
|
1079
1390
|
config.concurrency = concurrency;
|
|
1080
1391
|
config.funnels = funnels;
|
|
1081
1392
|
config.batchSize = batchSize;
|
|
@@ -1139,6 +1450,8 @@ async function makeHookArray(arr = [], opts = {}) {
|
|
|
1139
1450
|
if (existsSync(dataFolder)) writeDir = dataFolder;
|
|
1140
1451
|
else writeDir = path.resolve("./");
|
|
1141
1452
|
|
|
1453
|
+
if (NODE_ENV === "prod") writeDir = path.resolve(os.tmpdir());
|
|
1454
|
+
|
|
1142
1455
|
function getWritePath() {
|
|
1143
1456
|
if (isBATCH_MODE) {
|
|
1144
1457
|
return path.join(writeDir, `${filepath}-part-${batch.toString()}.${format}`);
|
|
@@ -1152,14 +1465,14 @@ async function makeHookArray(arr = [], opts = {}) {
|
|
|
1152
1465
|
return path.join(writeDir, `${filepath}.${format}`);
|
|
1153
1466
|
}
|
|
1154
1467
|
|
|
1155
|
-
async function transformThenPush(item) {
|
|
1468
|
+
async function transformThenPush(item, meta) {
|
|
1156
1469
|
if (item === null || item === undefined) return false;
|
|
1157
1470
|
if (typeof item === 'object' && Object.keys(item).length === 0) return false;
|
|
1158
|
-
|
|
1471
|
+
const allMetaData = { ...rest, ...meta };
|
|
1159
1472
|
if (Array.isArray(item)) {
|
|
1160
1473
|
for (const i of item) {
|
|
1161
1474
|
try {
|
|
1162
|
-
const enriched = await hook(i, type,
|
|
1475
|
+
const enriched = await hook(i, type, allMetaData);
|
|
1163
1476
|
if (Array.isArray(enriched)) enriched.forEach(e => arr.push(e));
|
|
1164
1477
|
else arr.push(enriched);
|
|
1165
1478
|
} catch (e) {
|
|
@@ -1169,7 +1482,7 @@ async function makeHookArray(arr = [], opts = {}) {
|
|
|
1169
1482
|
}
|
|
1170
1483
|
} else {
|
|
1171
1484
|
try {
|
|
1172
|
-
const enriched = await hook(item, type,
|
|
1485
|
+
const enriched = await hook(item, type, allMetaData);
|
|
1173
1486
|
if (Array.isArray(enriched)) enriched.forEach(e => arr.push(e));
|
|
1174
1487
|
else arr.push(enriched);
|
|
1175
1488
|
} catch (e) {
|
|
@@ -1284,92 +1597,93 @@ CLI
|
|
|
1284
1597
|
----
|
|
1285
1598
|
*/
|
|
1286
1599
|
|
|
1287
|
-
if (
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
else {
|
|
1300
|
-
if (complex) {
|
|
1301
|
-
console.log(`... using default COMPLEX configuration [everything] ...\n`);
|
|
1302
|
-
console.log(`... for more simple data, don't use the --complex flag ...\n`);
|
|
1303
|
-
console.log(`... or specify your own js config file (see docs or --help) ...\n`);
|
|
1304
|
-
config = require(path.resolve(__dirname, "./schemas/complex.js"));
|
|
1600
|
+
if (NODE_ENV !== "prod") {
|
|
1601
|
+
if (require.main === module) {
|
|
1602
|
+
isCLI = true;
|
|
1603
|
+
const args = /** @type {Config} */ (getCliParams());
|
|
1604
|
+
let { token, seed, format, numDays, numUsers, numEvents, region, writeToDisk, complex = false, hasSessionIds, hasAnonIds } = args;
|
|
1605
|
+
const suppliedConfig = args._[0];
|
|
1606
|
+
|
|
1607
|
+
//if the user specifies an separate config file
|
|
1608
|
+
let config = null;
|
|
1609
|
+
if (suppliedConfig) {
|
|
1610
|
+
console.log(`using ${suppliedConfig} for data\n`);
|
|
1611
|
+
config = require(path.resolve(suppliedConfig));
|
|
1305
1612
|
}
|
|
1306
1613
|
else {
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1614
|
+
if (complex) {
|
|
1615
|
+
console.log(`... using default COMPLEX configuration [everything] ...\n`);
|
|
1616
|
+
console.log(`... for more simple data, don't use the --complex flag ...\n`);
|
|
1617
|
+
console.log(`... or specify your own js config file (see docs or --help) ...\n`);
|
|
1618
|
+
config = require(path.resolve(__dirname, "./dungeons/complex.js"));
|
|
1619
|
+
}
|
|
1620
|
+
else {
|
|
1621
|
+
console.log(`... using default SIMPLE configuration [events + users] ...\n`);
|
|
1622
|
+
console.log(`... for more complex data, use the --complex flag ...\n`);
|
|
1623
|
+
config = require(path.resolve(__dirname, "./dungeons/simple.js"));
|
|
1624
|
+
}
|
|
1310
1625
|
}
|
|
1311
|
-
}
|
|
1312
1626
|
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
}
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
module.exports = main;
|
|
1627
|
+
//override config with cli params
|
|
1628
|
+
if (token) config.token = token;
|
|
1629
|
+
if (seed) config.seed = seed;
|
|
1630
|
+
if (format === "csv" && config.format === "json") format = "json";
|
|
1631
|
+
if (format) config.format = format;
|
|
1632
|
+
if (numDays) config.numDays = numDays;
|
|
1633
|
+
if (numUsers) config.numUsers = numUsers;
|
|
1634
|
+
if (numEvents) config.numEvents = numEvents;
|
|
1635
|
+
if (region) config.region = region;
|
|
1636
|
+
if (writeToDisk) config.writeToDisk = writeToDisk;
|
|
1637
|
+
if (writeToDisk === 'false') config.writeToDisk = false;
|
|
1638
|
+
if (hasSessionIds) config.hasSessionIds = hasSessionIds;
|
|
1639
|
+
if (hasAnonIds) config.hasAnonIds = hasAnonIds;
|
|
1640
|
+
config.verbose = true;
|
|
1641
|
+
|
|
1642
|
+
main(config)
|
|
1643
|
+
.then((data) => {
|
|
1644
|
+
log(`-----------------SUMMARY-----------------`);
|
|
1645
|
+
const d = { success: 0, bytes: 0 };
|
|
1646
|
+
const darr = [d];
|
|
1647
|
+
const { events = d, groups = darr, users = d } = data?.importResults || {};
|
|
1648
|
+
const files = data.files;
|
|
1649
|
+
const folder = files?.[0]?.split(path.basename(files?.[0]))?.shift() || "./data";
|
|
1650
|
+
const groupBytes = groups.reduce((acc, group) => {
|
|
1651
|
+
return acc + group.bytes;
|
|
1652
|
+
}, 0);
|
|
1653
|
+
const groupSuccess = groups.reduce((acc, group) => {
|
|
1654
|
+
return acc + group.success;
|
|
1655
|
+
}, 0);
|
|
1656
|
+
const bytes = events.bytes + groupBytes + users.bytes;
|
|
1657
|
+
const stats = {
|
|
1658
|
+
events: comma(events.success || 0),
|
|
1659
|
+
users: comma(users.success || 0),
|
|
1660
|
+
groups: comma(groupSuccess || 0),
|
|
1661
|
+
bytes: bytesHuman(bytes || 0),
|
|
1662
|
+
};
|
|
1663
|
+
if (bytes > 0) console.table(stats);
|
|
1664
|
+
log(`\nfiles written to ${folder || "no where; we didn't write anything"} ...`);
|
|
1665
|
+
log(" " + files?.flat().join("\n "));
|
|
1666
|
+
log(`\n----------------SUMMARY-----------------\n\n\n`);
|
|
1667
|
+
})
|
|
1668
|
+
.catch((e) => {
|
|
1669
|
+
log(`------------------ERROR------------------`);
|
|
1670
|
+
console.error(e);
|
|
1671
|
+
log(`------------------ERROR------------------`);
|
|
1672
|
+
debugger;
|
|
1673
|
+
})
|
|
1674
|
+
.finally(() => {
|
|
1675
|
+
log("enjoy your data! :)");
|
|
1676
|
+
});
|
|
1677
|
+
} else {
|
|
1678
|
+
main.generators = { makeEvent, makeFunnel, makeProfile, makeSCD, makeAdSpend, makeMirror };
|
|
1679
|
+
main.orchestrators = { userLoop, validateDungeonConfig, sendToMixpanel };
|
|
1680
|
+
main.meta = { inferFunnels, hookArray: makeHookArray };
|
|
1681
|
+
module.exports = main;
|
|
1682
|
+
}
|
|
1370
1683
|
}
|
|
1371
1684
|
|
|
1372
1685
|
|
|
1686
|
+
|
|
1373
1687
|
/*
|
|
1374
1688
|
----
|
|
1375
1689
|
HELPERS
|
|
@@ -1394,7 +1708,7 @@ function track(name, props, ...rest) {
|
|
|
1394
1708
|
}
|
|
1395
1709
|
|
|
1396
1710
|
|
|
1397
|
-
/** @typedef {import('./types.js').
|
|
1711
|
+
/** @typedef {import('./types.js').Dungeon} Config */
|
|
1398
1712
|
/** @typedef {import('./types.js').AllData} AllData */
|
|
1399
1713
|
/** @typedef {import('./types.js').EventConfig} EventConfig */
|
|
1400
1714
|
/** @typedef {import('./types.js').Funnel} Funnel */
|
|
@@ -1407,4 +1721,5 @@ function track(name, props, ...rest) {
|
|
|
1407
1721
|
/** @typedef {import('./types.js').ValueValid} ValueValid */
|
|
1408
1722
|
/** @typedef {import('./types.js').HookedArray} hookArray */
|
|
1409
1723
|
/** @typedef {import('./types.js').hookArrayOptions} hookArrayOptions */
|
|
1410
|
-
/** @typedef {import('./types.js').GroupProfileSchema} GroupProfile */
|
|
1724
|
+
/** @typedef {import('./types.js').GroupProfileSchema} GroupProfile */
|
|
1725
|
+
/** @typedef {import('./types.js').SCDProp} SCDProp */
|