make-mp-data 1.4.2 → 1.4.4
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/{chart.js → core/chart.js} +11 -4
- package/{cli.js → core/cli.js} +84 -6
- package/{defaults.js → core/defaults.js} +48 -62
- package/{index.js → core/index.js} +113 -89
- package/{utils.js → core/utils.js} +139 -69
- package/package.json +7 -8
- 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 +1 -1
- package/scratch.mjs +18 -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 +153 -6
- package/tsconfig.json +1 -1
- package/types.d.ts +2 -1
- /package/{testCases.mjs → tests/testCases.mjs} +0 -0
|
@@ -6,18 +6,18 @@ by AK
|
|
|
6
6
|
ak@mixpanel.com
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
|
-
//todo: ads-data
|
|
10
|
-
//todo: cart analysis
|
|
11
9
|
//todo: churn ... is churnFunnel, possible to return, etc
|
|
12
|
-
//todo: fixedTimeFunnel? if set this funnel will occur for all users at the same time ['
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
/** @typedef {import('
|
|
17
|
-
/** @typedef {import('
|
|
18
|
-
/** @typedef {import('
|
|
19
|
-
/** @typedef {import('
|
|
20
|
-
/** @typedef {import('
|
|
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
21
|
|
|
22
22
|
const dayjs = require("dayjs");
|
|
23
23
|
const utc = require("dayjs/plugin/utc");
|
|
@@ -29,10 +29,11 @@ const os = require("os");
|
|
|
29
29
|
const path = require("path");
|
|
30
30
|
const { comma, bytesHuman, makeName, md5, clone, tracker, uid } = require("ak-tools");
|
|
31
31
|
const { generateLineChart } = require('./chart.js');
|
|
32
|
-
const { version } = require('
|
|
32
|
+
const { version } = require('../package.json');
|
|
33
33
|
const mp = require("mixpanel-import");
|
|
34
34
|
const metrics = tracker("make-mp-data", "db99eb8f67ae50949a13c27cacf57d41", os.userInfo().username);
|
|
35
35
|
|
|
36
|
+
|
|
36
37
|
const u = require("./utils.js");
|
|
37
38
|
const getCliParams = require("./cli.js");
|
|
38
39
|
const { campaigns, devices, locations } = require('./defaults.js');
|
|
@@ -41,7 +42,7 @@ let VERBOSE = false;
|
|
|
41
42
|
let isCLI = false;
|
|
42
43
|
/** @type {Config} */
|
|
43
44
|
let CONFIG;
|
|
44
|
-
|
|
45
|
+
let CAMPAIGNS;
|
|
45
46
|
let DEFAULTS;
|
|
46
47
|
require('dotenv').config();
|
|
47
48
|
|
|
@@ -156,6 +157,7 @@ async function main(config) {
|
|
|
156
157
|
global.MP_SIMULATION_CONFIG = config;
|
|
157
158
|
CONFIG = config;
|
|
158
159
|
VERBOSE = verbose;
|
|
160
|
+
CAMPAIGNS = campaigns;
|
|
159
161
|
DEFAULTS = {
|
|
160
162
|
locations: u.pickAWinner(locations, 0),
|
|
161
163
|
iOSDevices: u.pickAWinner(devices.iosDevices, 0),
|
|
@@ -177,6 +179,7 @@ async function main(config) {
|
|
|
177
179
|
//setup all the data structures we will push into
|
|
178
180
|
const eventData = u.enrichArray([], { hook, type: "event", config });
|
|
179
181
|
const userProfilesData = u.enrichArray([], { hook, type: "user", config });
|
|
182
|
+
const adSpendData = u.enrichArray([], { hook, type: "ad-spend", config });
|
|
180
183
|
const scdTableKeys = Object.keys(scdProps);
|
|
181
184
|
const scdTableData = [];
|
|
182
185
|
for (const [index, key] of scdTableKeys.entries()) {
|
|
@@ -196,14 +199,14 @@ async function main(config) {
|
|
|
196
199
|
//user loop
|
|
197
200
|
log(`---------------SIMULATION----------------`, "\n\n");
|
|
198
201
|
loopUsers: for (let i = 1; i < numUsers + 1; i++) {
|
|
199
|
-
u.progress("users", i);
|
|
202
|
+
u.progress([["users", i], ["events", eventData.length]]);
|
|
200
203
|
const userId = chance.guid();
|
|
201
204
|
const user = u.person(userId, numDays, isAnonymous);
|
|
202
205
|
const { distinct_id, created, anonymousIds, sessionIds } = user;
|
|
203
206
|
let numEventsPreformed = 0;
|
|
204
207
|
|
|
205
208
|
if (hasLocation) {
|
|
206
|
-
const location = u.choose(DEFAULTS.locations().map(l => { delete l.country; return l; }));
|
|
209
|
+
const location = u.choose(clone(DEFAULTS.locations()).map(l => { delete l.country; return l; }));
|
|
207
210
|
for (const key in location) {
|
|
208
211
|
user[key] = location[key];
|
|
209
212
|
}
|
|
@@ -269,6 +272,17 @@ async function main(config) {
|
|
|
269
272
|
// end individual user loop
|
|
270
273
|
}
|
|
271
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
|
+
|
|
272
286
|
//flatten SCD tables
|
|
273
287
|
scdTableData.forEach((table, index) => scdTableData[index] = table.flat());
|
|
274
288
|
|
|
@@ -280,7 +294,7 @@ async function main(config) {
|
|
|
280
294
|
const groupCardinality = groupPair[1];
|
|
281
295
|
const groupProfiles = [];
|
|
282
296
|
for (let i = 1; i < groupCardinality + 1; i++) {
|
|
283
|
-
u.progress("groups", i);
|
|
297
|
+
u.progress([["groups", i]]);
|
|
284
298
|
const group = {
|
|
285
299
|
[groupKey]: i,
|
|
286
300
|
...makeProfile(groupProps[groupKey])
|
|
@@ -297,7 +311,7 @@ async function main(config) {
|
|
|
297
311
|
const { key, entries, attributes } = lookupTable;
|
|
298
312
|
const data = [];
|
|
299
313
|
for (let i = 1; i < entries + 1; i++) {
|
|
300
|
-
u.progress("lookups", i);
|
|
314
|
+
u.progress([["lookups", i]]);
|
|
301
315
|
const item = {
|
|
302
316
|
[key]: i,
|
|
303
317
|
...makeProfile(attributes),
|
|
@@ -352,11 +366,12 @@ async function main(config) {
|
|
|
352
366
|
}
|
|
353
367
|
}
|
|
354
368
|
|
|
355
|
-
const { eventFiles, userFiles, scdFiles, groupFiles, lookupFiles, mirrorFiles, folder } =
|
|
369
|
+
const { eventFiles, userFiles, scdFiles, groupFiles, lookupFiles, mirrorFiles, folder, adSpendFiles } =
|
|
356
370
|
u.buildFileNames(config);
|
|
357
371
|
const pairs = [
|
|
358
372
|
[eventFiles, [eventData]],
|
|
359
373
|
[userFiles, [userProfilesData]],
|
|
374
|
+
[adSpendFiles, [adSpendData]],
|
|
360
375
|
[scdFiles, scdTableData],
|
|
361
376
|
[groupFiles, groupProfilesData],
|
|
362
377
|
[lookupFiles, lookupTableData],
|
|
@@ -431,7 +446,7 @@ async function main(config) {
|
|
|
431
446
|
|
|
432
447
|
if (eventData) {
|
|
433
448
|
log(`importing events to mixpanel...\n`);
|
|
434
|
-
const imported = await mp(creds, eventData, {
|
|
449
|
+
const imported = await mp(creds, clone(eventData), {
|
|
435
450
|
recordType: "event",
|
|
436
451
|
...commonOpts,
|
|
437
452
|
});
|
|
@@ -440,19 +455,28 @@ async function main(config) {
|
|
|
440
455
|
}
|
|
441
456
|
if (userProfilesData) {
|
|
442
457
|
log(`importing user profiles to mixpanel...\n`);
|
|
443
|
-
const imported = await mp(creds, userProfilesData, {
|
|
458
|
+
const imported = await mp(creds, clone(userProfilesData), {
|
|
444
459
|
recordType: "user",
|
|
445
460
|
...commonOpts,
|
|
446
461
|
});
|
|
447
462
|
log(`\tsent ${comma(imported.success)} user profiles\n`);
|
|
448
463
|
importResults.users = imported;
|
|
449
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
|
+
}
|
|
450
474
|
if (groupProfilesData) {
|
|
451
475
|
for (const groupProfiles of groupProfilesData) {
|
|
452
476
|
const groupKey = groupProfiles.key;
|
|
453
477
|
const data = groupProfiles.data;
|
|
454
478
|
log(`importing ${groupKey} profiles to mixpanel...\n`);
|
|
455
|
-
const imported = await mp({ token, groupKey }, data, {
|
|
479
|
+
const imported = await mp({ token, groupKey }, clone(data), {
|
|
456
480
|
recordType: "group",
|
|
457
481
|
...commonOpts,
|
|
458
482
|
|
|
@@ -475,6 +499,7 @@ async function main(config) {
|
|
|
475
499
|
groupProfilesData,
|
|
476
500
|
lookupTableData,
|
|
477
501
|
mirrorEventData,
|
|
502
|
+
adSpendData
|
|
478
503
|
};
|
|
479
504
|
}
|
|
480
505
|
|
|
@@ -506,7 +531,7 @@ function makeEvent(distinct_id, anonymousIds, sessionIds, earliestTime, chosenEv
|
|
|
506
531
|
|
|
507
532
|
let defaultProps = {};
|
|
508
533
|
let devicePool = [];
|
|
509
|
-
if (hasLocation) defaultProps.location = DEFAULTS.locations().map(l => { delete l.country_code; return l; });
|
|
534
|
+
if (hasLocation) defaultProps.location = clone(DEFAULTS.locations()).map(l => { delete l.country_code; return l; });
|
|
510
535
|
if (hasBrowser) defaultProps.browser = DEFAULTS.browsers();
|
|
511
536
|
if (hasAndroidDevices) devicePool.push(DEFAULTS.androidDevices());
|
|
512
537
|
if (hasIOSDevices) devicePool.push(DEFAULTS.iOSDevices());
|
|
@@ -563,11 +588,20 @@ function makeEvent(distinct_id, anonymousIds, sessionIds, earliestTime, chosenEv
|
|
|
563
588
|
|
|
564
589
|
else if (typeof choice === "object") {
|
|
565
590
|
for (const subKey in choice) {
|
|
566
|
-
if (
|
|
591
|
+
if (typeof choice[subKey] === "string") {
|
|
592
|
+
if (!eventTemplate[subKey]) eventTemplate[subKey] = choice[subKey];
|
|
593
|
+
}
|
|
594
|
+
else if (Array.isArray(choice[subKey])) {
|
|
567
595
|
const subChoice = u.choose(choice[subKey]);
|
|
568
596
|
if (!eventTemplate[subKey]) eventTemplate[subKey] = subChoice;
|
|
569
597
|
}
|
|
570
|
-
|
|
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
|
+
|
|
571
605
|
}
|
|
572
606
|
}
|
|
573
607
|
|
|
@@ -787,7 +821,7 @@ function makeProfile(props, defaults) {
|
|
|
787
821
|
}
|
|
788
822
|
|
|
789
823
|
/**
|
|
790
|
-
* @param {import('
|
|
824
|
+
* @param {import('../types').ValueValid} prop
|
|
791
825
|
* @param {string} scdKey
|
|
792
826
|
* @param {string} distinct_id
|
|
793
827
|
* @param {number} mutations
|
|
@@ -816,68 +850,58 @@ function makeSCD(prop, scdKey, distinct_id, mutations, created) {
|
|
|
816
850
|
}
|
|
817
851
|
|
|
818
852
|
//todo
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
//
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
//
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
//
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
//
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
//
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
// $insert_id: campaign_id,
|
|
872
|
-
// time: dayjs.utc(day).hour(12).unix(),
|
|
873
|
-
// $source: "DM3",
|
|
874
|
-
// $mp_lib: "DM3"
|
|
875
|
-
// }
|
|
876
|
-
// });
|
|
877
|
-
// }
|
|
878
|
-
// }
|
|
879
|
-
// return data;
|
|
880
|
-
// }
|
|
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
|
+
}
|
|
900
|
+
|
|
901
|
+
|
|
902
|
+
}
|
|
903
|
+
return adSpendEvents;
|
|
904
|
+
}
|
|
881
905
|
|
|
882
906
|
|
|
883
907
|
|
|
@@ -905,12 +929,12 @@ if (require.main === module) {
|
|
|
905
929
|
console.log(`... using default COMPLEX configuration [everything] ...\n`);
|
|
906
930
|
console.log(`... for more simple data, don't use the --complex flag ...\n`);
|
|
907
931
|
console.log(`... or specify your own js config file (see docs or --help) ...\n`);
|
|
908
|
-
config = require(path.resolve(__dirname, "
|
|
932
|
+
config = require(path.resolve(__dirname, "../schemas/complex.js"));
|
|
909
933
|
}
|
|
910
934
|
else {
|
|
911
935
|
console.log(`... using default SIMPLE configuration [events + users] ...\n`);
|
|
912
936
|
console.log(`... for more complex data, use the --complex flag ...\n`);
|
|
913
|
-
config = require(path.resolve(__dirname, "
|
|
937
|
+
config = require(path.resolve(__dirname, "../schemas/simple.js"));
|
|
914
938
|
}
|
|
915
939
|
}
|
|
916
940
|
|
|
@@ -7,20 +7,24 @@ const dayjs = require('dayjs');
|
|
|
7
7
|
const utc = require('dayjs/plugin/utc');
|
|
8
8
|
const path = require('path');
|
|
9
9
|
const { mkdir } = require('ak-tools');
|
|
10
|
+
const { existsSync } = require('fs');
|
|
10
11
|
dayjs.extend(utc);
|
|
11
12
|
require('dotenv').config();
|
|
13
|
+
const { domainSuffix, domainPrefix } = require('./defaults');
|
|
12
14
|
|
|
13
|
-
/** @typedef {import('
|
|
14
|
-
/** @typedef {import('
|
|
15
|
-
/** @typedef {import('
|
|
16
|
-
/** @typedef {import('
|
|
17
|
-
/** @typedef {import('
|
|
18
|
-
/** @typedef {import('
|
|
19
|
-
/** @typedef {import('
|
|
15
|
+
/** @typedef {import('../types').Config} Config */
|
|
16
|
+
/** @typedef {import('../types').EventConfig} EventConfig */
|
|
17
|
+
/** @typedef {import('../types').ValueValid} ValueValid */
|
|
18
|
+
/** @typedef {import('../types').EnrichedArray} EnrichArray */
|
|
19
|
+
/** @typedef {import('../types').EnrichArrayOptions} EnrichArrayOptions */
|
|
20
|
+
/** @typedef {import('../types').Person} Person */
|
|
21
|
+
/** @typedef {import('../types').Funnel} Funnel */
|
|
20
22
|
|
|
21
23
|
let globalChance;
|
|
22
24
|
let chanceInitialized = false;
|
|
23
25
|
|
|
26
|
+
|
|
27
|
+
|
|
24
28
|
/*
|
|
25
29
|
----
|
|
26
30
|
RNG
|
|
@@ -127,6 +131,21 @@ function dates(inTheLast = 30, numPairs = 5, format = 'YYYY-MM-DD') {
|
|
|
127
131
|
return pairs;
|
|
128
132
|
};
|
|
129
133
|
|
|
134
|
+
function datesBetween(start, end) {
|
|
135
|
+
const result = [];
|
|
136
|
+
if (typeof start === 'number') start = dayjs.unix(start).utc();
|
|
137
|
+
if (typeof start !== 'number') start = dayjs(start).utc();
|
|
138
|
+
if (typeof end === 'number') end = dayjs.unix(end).utc();
|
|
139
|
+
if (typeof end !== 'number') end = dayjs(end).utc();
|
|
140
|
+
const diff = end.diff(start, 'day');
|
|
141
|
+
for (let i = 0; i < diff; i++) {
|
|
142
|
+
const day = start.add(i, 'day').startOf('day').add(12, 'hour');
|
|
143
|
+
result.push(day.toISOString());
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
return result;
|
|
147
|
+
}
|
|
148
|
+
|
|
130
149
|
/**
|
|
131
150
|
* returns a random date
|
|
132
151
|
* @param {any} start
|
|
@@ -223,77 +242,66 @@ function integer(min = 1, max = 100) {
|
|
|
223
242
|
};
|
|
224
243
|
|
|
225
244
|
|
|
245
|
+
/**
|
|
246
|
+
* Creates a function that generates a weighted list of items
|
|
247
|
+
* with a higher likelihood of picking a specified index and clear second and third place indices.
|
|
248
|
+
*
|
|
249
|
+
* @param {Array} items - The list of items to pick from.
|
|
250
|
+
* @param {number} [mostChosenIndex] - The index of the item to be most favored.
|
|
251
|
+
* @returns {function} - A function that returns a weighted list of items.
|
|
252
|
+
*/
|
|
226
253
|
function pickAWinner(items, mostChosenIndex) {
|
|
227
254
|
const chance = getChance();
|
|
228
|
-
|
|
229
|
-
|
|
255
|
+
|
|
256
|
+
// Ensure mostChosenIndex is within the bounds of the items array
|
|
257
|
+
if (!items) return () => { return ""; };
|
|
258
|
+
if (!items.length) return () => { return ""; };
|
|
259
|
+
if (!mostChosenIndex) mostChosenIndex = chance.integer({ min: 0, max: items.length - 1 });
|
|
260
|
+
if (mostChosenIndex >= items.length) mostChosenIndex = items.length - 1;
|
|
261
|
+
|
|
262
|
+
// Calculate second and third most chosen indices
|
|
263
|
+
const secondMostChosenIndex = (mostChosenIndex + 1) % items.length;
|
|
264
|
+
const thirdMostChosenIndex = (mostChosenIndex + 2) % items.length;
|
|
265
|
+
|
|
266
|
+
// Return a function that generates a weighted list
|
|
230
267
|
return function () {
|
|
231
268
|
const weighted = [];
|
|
232
269
|
for (let i = 0; i < 10; i++) {
|
|
233
|
-
|
|
270
|
+
const rand = chance.d10(); // Random number between 1 and 10
|
|
271
|
+
|
|
272
|
+
// 35% chance to favor the most chosen index
|
|
273
|
+
if (chance.bool({ likelihood: 35 })) {
|
|
274
|
+
// 50% chance to slightly alter the index
|
|
234
275
|
if (chance.bool({ likelihood: 50 })) {
|
|
235
276
|
weighted.push(items[mostChosenIndex]);
|
|
236
|
-
}
|
|
237
|
-
else {
|
|
238
|
-
const rand = chance.d10();
|
|
277
|
+
} else {
|
|
239
278
|
const addOrSubtract = chance.bool({ likelihood: 50 }) ? -rand : rand;
|
|
240
279
|
let newIndex = mostChosenIndex + addOrSubtract;
|
|
280
|
+
|
|
281
|
+
// Ensure newIndex is within bounds
|
|
241
282
|
if (newIndex < 0) newIndex = 0;
|
|
242
|
-
if (newIndex
|
|
283
|
+
if (newIndex >= items.length) newIndex = items.length - 1;
|
|
243
284
|
weighted.push(items[newIndex]);
|
|
244
285
|
}
|
|
245
286
|
}
|
|
287
|
+
// 25% chance to favor the second most chosen index
|
|
288
|
+
else if (chance.bool({ likelihood: 25 })) {
|
|
289
|
+
weighted.push(items[secondMostChosenIndex]);
|
|
290
|
+
}
|
|
291
|
+
// 15% chance to favor the third most chosen index
|
|
292
|
+
else if (chance.bool({ likelihood: 15 })) {
|
|
293
|
+
weighted.push(items[thirdMostChosenIndex]);
|
|
294
|
+
}
|
|
295
|
+
// Otherwise, pick a random item from the list
|
|
246
296
|
else {
|
|
247
297
|
weighted.push(chance.pickone(items));
|
|
248
298
|
}
|
|
249
299
|
}
|
|
250
300
|
return weighted;
|
|
251
|
-
|
|
252
301
|
};
|
|
253
302
|
}
|
|
254
303
|
|
|
255
304
|
|
|
256
|
-
function inferFunnels(events) {
|
|
257
|
-
const createdFunnels = [];
|
|
258
|
-
const firstEvents = events.filter((e) => e.isFirstEvent).map((e) => e.event);
|
|
259
|
-
const usageEvents = events.filter((e) => !e.isFirstEvent).map((e) => e.event);
|
|
260
|
-
const numFunnelsToCreate = Math.ceil(usageEvents.length);
|
|
261
|
-
/** @type {Funnel} */
|
|
262
|
-
const funnelTemplate = {
|
|
263
|
-
sequence: [],
|
|
264
|
-
conversionRate: 50,
|
|
265
|
-
order: 'sequential',
|
|
266
|
-
requireRepeats: false,
|
|
267
|
-
props: {},
|
|
268
|
-
timeToConvert: 1,
|
|
269
|
-
isFirstFunnel: false,
|
|
270
|
-
weight: 1
|
|
271
|
-
};
|
|
272
|
-
if (firstEvents.length) {
|
|
273
|
-
for (const event of firstEvents) {
|
|
274
|
-
createdFunnels.push({ ...clone(funnelTemplate), sequence: [event], isFirstFunnel: true, conversionRate: 100 });
|
|
275
|
-
}
|
|
276
|
-
}
|
|
277
|
-
|
|
278
|
-
//at least one funnel with all usage events
|
|
279
|
-
createdFunnels.push({ ...clone(funnelTemplate), sequence: usageEvents });
|
|
280
|
-
|
|
281
|
-
//for the rest, make random funnels
|
|
282
|
-
followUpFunnels: for (let i = 1; i < numFunnelsToCreate; i++) {
|
|
283
|
-
/** @type {Funnel} */
|
|
284
|
-
const funnel = { ...clone(funnelTemplate) };
|
|
285
|
-
funnel.conversionRate = integer(25, 75);
|
|
286
|
-
funnel.timeToConvert = integer(1, 10);
|
|
287
|
-
funnel.weight = integer(1, 10);
|
|
288
|
-
const sequence = shuffleArray(usageEvents).slice(0, integer(2, usageEvents.length));
|
|
289
|
-
funnel.sequence = sequence;
|
|
290
|
-
funnel.order = 'random';
|
|
291
|
-
createdFunnels.push(funnel);
|
|
292
|
-
}
|
|
293
|
-
|
|
294
|
-
return createdFunnels;
|
|
295
|
-
|
|
296
|
-
}
|
|
297
305
|
|
|
298
306
|
/*
|
|
299
307
|
----
|
|
@@ -360,6 +368,53 @@ function range(a, b, step = 1) {
|
|
|
360
368
|
};
|
|
361
369
|
|
|
362
370
|
|
|
371
|
+
/**
|
|
372
|
+
* create funnels out of random events
|
|
373
|
+
* @param {EventConfig[]} events
|
|
374
|
+
*/
|
|
375
|
+
function inferFunnels(events) {
|
|
376
|
+
const createdFunnels = [];
|
|
377
|
+
const firstEvents = events.filter((e) => e.isFirstEvent).map((e) => e.event);
|
|
378
|
+
const usageEvents = events.filter((e) => !e.isFirstEvent).map((e) => e.event);
|
|
379
|
+
const numFunnelsToCreate = Math.ceil(usageEvents.length);
|
|
380
|
+
/** @type {Funnel} */
|
|
381
|
+
const funnelTemplate = {
|
|
382
|
+
sequence: [],
|
|
383
|
+
conversionRate: 50,
|
|
384
|
+
order: 'sequential',
|
|
385
|
+
requireRepeats: false,
|
|
386
|
+
props: {},
|
|
387
|
+
timeToConvert: 1,
|
|
388
|
+
isFirstFunnel: false,
|
|
389
|
+
weight: 1
|
|
390
|
+
};
|
|
391
|
+
if (firstEvents.length) {
|
|
392
|
+
for (const event of firstEvents) {
|
|
393
|
+
createdFunnels.push({ ...clone(funnelTemplate), sequence: [event], isFirstFunnel: true, conversionRate: 100 });
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
//at least one funnel with all usage events
|
|
398
|
+
createdFunnels.push({ ...clone(funnelTemplate), sequence: usageEvents });
|
|
399
|
+
|
|
400
|
+
//for the rest, make random funnels
|
|
401
|
+
followUpFunnels: for (let i = 1; i < numFunnelsToCreate; i++) {
|
|
402
|
+
/** @type {Funnel} */
|
|
403
|
+
const funnel = { ...clone(funnelTemplate) };
|
|
404
|
+
funnel.conversionRate = integer(25, 75);
|
|
405
|
+
funnel.timeToConvert = integer(1, 10);
|
|
406
|
+
funnel.weight = integer(1, 10);
|
|
407
|
+
const sequence = shuffleArray(usageEvents).slice(0, integer(2, usageEvents.length));
|
|
408
|
+
funnel.sequence = sequence;
|
|
409
|
+
funnel.order = 'random';
|
|
410
|
+
createdFunnels.push(funnel);
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
return createdFunnels;
|
|
414
|
+
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
|
|
363
418
|
/*
|
|
364
419
|
----
|
|
365
420
|
STREAMERS
|
|
@@ -425,8 +480,11 @@ function weighFunnels(acc, funnel) {
|
|
|
425
480
|
/**
|
|
426
481
|
* a utility function to generate a range of numbers within a given skew
|
|
427
482
|
* Skew = 0.5: The values are more concentrated towards the extremes (both ends of the range) with a noticeable dip in the middle. The distribution appears more "U" shaped. Larger sizes result in smoother distributions but maintain the overall shape.
|
|
483
|
+
*
|
|
428
484
|
* Skew = 1: This represents the default normal distribution without skew. The values are normally distributed around the mean. Larger sizes create a clearer bell-shaped curve.
|
|
485
|
+
*
|
|
429
486
|
* Skew = 2: The values are more concentrated towards the mean, with a steeper drop-off towards the extremes. The distribution appears more peaked, resembling a "sharper" bell curve. Larger sizes enhance the clarity of this peaked distribution.
|
|
487
|
+
*
|
|
430
488
|
* Size represents the size of the pool to choose from; Larger sizes result in smoother distributions but maintain the overall shape.
|
|
431
489
|
* @param {number} min
|
|
432
490
|
* @param {number} max
|
|
@@ -517,6 +575,7 @@ function shuffleOutside(array) {
|
|
|
517
575
|
}
|
|
518
576
|
|
|
519
577
|
/**
|
|
578
|
+
* given a funnel, shuffle the events in the sequence with random events
|
|
520
579
|
* @param {EventConfig[]} funnel
|
|
521
580
|
* @param {EventConfig[]} possibles
|
|
522
581
|
*/
|
|
@@ -662,13 +721,18 @@ function buildFileNames(config) {
|
|
|
662
721
|
// const current = dayjs.utc().format("MM-DD-HH");
|
|
663
722
|
const simName = config.simulationName;
|
|
664
723
|
let writeDir = "./";
|
|
665
|
-
if (config.writeToDisk)
|
|
724
|
+
if (config.writeToDisk) {
|
|
725
|
+
const dataFolder = path.resolve("./data");
|
|
726
|
+
if (existsSync(dataFolder)) writeDir = dataFolder;
|
|
727
|
+
else writeDir = path.resolve("./");
|
|
728
|
+
}
|
|
666
729
|
if (typeof writeDir !== "string") throw new Error("writeDir must be a string");
|
|
667
730
|
if (typeof simName !== "string") throw new Error("simName must be a string");
|
|
668
731
|
|
|
669
732
|
const writePaths = {
|
|
670
733
|
eventFiles: [path.join(writeDir, `${simName}-EVENTS.${extension}`)],
|
|
671
734
|
userFiles: [path.join(writeDir, `${simName}-USERS.${extension}`)],
|
|
735
|
+
adSpendFiles: [path.join(writeDir, `${simName}-AD-SPEND.${extension}`)],
|
|
672
736
|
scdFiles: [],
|
|
673
737
|
mirrorFiles: [],
|
|
674
738
|
groupFiles: [],
|
|
@@ -713,11 +777,19 @@ function buildFileNames(config) {
|
|
|
713
777
|
return writePaths;
|
|
714
778
|
}
|
|
715
779
|
|
|
716
|
-
|
|
717
|
-
|
|
780
|
+
/**
|
|
781
|
+
* @param {[string, number][]} arrayOfArrays
|
|
782
|
+
*/
|
|
783
|
+
function progress(arrayOfArrays) {
|
|
718
784
|
// @ts-ignore
|
|
719
785
|
readline.cursorTo(process.stdout, 0);
|
|
720
|
-
|
|
786
|
+
let message = "";
|
|
787
|
+
for (const status of arrayOfArrays) {
|
|
788
|
+
const [thing, p] = status;
|
|
789
|
+
message += `${thing} processed: ${comma(p)}\t\t`;
|
|
790
|
+
}
|
|
791
|
+
|
|
792
|
+
process.stdout.write(message);
|
|
721
793
|
};
|
|
722
794
|
|
|
723
795
|
|
|
@@ -823,19 +895,16 @@ function TimeSoup(earliestTime, latestTime, peaks = 5, deviation = 2, mean = 0)
|
|
|
823
895
|
function person(userId, bornDaysAgo = 30, isAnonymous = false) {
|
|
824
896
|
const chance = getChance();
|
|
825
897
|
//names and photos
|
|
826
|
-
const l = chance.letter;
|
|
898
|
+
const l = chance.letter.bind(chance);
|
|
827
899
|
let gender = chance.pickone(['male', 'female']);
|
|
828
900
|
if (!gender) gender = "female";
|
|
829
901
|
// @ts-ignore
|
|
830
902
|
let first = chance.first({ gender });
|
|
831
903
|
let last = chance.last();
|
|
832
904
|
let name = `${first} ${last}`;
|
|
833
|
-
let email = `${first[0]}.${last}@${
|
|
905
|
+
let email = `${first[0]}.${last}@${choose(domainPrefix)}.${choose(domainSuffix)}`;
|
|
834
906
|
let avatarPrefix = `https://randomuser.me/api/portraits`;
|
|
835
|
-
let randomAvatarNumber =
|
|
836
|
-
min: 1,
|
|
837
|
-
max: 99
|
|
838
|
-
});
|
|
907
|
+
let randomAvatarNumber = integer(1, 99);
|
|
839
908
|
let avPath = gender === 'male' ? `/men/${randomAvatarNumber}.jpg` : `/women/${randomAvatarNumber}.jpg`;
|
|
840
909
|
let avatar = avatarPrefix + avPath;
|
|
841
910
|
let created = dayjs.unix(global.NOW).subtract(bornDaysAgo, 'day').format('YYYY-MM-DD');
|
|
@@ -855,8 +924,8 @@ function person(userId, bornDaysAgo = 30, isAnonymous = false) {
|
|
|
855
924
|
|
|
856
925
|
if (isAnonymous) {
|
|
857
926
|
user.name = "Anonymous User";
|
|
858
|
-
user.email =
|
|
859
|
-
delete user.avatar;
|
|
927
|
+
user.email = l() + l() + `*`.repeat(integer(3, 6)) + l() + `@` + l() + `*`.repeat(integer(3, 6)) + l() + `.` + choose(domainSuffix);
|
|
928
|
+
delete user.avatar;
|
|
860
929
|
|
|
861
930
|
}
|
|
862
931
|
|
|
@@ -980,5 +1049,6 @@ module.exports = {
|
|
|
980
1049
|
buildFileNames,
|
|
981
1050
|
streamJSON,
|
|
982
1051
|
streamCSV,
|
|
983
|
-
inferFunnels
|
|
1052
|
+
inferFunnels,
|
|
1053
|
+
datesBetween
|
|
984
1054
|
};
|