make-mp-data 1.4.1 → 1.4.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/README.md +3 -2
- package/{cli.js → core/cli.js} +1 -1
- package/core/defaults.js +960 -0
- package/{index.js → core/index.js} +217 -87
- package/{utils.js → core/utils.js} +163 -34
- package/package.json +8 -9
- package/schemas/anon.js +104 -0
- package/schemas/complex.js +11 -2
- package/schemas/deepNest.js +5 -1
- package/schemas/foobar.js +2 -2
- package/schemas/funnels.js +1 -1
- package/schemas/simple.js +11 -1
- package/scratch.mjs +19 -5
- package/scripts/jsdoctest.js +1 -1
- package/tests/e2e.test.js +25 -7
- package/{testSoup.mjs → tests/testSoup.mjs} +2 -2
- package/tests/unit.test.js +157 -9
- package/tsconfig.json +1 -1
- package/types.d.ts +17 -3
- package/defaults.js +0 -11662
- /package/{chart.js → core/chart.js} +0 -0
- /package/{testCases.mjs → tests/testCases.mjs} +0 -0
|
@@ -7,31 +7,43 @@ ak@mixpanel.com
|
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
9
|
//todo: churn ... is churnFunnel, possible to return, etc
|
|
10
|
-
//todo: fixedTimeFunnel? if set this funnel will occur for all users at the same time ['
|
|
11
|
-
//todo
|
|
12
|
-
//todo
|
|
13
|
-
|
|
14
|
-
|
|
10
|
+
//todo: fixedTimeFunnel? if set this funnel will occur for all users at the same time ['cards charged', 'charge complete']
|
|
11
|
+
//todo: send SCD data to mixpanel
|
|
12
|
+
//todo: send and map lookup tables to mixpanel
|
|
13
|
+
|
|
14
|
+
/** @typedef {import('../types').Config} Config */
|
|
15
|
+
/** @typedef {import('../types').EventConfig} EventConfig */
|
|
16
|
+
/** @typedef {import('../types').Funnel} Funnel */
|
|
17
|
+
/** @typedef {import('../types').Person} Person */
|
|
18
|
+
/** @typedef {import('../types').SCDTableRow} SCDTableRow */
|
|
19
|
+
/** @typedef {import('../types').UserProfile} UserProfile */
|
|
20
|
+
/** @typedef {import('../types').EventSpec} EventSpec */
|
|
21
|
+
|
|
15
22
|
const dayjs = require("dayjs");
|
|
16
23
|
const utc = require("dayjs/plugin/utc");
|
|
17
24
|
dayjs.extend(utc);
|
|
18
25
|
const NOW = dayjs('2024-02-02').unix(); //this is a FIXED POINT and we will shift it later
|
|
19
26
|
global.NOW = NOW;
|
|
20
|
-
|
|
27
|
+
|
|
28
|
+
const os = require("os");
|
|
21
29
|
const path = require("path");
|
|
22
30
|
const { comma, bytesHuman, makeName, md5, clone, tracker, uid } = require("ak-tools");
|
|
23
31
|
const { generateLineChart } = require('./chart.js');
|
|
24
|
-
const { version } = require('
|
|
25
|
-
const
|
|
32
|
+
const { version } = require('../package.json');
|
|
33
|
+
const mp = require("mixpanel-import");
|
|
26
34
|
const metrics = tracker("make-mp-data", "db99eb8f67ae50949a13c27cacf57d41", os.userInfo().username);
|
|
27
35
|
|
|
28
36
|
|
|
29
37
|
const u = require("./utils.js");
|
|
30
|
-
const
|
|
38
|
+
const getCliParams = require("./cli.js");
|
|
39
|
+
const { campaigns, devices, locations } = require('./defaults.js');
|
|
31
40
|
|
|
32
41
|
let VERBOSE = false;
|
|
33
42
|
let isCLI = false;
|
|
43
|
+
/** @type {Config} */
|
|
34
44
|
let CONFIG;
|
|
45
|
+
let CAMPAIGNS;
|
|
46
|
+
let DEFAULTS;
|
|
35
47
|
require('dotenv').config();
|
|
36
48
|
|
|
37
49
|
|
|
@@ -40,25 +52,20 @@ function track(name, props, ...rest) {
|
|
|
40
52
|
metrics(name, props, ...rest);
|
|
41
53
|
}
|
|
42
54
|
|
|
43
|
-
|
|
44
|
-
/** @typedef {import('./types.d.ts').EventConfig} EventConfig */
|
|
45
|
-
/** @typedef {import('./types.d.ts').Funnel} Funnel */
|
|
46
|
-
/** @typedef {import('./types.d.ts').Person} Person */
|
|
47
|
-
/** @typedef {import('./types.d.ts').SCDTableRow} SCDTableRow */
|
|
48
|
-
/** @typedef {import('./types.d.ts').UserProfile} UserProfile */
|
|
49
|
-
/** @typedef {import('./types.d.ts').EventSpec} EventSpec */
|
|
55
|
+
|
|
50
56
|
|
|
51
57
|
/**
|
|
52
58
|
* generates fake mixpanel data
|
|
53
59
|
* @param {Config} config
|
|
54
60
|
*/
|
|
55
61
|
async function main(config) {
|
|
56
|
-
|
|
62
|
+
|
|
63
|
+
//seed the random number generator
|
|
64
|
+
// ^ this is critical; same seed = same data; seed can be passed in as an env var or in the config
|
|
57
65
|
const seedWord = process.env.SEED || config.seed || "hello friend!";
|
|
58
66
|
config.seed = seedWord;
|
|
59
67
|
u.initChance(seedWord);
|
|
60
|
-
const chance = u.getChance();
|
|
61
|
-
config.chance = chance;
|
|
68
|
+
const chance = u.getChance(); // ! this is the only safe way to get the chance instance
|
|
62
69
|
let {
|
|
63
70
|
seed,
|
|
64
71
|
numEvents = 100000,
|
|
@@ -67,10 +74,9 @@ async function main(config) {
|
|
|
67
74
|
epochStart = 0,
|
|
68
75
|
epochEnd = dayjs().unix(),
|
|
69
76
|
events = [{ event: "foo" }, { event: "bar" }, { event: "baz" }],
|
|
70
|
-
superProps = {
|
|
77
|
+
superProps = { luckyNumber: [2, 2, 4, 4, 42, 42, 42, 2, 2, 4, 4, 42, 42, 42, 420] },
|
|
71
78
|
funnels = [],
|
|
72
79
|
userProps = {
|
|
73
|
-
favoriteColor: ["red", "green", "blue", "yellow"],
|
|
74
80
|
spiritAnimal: chance.animal.bind(chance),
|
|
75
81
|
},
|
|
76
82
|
scdProps = {},
|
|
@@ -88,10 +94,20 @@ async function main(config) {
|
|
|
88
94
|
makeChart = false,
|
|
89
95
|
soup = {},
|
|
90
96
|
hook = (record) => record,
|
|
97
|
+
hasAdSpend = false,
|
|
98
|
+
hasCampaigns = false,
|
|
99
|
+
hasLocation = false,
|
|
100
|
+
isAnonymous = false,
|
|
101
|
+
hasBrowser = false,
|
|
102
|
+
hasAndroidDevices = false,
|
|
103
|
+
hasDesktopDevices = false,
|
|
104
|
+
hasIOSDevices = false
|
|
91
105
|
} = config;
|
|
106
|
+
|
|
92
107
|
if (!config.superProps) config.superProps = superProps;
|
|
93
108
|
if (!config.userProps || Object.keys(config?.userProps)) config.userProps = userProps;
|
|
94
|
-
|
|
109
|
+
|
|
110
|
+
|
|
95
111
|
config.simulationName = makeName();
|
|
96
112
|
const { simulationName } = config;
|
|
97
113
|
if (epochStart && !numDays) numDays = dayjs.unix(epochEnd).diff(dayjs.unix(epochStart), "day");
|
|
@@ -123,13 +139,34 @@ async function main(config) {
|
|
|
123
139
|
config.makeChart = makeChart;
|
|
124
140
|
config.soup = soup;
|
|
125
141
|
config.hook = hook;
|
|
142
|
+
config.hasAdSpend = hasAdSpend;
|
|
143
|
+
config.hasCampaigns = hasCampaigns;
|
|
144
|
+
config.hasLocation = hasLocation;
|
|
145
|
+
config.isAnonymous = isAnonymous;
|
|
146
|
+
config.hasBrowser = hasBrowser;
|
|
147
|
+
config.hasAndroidDevices = hasAndroidDevices;
|
|
148
|
+
config.hasDesktopDevices = hasDesktopDevices;
|
|
149
|
+
config.hasIOSDevices = hasIOSDevices;
|
|
126
150
|
|
|
127
151
|
//event validation
|
|
128
152
|
const validatedEvents = u.validateEventConfig(events);
|
|
129
153
|
events = validatedEvents;
|
|
130
154
|
config.events = validatedEvents;
|
|
155
|
+
|
|
156
|
+
//globals
|
|
131
157
|
global.MP_SIMULATION_CONFIG = config;
|
|
132
158
|
CONFIG = config;
|
|
159
|
+
VERBOSE = verbose;
|
|
160
|
+
CAMPAIGNS = campaigns;
|
|
161
|
+
DEFAULTS = {
|
|
162
|
+
locations: u.pickAWinner(locations, 0),
|
|
163
|
+
iOSDevices: u.pickAWinner(devices.iosDevices, 0),
|
|
164
|
+
androidDevices: u.pickAWinner(devices.androidDevices, 0),
|
|
165
|
+
desktopDevices: u.pickAWinner(devices.desktopDevices, 0),
|
|
166
|
+
browsers: u.pickAWinner(devices.browsers, 0),
|
|
167
|
+
campaigns: u.pickAWinner(campaigns, 0),
|
|
168
|
+
};
|
|
169
|
+
|
|
133
170
|
const runId = uid(42);
|
|
134
171
|
let trackingParams = { runId, seed, numEvents, numUsers, numDays, anonIds, sessionIds, format, targetToken: token, region, writeToDisk, isCLI, version };
|
|
135
172
|
track('start simulation', trackingParams);
|
|
@@ -142,6 +179,7 @@ async function main(config) {
|
|
|
142
179
|
//setup all the data structures we will push into
|
|
143
180
|
const eventData = u.enrichArray([], { hook, type: "event", config });
|
|
144
181
|
const userProfilesData = u.enrichArray([], { hook, type: "user", config });
|
|
182
|
+
const adSpendData = u.enrichArray([], { hook, type: "ad-spend", config });
|
|
145
183
|
const scdTableKeys = Object.keys(scdProps);
|
|
146
184
|
const scdTableData = [];
|
|
147
185
|
for (const [index, key] of scdTableKeys.entries()) {
|
|
@@ -153,7 +191,7 @@ async function main(config) {
|
|
|
153
191
|
|
|
154
192
|
// if no funnels, make some out of events...
|
|
155
193
|
if (!funnels || !funnels.length) {
|
|
156
|
-
funnels = inferFunnels(events);
|
|
194
|
+
funnels = u.inferFunnels(events);
|
|
157
195
|
config.funnels = funnels;
|
|
158
196
|
CONFIG = config;
|
|
159
197
|
}
|
|
@@ -161,13 +199,21 @@ async function main(config) {
|
|
|
161
199
|
//user loop
|
|
162
200
|
log(`---------------SIMULATION----------------`, "\n\n");
|
|
163
201
|
loopUsers: for (let i = 1; i < numUsers + 1; i++) {
|
|
164
|
-
u.progress("users", i);
|
|
202
|
+
u.progress([["users", i], ["events", eventData.length]]);
|
|
165
203
|
const userId = chance.guid();
|
|
166
|
-
|
|
167
|
-
const user = u.generateUser(userId, numDays);
|
|
204
|
+
const user = u.person(userId, numDays, isAnonymous);
|
|
168
205
|
const { distinct_id, created, anonymousIds, sessionIds } = user;
|
|
169
206
|
let numEventsPreformed = 0;
|
|
170
207
|
|
|
208
|
+
if (hasLocation) {
|
|
209
|
+
const location = u.choose(clone(DEFAULTS.locations()).map(l => { delete l.country; return l; }));
|
|
210
|
+
for (const key in location) {
|
|
211
|
+
user[key] = location[key];
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
|
|
171
217
|
// profile creation
|
|
172
218
|
const profile = makeProfile(userProps, user);
|
|
173
219
|
userProfilesData.hookPush(profile);
|
|
@@ -226,6 +272,17 @@ async function main(config) {
|
|
|
226
272
|
// end individual user loop
|
|
227
273
|
}
|
|
228
274
|
|
|
275
|
+
if (hasAdSpend) {
|
|
276
|
+
const days = u.datesBetween(epochStart, epochEnd);
|
|
277
|
+
for (const day of days) {
|
|
278
|
+
const dailySpendData = makeAdSpend(day);
|
|
279
|
+
for (const spendEvent of dailySpendData) {
|
|
280
|
+
adSpendData.hookPush(spendEvent);
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
}
|
|
285
|
+
|
|
229
286
|
//flatten SCD tables
|
|
230
287
|
scdTableData.forEach((table, index) => scdTableData[index] = table.flat());
|
|
231
288
|
|
|
@@ -237,7 +294,7 @@ async function main(config) {
|
|
|
237
294
|
const groupCardinality = groupPair[1];
|
|
238
295
|
const groupProfiles = [];
|
|
239
296
|
for (let i = 1; i < groupCardinality + 1; i++) {
|
|
240
|
-
u.progress("groups", i);
|
|
297
|
+
u.progress([["groups", i]]);
|
|
241
298
|
const group = {
|
|
242
299
|
[groupKey]: i,
|
|
243
300
|
...makeProfile(groupProps[groupKey])
|
|
@@ -254,7 +311,7 @@ async function main(config) {
|
|
|
254
311
|
const { key, entries, attributes } = lookupTable;
|
|
255
312
|
const data = [];
|
|
256
313
|
for (let i = 1; i < entries + 1; i++) {
|
|
257
|
-
u.progress("lookups", i);
|
|
314
|
+
u.progress([["lookups", i]]);
|
|
258
315
|
const item = {
|
|
259
316
|
[key]: i,
|
|
260
317
|
...makeProfile(attributes),
|
|
@@ -309,11 +366,12 @@ async function main(config) {
|
|
|
309
366
|
}
|
|
310
367
|
}
|
|
311
368
|
|
|
312
|
-
const { eventFiles, userFiles, scdFiles, groupFiles, lookupFiles, mirrorFiles, folder } =
|
|
369
|
+
const { eventFiles, userFiles, scdFiles, groupFiles, lookupFiles, mirrorFiles, folder, adSpendFiles } =
|
|
313
370
|
u.buildFileNames(config);
|
|
314
371
|
const pairs = [
|
|
315
372
|
[eventFiles, [eventData]],
|
|
316
373
|
[userFiles, [userProfilesData]],
|
|
374
|
+
[adSpendFiles, [adSpendData]],
|
|
317
375
|
[scdFiles, scdTableData],
|
|
318
376
|
[groupFiles, groupProfilesData],
|
|
319
377
|
[lookupFiles, lookupTableData],
|
|
@@ -379,7 +437,7 @@ async function main(config) {
|
|
|
379
437
|
fixData: true,
|
|
380
438
|
verbose: false,
|
|
381
439
|
forceStream: true,
|
|
382
|
-
strict:
|
|
440
|
+
strict: false, //! sometimes we get events in the future... it happens
|
|
383
441
|
dryRun: false,
|
|
384
442
|
abridged: false,
|
|
385
443
|
fixJson: true,
|
|
@@ -388,7 +446,7 @@ async function main(config) {
|
|
|
388
446
|
|
|
389
447
|
if (eventData) {
|
|
390
448
|
log(`importing events to mixpanel...\n`);
|
|
391
|
-
const imported = await mp(creds, eventData, {
|
|
449
|
+
const imported = await mp(creds, clone(eventData), {
|
|
392
450
|
recordType: "event",
|
|
393
451
|
...commonOpts,
|
|
394
452
|
});
|
|
@@ -397,19 +455,28 @@ async function main(config) {
|
|
|
397
455
|
}
|
|
398
456
|
if (userProfilesData) {
|
|
399
457
|
log(`importing user profiles to mixpanel...\n`);
|
|
400
|
-
const imported = await mp(creds, userProfilesData, {
|
|
458
|
+
const imported = await mp(creds, clone(userProfilesData), {
|
|
401
459
|
recordType: "user",
|
|
402
460
|
...commonOpts,
|
|
403
461
|
});
|
|
404
462
|
log(`\tsent ${comma(imported.success)} user profiles\n`);
|
|
405
463
|
importResults.users = imported;
|
|
406
464
|
}
|
|
465
|
+
if (adSpendData) {
|
|
466
|
+
log(`importing ad spend data to mixpanel...\n`);
|
|
467
|
+
const imported = await mp(creds, clone(adSpendData), {
|
|
468
|
+
recordType: "event",
|
|
469
|
+
...commonOpts,
|
|
470
|
+
});
|
|
471
|
+
log(`\tsent ${comma(imported.success)} ad spend events\n`);
|
|
472
|
+
importResults.adSpend = imported;
|
|
473
|
+
}
|
|
407
474
|
if (groupProfilesData) {
|
|
408
475
|
for (const groupProfiles of groupProfilesData) {
|
|
409
476
|
const groupKey = groupProfiles.key;
|
|
410
477
|
const data = groupProfiles.data;
|
|
411
478
|
log(`importing ${groupKey} profiles to mixpanel...\n`);
|
|
412
|
-
const imported = await mp({ token, groupKey }, data, {
|
|
479
|
+
const imported = await mp({ token, groupKey }, clone(data), {
|
|
413
480
|
recordType: "group",
|
|
414
481
|
...commonOpts,
|
|
415
482
|
|
|
@@ -432,6 +499,7 @@ async function main(config) {
|
|
|
432
499
|
groupProfilesData,
|
|
433
500
|
lookupTableData,
|
|
434
501
|
mirrorEventData,
|
|
502
|
+
adSpendData
|
|
435
503
|
};
|
|
436
504
|
}
|
|
437
505
|
|
|
@@ -452,27 +520,42 @@ async function main(config) {
|
|
|
452
520
|
* @param {Boolean} isFirstEvent=false
|
|
453
521
|
*/
|
|
454
522
|
function makeEvent(distinct_id, anonymousIds, sessionIds, earliestTime, chosenEvent, superProps, groupKeys, isFirstEvent = false) {
|
|
455
|
-
const
|
|
523
|
+
const chance = u.getChance();
|
|
524
|
+
const { mean = 0, deviation = 2, peaks = 5 } = CONFIG.soup;
|
|
525
|
+
const { hasAndroidDevices, hasBrowser, hasCampaigns, hasDesktopDevices, hasIOSDevices, hasLocation } = CONFIG;
|
|
456
526
|
//event model
|
|
457
527
|
const eventTemplate = {
|
|
458
528
|
event: chosenEvent.event,
|
|
459
529
|
source: "dm4",
|
|
460
530
|
};
|
|
461
531
|
|
|
532
|
+
let defaultProps = {};
|
|
533
|
+
let devicePool = [];
|
|
534
|
+
if (hasLocation) defaultProps.location = clone(DEFAULTS.locations()).map(l => { delete l.country_code; return l; });
|
|
535
|
+
if (hasBrowser) defaultProps.browser = DEFAULTS.browsers();
|
|
536
|
+
if (hasAndroidDevices) devicePool.push(DEFAULTS.androidDevices());
|
|
537
|
+
if (hasIOSDevices) devicePool.push(DEFAULTS.iOSDevices());
|
|
538
|
+
if (hasDesktopDevices) devicePool.push(DEFAULTS.desktopDevices());
|
|
539
|
+
// we don't always have campaigns, because of attribution
|
|
540
|
+
if (hasCampaigns && chance.bool({ likelihood: 25 })) defaultProps.campaigns = DEFAULTS.campaigns();
|
|
541
|
+
const devices = devicePool.flat();
|
|
542
|
+
if (devices.length) defaultProps.device = devices;
|
|
543
|
+
|
|
544
|
+
|
|
462
545
|
//event time
|
|
463
546
|
if (earliestTime > NOW) {
|
|
464
547
|
earliestTime = dayjs.unix(NOW).subtract(2, 'd').unix();
|
|
465
548
|
};
|
|
466
549
|
|
|
467
550
|
if (isFirstEvent) eventTemplate.time = dayjs.unix(earliestTime).toISOString();
|
|
468
|
-
if (!isFirstEvent) eventTemplate.time = u.TimeSoup(earliestTime, NOW, peaks,
|
|
551
|
+
if (!isFirstEvent) eventTemplate.time = u.TimeSoup(earliestTime, NOW, peaks, deviation, mean);
|
|
469
552
|
|
|
470
553
|
// anonymous and session ids
|
|
471
|
-
if (CONFIG?.anonIds) eventTemplate.device_id =
|
|
472
|
-
if (CONFIG?.sessionIds) eventTemplate.session_id =
|
|
554
|
+
if (CONFIG?.anonIds) eventTemplate.device_id = chance.pickone(anonymousIds);
|
|
555
|
+
if (CONFIG?.sessionIds) eventTemplate.session_id = chance.pickone(sessionIds);
|
|
473
556
|
|
|
474
557
|
//sometimes have a user_id
|
|
475
|
-
if (!isFirstEvent &&
|
|
558
|
+
if (!isFirstEvent && chance.bool({ likelihood: 42 })) eventTemplate.user_id = distinct_id;
|
|
476
559
|
|
|
477
560
|
// ensure that there is a user_id or device_id
|
|
478
561
|
if (!eventTemplate.user_id && !eventTemplate.device_id) eventTemplate.user_id = distinct_id;
|
|
@@ -489,6 +572,43 @@ function makeEvent(distinct_id, anonymousIds, sessionIds, earliestTime, chosenEv
|
|
|
489
572
|
}
|
|
490
573
|
}
|
|
491
574
|
|
|
575
|
+
//iterate through default properties
|
|
576
|
+
for (const key in defaultProps) {
|
|
577
|
+
if (Array.isArray(defaultProps[key])) {
|
|
578
|
+
const choice = u.choose(defaultProps[key]);
|
|
579
|
+
if (typeof choice === "string") {
|
|
580
|
+
if (!eventTemplate[key]) eventTemplate[key] = choice;
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
else if (Array.isArray(choice)) {
|
|
584
|
+
for (const subChoice of choice) {
|
|
585
|
+
if (!eventTemplate[key]) eventTemplate[key] = subChoice;
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
else if (typeof choice === "object") {
|
|
590
|
+
for (const subKey in choice) {
|
|
591
|
+
if (typeof choice[subKey] === "string") {
|
|
592
|
+
if (!eventTemplate[subKey]) eventTemplate[subKey] = choice[subKey];
|
|
593
|
+
}
|
|
594
|
+
else if (Array.isArray(choice[subKey])) {
|
|
595
|
+
const subChoice = u.choose(choice[subKey]);
|
|
596
|
+
if (!eventTemplate[subKey]) eventTemplate[subKey] = subChoice;
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
else if (typeof choice[subKey] === "object") {
|
|
600
|
+
for (const subSubKey in choice[subKey]) {
|
|
601
|
+
if (!eventTemplate[subSubKey]) eventTemplate[subSubKey] = choice[subKey][subSubKey];
|
|
602
|
+
}
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
|
|
609
|
+
}
|
|
610
|
+
}
|
|
611
|
+
|
|
492
612
|
//iterate through groups
|
|
493
613
|
for (const groupPair of groupKeys) {
|
|
494
614
|
const groupKey = groupPair[0];
|
|
@@ -518,6 +638,7 @@ function makeEvent(distinct_id, anonymousIds, sessionIds, earliestTime, chosenEv
|
|
|
518
638
|
* @return {[EventSpec[], Boolean]}
|
|
519
639
|
*/
|
|
520
640
|
function makeFunnel(funnel, user, profile, scd, firstEventTime, config) {
|
|
641
|
+
const chance = u.getChance();
|
|
521
642
|
const { hook } = config;
|
|
522
643
|
hook(funnel, "funnel-pre", { user, profile, scd, funnel, config });
|
|
523
644
|
let {
|
|
@@ -532,6 +653,7 @@ function makeFunnel(funnel, user, profile, scd, firstEventTime, config) {
|
|
|
532
653
|
const { superProps, groupKeys } = config;
|
|
533
654
|
const { name, email } = profile;
|
|
534
655
|
|
|
656
|
+
//choose the properties for this funnel
|
|
535
657
|
const chosenFunnelProps = { ...props, ...superProps };
|
|
536
658
|
for (const key in props) {
|
|
537
659
|
try {
|
|
@@ -563,7 +685,7 @@ function makeFunnel(funnel, user, profile, scd, firstEventTime, config) {
|
|
|
563
685
|
.reduce((acc, step) => {
|
|
564
686
|
if (!requireRepeats) {
|
|
565
687
|
if (acc.find(e => e.event === step.event)) {
|
|
566
|
-
if (
|
|
688
|
+
if (chance.bool({ likelihood: 50 })) {
|
|
567
689
|
conversionRate = Math.floor(conversionRate * 1.25); //increase conversion rate
|
|
568
690
|
acc.push(step);
|
|
569
691
|
}
|
|
@@ -583,7 +705,7 @@ function makeFunnel(funnel, user, profile, scd, firstEventTime, config) {
|
|
|
583
705
|
return acc;
|
|
584
706
|
}, []);
|
|
585
707
|
|
|
586
|
-
let doesUserConvert =
|
|
708
|
+
let doesUserConvert = chance.bool({ likelihood: conversionRate });
|
|
587
709
|
let numStepsUserWillTake = sequence.length;
|
|
588
710
|
if (!doesUserConvert) numStepsUserWillTake = u.integer(1, sequence.length - 1);
|
|
589
711
|
const funnelTotalRelativeTimeInHours = timeToConvert / numStepsUserWillTake;
|
|
@@ -677,49 +799,6 @@ function makeFunnel(funnel, user, profile, scd, firstEventTime, config) {
|
|
|
677
799
|
}
|
|
678
800
|
|
|
679
801
|
|
|
680
|
-
function inferFunnels(events) {
|
|
681
|
-
const createdFunnels = [];
|
|
682
|
-
const firstEvents = events.filter((e) => e.isFirstEvent).map((e) => e.event);
|
|
683
|
-
const usageEvents = events.filter((e) => !e.isFirstEvent).map((e) => e.event);
|
|
684
|
-
const numFunnelsToCreate = Math.ceil(usageEvents.length);
|
|
685
|
-
/** @type {Funnel} */
|
|
686
|
-
const funnelTemplate = {
|
|
687
|
-
sequence: [],
|
|
688
|
-
conversionRate: 50,
|
|
689
|
-
order: 'sequential',
|
|
690
|
-
requireRepeats: false,
|
|
691
|
-
props: {},
|
|
692
|
-
timeToConvert: 1,
|
|
693
|
-
isFirstFunnel: false,
|
|
694
|
-
weight: 1
|
|
695
|
-
};
|
|
696
|
-
if (firstEvents.length) {
|
|
697
|
-
for (const event of firstEvents) {
|
|
698
|
-
createdFunnels.push({ ...clone(funnelTemplate), sequence: [event], isFirstFunnel: true, conversionRate: 100 });
|
|
699
|
-
}
|
|
700
|
-
}
|
|
701
|
-
|
|
702
|
-
//at least one funnel with all usage events
|
|
703
|
-
createdFunnels.push({ ...clone(funnelTemplate), sequence: usageEvents });
|
|
704
|
-
|
|
705
|
-
//for the rest, make random funnels
|
|
706
|
-
followUpFunnels: for (let i = 1; i < numFunnelsToCreate; i++) {
|
|
707
|
-
/** @type {Funnel} */
|
|
708
|
-
const funnel = { ...clone(funnelTemplate) };
|
|
709
|
-
funnel.conversionRate = u.integer(25, 75);
|
|
710
|
-
funnel.timeToConvert = u.integer(1, 10);
|
|
711
|
-
funnel.weight = u.integer(1, 10);
|
|
712
|
-
const sequence = u.shuffleArray(usageEvents).slice(0, u.integer(2, usageEvents.length));
|
|
713
|
-
funnel.sequence = sequence;
|
|
714
|
-
funnel.order = 'random';
|
|
715
|
-
createdFunnels.push(funnel);
|
|
716
|
-
}
|
|
717
|
-
|
|
718
|
-
return createdFunnels;
|
|
719
|
-
|
|
720
|
-
}
|
|
721
|
-
|
|
722
|
-
|
|
723
802
|
function makeProfile(props, defaults) {
|
|
724
803
|
//build the spec
|
|
725
804
|
const profile = {
|
|
@@ -742,7 +821,7 @@ function makeProfile(props, defaults) {
|
|
|
742
821
|
}
|
|
743
822
|
|
|
744
823
|
/**
|
|
745
|
-
* @param {import('
|
|
824
|
+
* @param {import('../types').ValueValid} prop
|
|
746
825
|
* @param {string} scdKey
|
|
747
826
|
* @param {string} distinct_id
|
|
748
827
|
* @param {number} mutations
|
|
@@ -770,8 +849,59 @@ function makeSCD(prop, scdKey, distinct_id, mutations, created) {
|
|
|
770
849
|
return scdEntries;
|
|
771
850
|
}
|
|
772
851
|
|
|
852
|
+
//todo
|
|
853
|
+
function makeAdSpend(day) {
|
|
854
|
+
const chance = u.getChance();
|
|
855
|
+
const adSpendEvents = [];
|
|
856
|
+
for (const network of CAMPAIGNS) {
|
|
857
|
+
const campaigns = network.utm_campaign;
|
|
858
|
+
loopCampaigns: for (const campaign of campaigns) {
|
|
859
|
+
if (campaign === "$organic") continue loopCampaigns;
|
|
860
|
+
|
|
861
|
+
const CAC = u.integer(42, 420); //todo: get the # of users created in this day from eventData
|
|
862
|
+
// Randomly generating cost
|
|
863
|
+
const cost = chance.floating({ min: 10, max: 250, fixed: 2 });
|
|
864
|
+
|
|
865
|
+
// Ensuring realistic CPC and CTR
|
|
866
|
+
const avgCPC = chance.floating({ min: 0.33, max: 2.00, fixed: 4 });
|
|
867
|
+
const avgCTR = chance.floating({ min: 0.05, max: 0.25, fixed: 4 });
|
|
868
|
+
|
|
869
|
+
// Deriving impressions from cost and avg CPC
|
|
870
|
+
const clicks = Math.floor(cost / avgCPC);
|
|
871
|
+
const impressions = Math.floor(clicks / avgCTR);
|
|
872
|
+
const views = Math.floor(impressions * avgCTR);
|
|
873
|
+
|
|
874
|
+
//tags
|
|
875
|
+
const utm_medium = u.choose(u.pickAWinner(network.utm_medium)());
|
|
876
|
+
const utm_content = u.choose(u.pickAWinner(network.utm_content)());
|
|
877
|
+
const utm_term = u.choose(u.pickAWinner(network.utm_term)());
|
|
878
|
+
//each of these is a campaign
|
|
879
|
+
const adSpendEvent = {
|
|
880
|
+
event: "Ad Data",
|
|
881
|
+
time: day,
|
|
882
|
+
source: 'dm4',
|
|
883
|
+
utm_campaign: campaign,
|
|
884
|
+
campaign_id: md5(network.utm_source[0] + '-' + campaign),
|
|
885
|
+
network: network.utm_source[0].toUpperCase(),
|
|
886
|
+
distinct_id: network.utm_source[0].toUpperCase(),
|
|
887
|
+
utm_source: network.utm_source[0],
|
|
888
|
+
utm_medium,
|
|
889
|
+
utm_content,
|
|
890
|
+
utm_term,
|
|
891
|
+
|
|
892
|
+
clicks,
|
|
893
|
+
views,
|
|
894
|
+
impressions,
|
|
895
|
+
cost,
|
|
896
|
+
date: dayjs(day).format("YYYY-MM-DD"),
|
|
897
|
+
};
|
|
898
|
+
adSpendEvents.push(adSpendEvent);
|
|
899
|
+
}
|
|
773
900
|
|
|
774
901
|
|
|
902
|
+
}
|
|
903
|
+
return adSpendEvents;
|
|
904
|
+
}
|
|
775
905
|
|
|
776
906
|
|
|
777
907
|
|
|
@@ -782,7 +912,7 @@ function makeSCD(prop, scdKey, distinct_id, mutations, created) {
|
|
|
782
912
|
// this is for CLI
|
|
783
913
|
if (require.main === module) {
|
|
784
914
|
isCLI = true;
|
|
785
|
-
const args =
|
|
915
|
+
const args = getCliParams();
|
|
786
916
|
// @ts-ignore
|
|
787
917
|
let { token, seed, format, numDays, numUsers, numEvents, region, writeToDisk, complex = false, sessionIds, anonIds } = args;
|
|
788
918
|
// @ts-ignore
|
|
@@ -799,12 +929,12 @@ if (require.main === module) {
|
|
|
799
929
|
console.log(`... using default COMPLEX configuration [everything] ...\n`);
|
|
800
930
|
console.log(`... for more simple data, don't use the --complex flag ...\n`);
|
|
801
931
|
console.log(`... or specify your own js config file (see docs or --help) ...\n`);
|
|
802
|
-
config = require(path.resolve(__dirname, "
|
|
932
|
+
config = require(path.resolve(__dirname, "../schemas/complex.js"));
|
|
803
933
|
}
|
|
804
934
|
else {
|
|
805
935
|
console.log(`... using default SIMPLE configuration [events + users] ...\n`);
|
|
806
936
|
console.log(`... for more complex data, use the --complex flag ...\n`);
|
|
807
|
-
config = require(path.resolve(__dirname, "
|
|
937
|
+
config = require(path.resolve(__dirname, "../schemas/simple.js"));
|
|
808
938
|
}
|
|
809
939
|
}
|
|
810
940
|
|