make-mp-data 1.5.1 → 1.5.3
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 +37 -14
- package/.vscode/settings.json +2 -0
- package/.vscode/tasks.json +12 -0
- package/components/ai.js +93 -0
- package/components/chart.js +14 -0
- package/components/cli.js +8 -2
- package/components/project.js +11 -0
- package/components/prompt.txt +98 -0
- package/components/utils.js +126 -5
- package/{schemas → dungeons}/adspend.js +1 -1
- package/{schemas → dungeons}/anon.js +1 -1
- package/{schemas → dungeons}/big.js +1 -1
- package/{schemas → dungeons}/business.js +1 -1
- package/{schemas → dungeons}/complex.js +9 -9
- package/dungeons/foobar.js +241 -0
- package/{schemas → dungeons}/funnels.js +2 -3
- package/dungeons/gaming.js +314 -0
- package/{schemas → dungeons}/mirror.js +1 -1
- package/{schemas → dungeons}/sanity.js +1 -1
- package/dungeons/scd.js +205 -0
- package/dungeons/session-replay.js +175 -0
- package/{schemas → dungeons}/simple.js +1 -1
- package/dungeons/userAgent.js +190 -0
- package/env.yaml +1 -0
- package/index.js +453 -154
- package/package.json +9 -5
- package/scripts/deploy.sh +11 -0
- package/scripts/new-dungeon.sh +10 -4
- 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 +3 -2
- package/tests/jest.config.js +8 -0
- package/tests/unit.test.js +1 -1
- package/tsconfig.json +1 -1
- package/types.d.ts +40 -9
- package/schemas/foobar.js +0 -125
- package/schemas/session-replay.js +0 -136
- /package/dungeons/{.gitkeep → customers/.gitkeep} +0 -0
package/index.js
CHANGED
|
@@ -10,6 +10,8 @@ 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
|
|
@@ -20,16 +22,17 @@ const FIXED_NOW = dayjs('2024-02-02').unix();
|
|
|
20
22
|
global.FIXED_NOW = FIXED_NOW;
|
|
21
23
|
// ^ this creates a FIXED POINT in time; we will shift it later
|
|
22
24
|
let FIXED_BEGIN = dayjs.unix(FIXED_NOW).subtract(90, 'd').unix();
|
|
25
|
+
global.FIXED_BEGIN = FIXED_BEGIN;
|
|
23
26
|
const actualNow = dayjs();
|
|
24
27
|
const timeShift = actualNow.diff(dayjs.unix(FIXED_NOW), "seconds");
|
|
25
28
|
const daysShift = actualNow.diff(dayjs.unix(FIXED_NOW), "days");
|
|
26
29
|
|
|
27
30
|
// UTILS
|
|
28
|
-
const { existsSync } = require("fs");
|
|
31
|
+
const { existsSync, writeFileSync } = require("fs");
|
|
29
32
|
const pLimit = require('p-limit');
|
|
30
33
|
const os = require("os");
|
|
31
34
|
const path = require("path");
|
|
32
|
-
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");
|
|
33
36
|
const jobTimer = timer('job');
|
|
34
37
|
const { generateLineChart } = require('./components/chart.js');
|
|
35
38
|
const { version } = require('./package.json');
|
|
@@ -37,6 +40,11 @@ const mp = require("mixpanel-import");
|
|
|
37
40
|
const u = require("./components/utils.js");
|
|
38
41
|
const getCliParams = require("./components/cli.js");
|
|
39
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');
|
|
40
48
|
|
|
41
49
|
// DEFAULTS
|
|
42
50
|
const { campaigns, devices, locations } = require('./components/defaults.js');
|
|
@@ -48,13 +56,17 @@ let STORAGE;
|
|
|
48
56
|
let CONFIG;
|
|
49
57
|
require('dotenv').config();
|
|
50
58
|
|
|
59
|
+
const { NODE_ENV = "unknown" } = process.env;
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
|
|
51
63
|
|
|
52
64
|
// RUN STATE
|
|
53
65
|
let VERBOSE = false;
|
|
54
66
|
let isCLI = false;
|
|
55
67
|
// if we are running in batch mode, we MUST write to disk before we can send to mixpanel
|
|
56
68
|
let isBATCH_MODE = false;
|
|
57
|
-
let BATCH_SIZE =
|
|
69
|
+
let BATCH_SIZE = 1_000_000;
|
|
58
70
|
|
|
59
71
|
//todo: these should be moved into the hookedArrays
|
|
60
72
|
let operations = 0;
|
|
@@ -95,7 +107,7 @@ async function main(config) {
|
|
|
95
107
|
|
|
96
108
|
//TRACKING
|
|
97
109
|
const runId = uid(42);
|
|
98
|
-
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;
|
|
99
111
|
let { funnels } = config;
|
|
100
112
|
trackingParams.runId = runId;
|
|
101
113
|
trackingParams.version = version;
|
|
@@ -106,11 +118,14 @@ async function main(config) {
|
|
|
106
118
|
const eventData = await makeHookArray([], { hook, type: "event", config, format, filepath: `${simulationName}-EVENTS` });
|
|
107
119
|
const userProfilesData = await makeHookArray([], { hook, type: "user", config, format, filepath: `${simulationName}-USERS` });
|
|
108
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` });
|
|
109
122
|
|
|
110
123
|
// SCDs, Groups, + Lookups may have multiple tables
|
|
111
124
|
const scdTableKeys = Object.keys(scdProps);
|
|
112
125
|
const scdTableData = await Promise.all(scdTableKeys.map(async (key) =>
|
|
113
|
-
|
|
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}` })
|
|
114
129
|
));
|
|
115
130
|
const groupTableKeys = Object.keys(groupKeys);
|
|
116
131
|
const groupProfilesData = await Promise.all(groupTableKeys.map(async (key, index) => {
|
|
@@ -126,7 +141,17 @@ async function main(config) {
|
|
|
126
141
|
|
|
127
142
|
const mirrorEventData = await makeHookArray([], { hook, type: "mirror", config, format, filepath: `${simulationName}-MIRROR` });
|
|
128
143
|
|
|
129
|
-
STORAGE = {
|
|
144
|
+
STORAGE = {
|
|
145
|
+
eventData,
|
|
146
|
+
userProfilesData,
|
|
147
|
+
scdTableData,
|
|
148
|
+
groupProfilesData,
|
|
149
|
+
lookupTableData,
|
|
150
|
+
mirrorEventData,
|
|
151
|
+
adSpendData,
|
|
152
|
+
groupEventData
|
|
153
|
+
|
|
154
|
+
};
|
|
130
155
|
|
|
131
156
|
|
|
132
157
|
track('start simulation', trackingParams);
|
|
@@ -159,22 +184,72 @@ async function main(config) {
|
|
|
159
184
|
log("\n");
|
|
160
185
|
|
|
161
186
|
//GROUP PROFILES
|
|
187
|
+
const groupSCDs = t.objFilter(scdProps, (scd) => scd.type !== 'user');
|
|
162
188
|
for (const [index, groupPair] of groupKeys.entries()) {
|
|
163
189
|
const groupKey = groupPair[0];
|
|
164
190
|
const groupCardinality = groupPair[1];
|
|
165
191
|
for (let i = 1; i < groupCardinality + 1; i++) {
|
|
166
192
|
if (VERBOSE) u.progress([["groups", i]]);
|
|
167
|
-
|
|
193
|
+
|
|
194
|
+
const props = await makeProfile(groupProps[groupKey], { created: () => { return dayjs().subtract(u.integer(0, CONFIG.numDays || 30), 'd').toISOString(); } });
|
|
168
195
|
const group = {
|
|
169
196
|
[groupKey]: i,
|
|
170
197
|
...props,
|
|
171
198
|
};
|
|
172
199
|
group["distinct_id"] = i.toString();
|
|
173
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
|
+
|
|
174
219
|
}
|
|
175
220
|
}
|
|
176
221
|
log("\n");
|
|
177
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
|
+
|
|
178
253
|
//LOOKUP TABLES
|
|
179
254
|
for (const [index, lookupTable] of lookupTables.entries()) {
|
|
180
255
|
const { key, entries, attributes } = lookupTable;
|
|
@@ -200,14 +275,14 @@ async function main(config) {
|
|
|
200
275
|
log(`---------------SIMULATION----------------`, "\n");
|
|
201
276
|
|
|
202
277
|
// draw charts
|
|
203
|
-
const { makeChart } = config;
|
|
278
|
+
const { makeChart = false } = config;
|
|
204
279
|
if (makeChart) {
|
|
205
280
|
const bornEvents = config.events?.filter((e) => e?.isFirstEvent)?.map(e => e.event) || [];
|
|
206
281
|
const bornFunnels = config.funnels?.filter((f) => f.isFirstFunnel)?.map(f => f.sequence[0]) || [];
|
|
207
282
|
const bornBehaviors = [...bornEvents, ...bornFunnels];
|
|
208
283
|
const chart = await generateLineChart(eventData, bornBehaviors, makeChart);
|
|
209
284
|
}
|
|
210
|
-
const { writeToDisk, token } = config;
|
|
285
|
+
const { writeToDisk = true, token } = config;
|
|
211
286
|
if (!writeToDisk && !token) {
|
|
212
287
|
jobTimer.stop(false);
|
|
213
288
|
const { start, end, delta, human } = jobTimer.report(false);
|
|
@@ -246,6 +321,7 @@ async function main(config) {
|
|
|
246
321
|
jobTimer.stop(false);
|
|
247
322
|
const { start, end, delta, human } = jobTimer.report(false);
|
|
248
323
|
|
|
324
|
+
if (process.env.NODE_ENV === 'dev') debugger;
|
|
249
325
|
return {
|
|
250
326
|
...STORAGE,
|
|
251
327
|
importResults,
|
|
@@ -256,6 +332,53 @@ async function main(config) {
|
|
|
256
332
|
|
|
257
333
|
|
|
258
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
|
+
|
|
259
382
|
|
|
260
383
|
/*
|
|
261
384
|
------
|
|
@@ -275,7 +398,7 @@ MODELS
|
|
|
275
398
|
* @param {Boolean} [isFirstEvent]
|
|
276
399
|
* @return {Promise<EventSchema>}
|
|
277
400
|
*/
|
|
278
|
-
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) {
|
|
279
402
|
operations++;
|
|
280
403
|
eventCount++;
|
|
281
404
|
if (!distinct_id) throw new Error("no distinct_id");
|
|
@@ -307,11 +430,13 @@ async function makeEvent(distinct_id, earliestTime, chosenEvent, anonymousIds, s
|
|
|
307
430
|
|
|
308
431
|
let defaultProps = {};
|
|
309
432
|
let devicePool = [];
|
|
433
|
+
|
|
310
434
|
if (hasLocation) defaultProps.location = DEFAULTS.locationsEvents();
|
|
311
435
|
if (hasBrowser) defaultProps.browser = DEFAULTS.browsers();
|
|
312
436
|
if (hasAndroidDevices) devicePool.push(DEFAULTS.androidDevices());
|
|
313
437
|
if (hasIOSDevices) devicePool.push(DEFAULTS.iOSDevices());
|
|
314
438
|
if (hasDesktopDevices) devicePool.push(DEFAULTS.desktopDevices());
|
|
439
|
+
|
|
315
440
|
// we don't always have campaigns, because of attribution
|
|
316
441
|
if (hasCampaigns && chance.bool({ likelihood: 25 })) defaultProps.campaigns = DEFAULTS.campaigns();
|
|
317
442
|
const devices = devicePool.flat();
|
|
@@ -319,13 +444,10 @@ async function makeEvent(distinct_id, earliestTime, chosenEvent, anonymousIds, s
|
|
|
319
444
|
|
|
320
445
|
|
|
321
446
|
//event time
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
if (isFirstEvent) eventTemplate.time = dayjs.unix(earliestTime).toISOString();
|
|
327
|
-
if (!isFirstEvent) eventTemplate.time = u.TimeSoup(earliestTime, FIXED_NOW, peaks, deviation, mean);
|
|
328
|
-
// eventTemplate.time = u.TimeSoup(earliestTime, FIXED_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
|
+
}
|
|
329
451
|
|
|
330
452
|
// anonymous and session ids
|
|
331
453
|
if (anonymousIds.length) eventTemplate.device_id = chance.pickone(anonymousIds);
|
|
@@ -350,39 +472,39 @@ async function makeEvent(distinct_id, earliestTime, chosenEvent, anonymousIds, s
|
|
|
350
472
|
}
|
|
351
473
|
|
|
352
474
|
//iterate through default properties
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
if (
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
else if (Array.isArray(choice)) {
|
|
361
|
-
for (const subChoice of choice) {
|
|
362
|
-
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;
|
|
363
481
|
}
|
|
364
|
-
}
|
|
365
482
|
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
if (!eventTemplate[subKey]) eventTemplate[subKey] = choice[subKey];
|
|
370
|
-
}
|
|
371
|
-
else if (Array.isArray(choice[subKey])) {
|
|
372
|
-
const subChoice = u.choose(choice[subKey]);
|
|
373
|
-
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;
|
|
374
486
|
}
|
|
487
|
+
}
|
|
375
488
|
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
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;
|
|
497
|
+
}
|
|
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
|
+
}
|
|
379
503
|
}
|
|
380
|
-
}
|
|
381
504
|
|
|
505
|
+
}
|
|
382
506
|
}
|
|
383
507
|
}
|
|
384
|
-
|
|
385
|
-
|
|
386
508
|
}
|
|
387
509
|
}
|
|
388
510
|
|
|
@@ -401,8 +523,10 @@ async function makeEvent(distinct_id, earliestTime, chosenEvent, anonymousIds, s
|
|
|
401
523
|
eventTemplate.insert_id = md5(JSON.stringify(eventTemplate));
|
|
402
524
|
|
|
403
525
|
// move time forward
|
|
404
|
-
|
|
405
|
-
|
|
526
|
+
if (earliestTime) {
|
|
527
|
+
const timeShifted = dayjs(eventTemplate.time).add(timeShift, "seconds").toISOString();
|
|
528
|
+
eventTemplate.time = timeShifted;
|
|
529
|
+
}
|
|
406
530
|
|
|
407
531
|
|
|
408
532
|
return eventTemplate;
|
|
@@ -492,6 +616,8 @@ async function makeFunnel(funnel, user, firstEventTime, profile, scd, config) {
|
|
|
492
616
|
return acc;
|
|
493
617
|
}, []);
|
|
494
618
|
|
|
619
|
+
if (conversionRate > 100) conversionRate = 100;
|
|
620
|
+
if (conversionRate < 0) conversionRate = 0;
|
|
495
621
|
let doesUserConvert = chance.bool({ likelihood: conversionRate });
|
|
496
622
|
let numStepsUserWillTake = sequence.length;
|
|
497
623
|
if (!doesUserConvert) numStepsUserWillTake = u.integer(1, sequence.length - 1);
|
|
@@ -604,12 +730,23 @@ async function makeProfile(props, defaults) {
|
|
|
604
730
|
...defaults,
|
|
605
731
|
};
|
|
606
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
|
+
|
|
607
744
|
for (const key in props) {
|
|
608
745
|
try {
|
|
609
746
|
profile[key] = u.choose(props[key]);
|
|
610
747
|
} catch (e) {
|
|
611
748
|
// never gets here
|
|
612
|
-
|
|
749
|
+
debugger;
|
|
613
750
|
}
|
|
614
751
|
}
|
|
615
752
|
|
|
@@ -617,42 +754,67 @@ async function makeProfile(props, defaults) {
|
|
|
617
754
|
}
|
|
618
755
|
|
|
619
756
|
/**
|
|
620
|
-
* @param {
|
|
757
|
+
* @param {SCDProp} scdProp
|
|
621
758
|
* @param {string} scdKey
|
|
622
759
|
* @param {string} distinct_id
|
|
623
760
|
* @param {number} mutations
|
|
624
761
|
* @param {string} created
|
|
625
762
|
* @return {Promise<SCDSchema[]>}
|
|
626
763
|
*/
|
|
627
|
-
async function makeSCD(
|
|
628
|
-
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 [];
|
|
629
768
|
const scdEntries = [];
|
|
630
769
|
let lastInserted = dayjs(created);
|
|
631
770
|
const deltaDays = dayjs().diff(lastInserted, "day");
|
|
771
|
+
const uuidKeyName = type === 'user' ? 'distinct_id' : type;
|
|
632
772
|
|
|
633
773
|
for (let i = 0; i < mutations; i++) {
|
|
634
774
|
if (lastInserted.isAfter(dayjs())) break;
|
|
635
|
-
let scd = await makeProfile({ [scdKey]:
|
|
775
|
+
let scd = await makeProfile({ [scdKey]: values }, { [uuidKeyName]: distinct_id });
|
|
636
776
|
|
|
637
777
|
// Explicitly constructing SCDSchema object with all required properties
|
|
638
778
|
const scdEntry = {
|
|
639
779
|
...scd, // spread existing properties
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
780
|
+
[uuidKeyName]: scd.distinct_id || distinct_id, // ensure distinct_id is set
|
|
781
|
+
startTime: null,
|
|
782
|
+
insertTime: null
|
|
643
783
|
};
|
|
644
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
|
+
|
|
645
800
|
// Ensure TypeScript sees all required properties are set
|
|
646
801
|
if (scdEntry.hasOwnProperty('insertTime') && scdEntry.hasOwnProperty('startTime')) {
|
|
647
802
|
scdEntries.push(scdEntry);
|
|
648
803
|
}
|
|
649
804
|
|
|
805
|
+
//advance time for next entry
|
|
650
806
|
lastInserted = lastInserted
|
|
651
807
|
.add(u.integer(0, deltaDays), "day")
|
|
652
|
-
.subtract(u.integer(1,
|
|
808
|
+
.subtract(u.integer(1, 9000), "seconds");
|
|
653
809
|
}
|
|
654
810
|
|
|
655
|
-
|
|
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;
|
|
656
818
|
}
|
|
657
819
|
|
|
658
820
|
|
|
@@ -806,24 +968,27 @@ async function userLoop(config, storage, concurrency = 1) {
|
|
|
806
968
|
userProps,
|
|
807
969
|
scdProps,
|
|
808
970
|
numDays,
|
|
971
|
+
percentUsersBornInDataset = 5,
|
|
809
972
|
} = config;
|
|
810
973
|
const { eventData, userProfilesData, scdTableData } = storage;
|
|
811
974
|
const avgEvPerUser = numEvents / numUsers;
|
|
975
|
+
const startTime = Date.now();
|
|
812
976
|
|
|
813
977
|
for (let i = 0; i < numUsers; i++) {
|
|
814
978
|
await USER_CONN(async () => {
|
|
815
979
|
userCount++;
|
|
816
|
-
|
|
980
|
+
const eps = Math.floor(eventCount / ((Date.now() - startTime) / 1000));
|
|
981
|
+
if (verbose) u.progress([["users", userCount], ["events", eventCount], ["eps", eps]]);
|
|
817
982
|
const userId = chance.guid();
|
|
818
983
|
const user = u.generateUser(userId, { numDays, isAnonymous, hasAvatar, hasAnonIds, hasSessionIds });
|
|
819
984
|
const { distinct_id, created } = user;
|
|
820
|
-
const userIsBornInDataset = chance.bool({ likelihood:
|
|
985
|
+
const userIsBornInDataset = chance.bool({ likelihood: percentUsersBornInDataset });
|
|
821
986
|
let numEventsPreformed = 0;
|
|
822
987
|
if (!userIsBornInDataset) delete user.created;
|
|
823
988
|
const adjustedCreated = userIsBornInDataset ? dayjs(created).subtract(daysShift, 'd') : dayjs.unix(global.FIXED_BEGIN);
|
|
824
989
|
|
|
825
990
|
if (hasLocation) {
|
|
826
|
-
const location = u.choose(DEFAULTS.locationsUsers);
|
|
991
|
+
const location = u.shuffleArray(u.choose(DEFAULTS.locationsUsers)).pop();
|
|
827
992
|
for (const key in location) {
|
|
828
993
|
user[key] = location[key];
|
|
829
994
|
}
|
|
@@ -831,16 +996,21 @@ async function userLoop(config, storage, concurrency = 1) {
|
|
|
831
996
|
|
|
832
997
|
// Profile creation
|
|
833
998
|
const profile = await makeProfile(userProps, user);
|
|
834
|
-
|
|
999
|
+
|
|
835
1000
|
|
|
836
1001
|
// SCD creation
|
|
837
|
-
const
|
|
1002
|
+
const scdUserTables = t.objFilter(scdProps, (scd) => scd.type === 'user');
|
|
1003
|
+
const scdTableKeys = Object.keys(scdUserTables);
|
|
1004
|
+
|
|
1005
|
+
|
|
838
1006
|
const userSCD = {};
|
|
839
1007
|
for (const [index, key] of scdTableKeys.entries()) {
|
|
840
|
-
|
|
1008
|
+
// @ts-ignore
|
|
1009
|
+
const { max = 100 } = scdProps[key];
|
|
1010
|
+
const mutations = chance.integer({ min: 1, max });
|
|
841
1011
|
const changes = await makeSCD(scdProps[key], key, distinct_id, mutations, created);
|
|
842
1012
|
userSCD[key] = changes;
|
|
843
|
-
await
|
|
1013
|
+
await config.hook(changes, "scd-pre", { profile, type: 'user', scd: { [key]: scdProps[key] }, config, allSCDs: userSCD });
|
|
844
1014
|
}
|
|
845
1015
|
|
|
846
1016
|
let numEventsThisUserWillPreform = Math.floor(chance.normal({
|
|
@@ -860,6 +1030,7 @@ async function userLoop(config, storage, concurrency = 1) {
|
|
|
860
1030
|
|
|
861
1031
|
const secondsInDay = 86400;
|
|
862
1032
|
const noise = () => chance.integer({ min: 0, max: secondsInDay });
|
|
1033
|
+
let usersEvents = [];
|
|
863
1034
|
|
|
864
1035
|
if (firstFunnels.length && userIsBornInDataset) {
|
|
865
1036
|
const firstFunnel = chance.pickone(firstFunnels, user);
|
|
@@ -868,7 +1039,8 @@ async function userLoop(config, storage, concurrency = 1) {
|
|
|
868
1039
|
const [data, userConverted] = await makeFunnel(firstFunnel, user, firstTime, profile, userSCD, config);
|
|
869
1040
|
userFirstEventTime = dayjs(data[0].time).subtract(timeShift, 'seconds').unix();
|
|
870
1041
|
numEventsPreformed += data.length;
|
|
871
|
-
await eventData.hookPush(data);
|
|
1042
|
+
// await eventData.hookPush(data, { profile });
|
|
1043
|
+
usersEvents.push(...data);
|
|
872
1044
|
if (!userConverted) {
|
|
873
1045
|
if (verbose) u.progress([["users", userCount], ["events", eventCount]]);
|
|
874
1046
|
return;
|
|
@@ -884,14 +1056,35 @@ async function userLoop(config, storage, concurrency = 1) {
|
|
|
884
1056
|
const currentFunnel = chance.pickone(usageFunnels);
|
|
885
1057
|
const [data, userConverted] = await makeFunnel(currentFunnel, user, userFirstEventTime, profile, userSCD, config);
|
|
886
1058
|
numEventsPreformed += data.length;
|
|
887
|
-
|
|
1059
|
+
usersEvents.push(...data);
|
|
1060
|
+
// await eventData.hookPush(data, { profile });
|
|
888
1061
|
} else {
|
|
889
1062
|
const data = await makeEvent(distinct_id, userFirstEventTime, u.choose(config.events), user.anonymousIds, user.sessionIds, {}, config.groupKeys, true);
|
|
890
1063
|
numEventsPreformed++;
|
|
891
|
-
|
|
1064
|
+
usersEvents.push(data);
|
|
1065
|
+
// await eventData.hookPush(data);
|
|
892
1066
|
}
|
|
893
1067
|
}
|
|
894
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
|
+
|
|
895
1088
|
if (verbose) u.progress([["users", userCount], ["events", eventCount]]);
|
|
896
1089
|
});
|
|
897
1090
|
}
|
|
@@ -906,8 +1099,18 @@ async function userLoop(config, storage, concurrency = 1) {
|
|
|
906
1099
|
* @param {Storage} storage
|
|
907
1100
|
*/
|
|
908
1101
|
async function sendToMixpanel(config, storage) {
|
|
909
|
-
const {
|
|
910
|
-
|
|
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;
|
|
911
1114
|
const importResults = { events: {}, users: {}, groups: [] };
|
|
912
1115
|
|
|
913
1116
|
/** @type {import('mixpanel-import').Creds} */
|
|
@@ -924,7 +1127,7 @@ async function sendToMixpanel(config, storage) {
|
|
|
924
1127
|
dryRun: false,
|
|
925
1128
|
abridged: false,
|
|
926
1129
|
fixJson: true,
|
|
927
|
-
showProgress: true,
|
|
1130
|
+
showProgress: NODE_ENV === "dev" ? true : false,
|
|
928
1131
|
streamFormat: mpImportFormat
|
|
929
1132
|
};
|
|
930
1133
|
|
|
@@ -960,7 +1163,7 @@ async function sendToMixpanel(config, storage) {
|
|
|
960
1163
|
log(`\tsent ${comma(imported.success)} user profiles\n`);
|
|
961
1164
|
importResults.users = imported;
|
|
962
1165
|
}
|
|
963
|
-
if (
|
|
1166
|
+
if (groupEventData || isBATCH_MODE) {
|
|
964
1167
|
log(`importing ad spend data to mixpanel...\n`);
|
|
965
1168
|
let adSpendDataToImport = clone(adSpendData);
|
|
966
1169
|
if (isBATCH_MODE) {
|
|
@@ -996,11 +1199,65 @@ async function sendToMixpanel(config, storage) {
|
|
|
996
1199
|
}
|
|
997
1200
|
}
|
|
998
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
|
+
|
|
999
1256
|
//if we are in batch mode, we need to delete the files
|
|
1000
1257
|
if (!writeToDisk && isBATCH_MODE) {
|
|
1001
1258
|
const writeDir = eventData?.getWriteDir() || userProfilesData?.getWriteDir();
|
|
1002
1259
|
const listDir = await ls(writeDir.split(path.basename(writeDir)).join(""));
|
|
1003
|
-
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-'));
|
|
1004
1261
|
for (const file of files) {
|
|
1005
1262
|
await rm(file);
|
|
1006
1263
|
}
|
|
@@ -1093,6 +1350,43 @@ function validateDungeonConfig(config) {
|
|
|
1093
1350
|
funnels = [...funnels, ...inferredFunnels];
|
|
1094
1351
|
}
|
|
1095
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
|
+
|
|
1096
1390
|
config.concurrency = concurrency;
|
|
1097
1391
|
config.funnels = funnels;
|
|
1098
1392
|
config.batchSize = batchSize;
|
|
@@ -1156,6 +1450,8 @@ async function makeHookArray(arr = [], opts = {}) {
|
|
|
1156
1450
|
if (existsSync(dataFolder)) writeDir = dataFolder;
|
|
1157
1451
|
else writeDir = path.resolve("./");
|
|
1158
1452
|
|
|
1453
|
+
if (NODE_ENV === "prod") writeDir = path.resolve(os.tmpdir());
|
|
1454
|
+
|
|
1159
1455
|
function getWritePath() {
|
|
1160
1456
|
if (isBATCH_MODE) {
|
|
1161
1457
|
return path.join(writeDir, `${filepath}-part-${batch.toString()}.${format}`);
|
|
@@ -1169,14 +1465,14 @@ async function makeHookArray(arr = [], opts = {}) {
|
|
|
1169
1465
|
return path.join(writeDir, `${filepath}.${format}`);
|
|
1170
1466
|
}
|
|
1171
1467
|
|
|
1172
|
-
async function transformThenPush(item) {
|
|
1468
|
+
async function transformThenPush(item, meta) {
|
|
1173
1469
|
if (item === null || item === undefined) return false;
|
|
1174
1470
|
if (typeof item === 'object' && Object.keys(item).length === 0) return false;
|
|
1175
|
-
|
|
1471
|
+
const allMetaData = { ...rest, ...meta };
|
|
1176
1472
|
if (Array.isArray(item)) {
|
|
1177
1473
|
for (const i of item) {
|
|
1178
1474
|
try {
|
|
1179
|
-
const enriched = await hook(i, type,
|
|
1475
|
+
const enriched = await hook(i, type, allMetaData);
|
|
1180
1476
|
if (Array.isArray(enriched)) enriched.forEach(e => arr.push(e));
|
|
1181
1477
|
else arr.push(enriched);
|
|
1182
1478
|
} catch (e) {
|
|
@@ -1186,7 +1482,7 @@ async function makeHookArray(arr = [], opts = {}) {
|
|
|
1186
1482
|
}
|
|
1187
1483
|
} else {
|
|
1188
1484
|
try {
|
|
1189
|
-
const enriched = await hook(item, type,
|
|
1485
|
+
const enriched = await hook(item, type, allMetaData);
|
|
1190
1486
|
if (Array.isArray(enriched)) enriched.forEach(e => arr.push(e));
|
|
1191
1487
|
else arr.push(enriched);
|
|
1192
1488
|
} catch (e) {
|
|
@@ -1301,92 +1597,94 @@ CLI
|
|
|
1301
1597
|
----
|
|
1302
1598
|
*/
|
|
1303
1599
|
|
|
1304
|
-
if (
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
else {
|
|
1317
|
-
if (complex) {
|
|
1318
|
-
console.log(`... using default COMPLEX configuration [everything] ...\n`);
|
|
1319
|
-
console.log(`... for more simple data, don't use the --complex flag ...\n`);
|
|
1320
|
-
console.log(`... or specify your own js config file (see docs or --help) ...\n`);
|
|
1321
|
-
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));
|
|
1322
1612
|
}
|
|
1323
1613
|
else {
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
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
|
+
}
|
|
1327
1625
|
}
|
|
1328
|
-
}
|
|
1329
1626
|
|
|
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
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
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(`\nlog written to ${folder} ...`);
|
|
1665
|
+
writeFileSync(path.join(folder, "log.txt"), JSON.stringify(data?.importResults, null, 2));
|
|
1666
|
+
// log(" " + files?.flat().join("\n "));
|
|
1667
|
+
log(`\n----------------SUMMARY-----------------\n\n\n`);
|
|
1668
|
+
})
|
|
1669
|
+
.catch((e) => {
|
|
1670
|
+
log(`------------------ERROR------------------`);
|
|
1671
|
+
console.error(e);
|
|
1672
|
+
log(`------------------ERROR------------------`);
|
|
1673
|
+
debugger;
|
|
1674
|
+
})
|
|
1675
|
+
.finally(() => {
|
|
1676
|
+
log("enjoy your data! :)");
|
|
1677
|
+
});
|
|
1678
|
+
} else {
|
|
1679
|
+
main.generators = { makeEvent, makeFunnel, makeProfile, makeSCD, makeAdSpend, makeMirror };
|
|
1680
|
+
main.orchestrators = { userLoop, validateDungeonConfig, sendToMixpanel };
|
|
1681
|
+
main.meta = { inferFunnels, hookArray: makeHookArray };
|
|
1682
|
+
module.exports = main;
|
|
1683
|
+
}
|
|
1387
1684
|
}
|
|
1388
1685
|
|
|
1389
1686
|
|
|
1687
|
+
|
|
1390
1688
|
/*
|
|
1391
1689
|
----
|
|
1392
1690
|
HELPERS
|
|
@@ -1411,7 +1709,7 @@ function track(name, props, ...rest) {
|
|
|
1411
1709
|
}
|
|
1412
1710
|
|
|
1413
1711
|
|
|
1414
|
-
/** @typedef {import('./types.js').
|
|
1712
|
+
/** @typedef {import('./types.js').Dungeon} Config */
|
|
1415
1713
|
/** @typedef {import('./types.js').AllData} AllData */
|
|
1416
1714
|
/** @typedef {import('./types.js').EventConfig} EventConfig */
|
|
1417
1715
|
/** @typedef {import('./types.js').Funnel} Funnel */
|
|
@@ -1424,4 +1722,5 @@ function track(name, props, ...rest) {
|
|
|
1424
1722
|
/** @typedef {import('./types.js').ValueValid} ValueValid */
|
|
1425
1723
|
/** @typedef {import('./types.js').HookedArray} hookArray */
|
|
1426
1724
|
/** @typedef {import('./types.js').hookArrayOptions} hookArrayOptions */
|
|
1427
|
-
/** @typedef {import('./types.js').GroupProfileSchema} GroupProfile */
|
|
1725
|
+
/** @typedef {import('./types.js').GroupProfileSchema} GroupProfile */
|
|
1726
|
+
/** @typedef {import('./types.js').SCDProp} SCDProp */
|