make-mp-data 1.4.0 → 1.4.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/defaults.js +974 -0
- package/index.js +303 -152
- package/package.json +3 -3
- package/schemas/complex.js +14 -14
- package/schemas/funnels.js +66 -67
- package/schemas/simple.js +14 -4
- package/scratch.mjs +7 -4
- package/tests/unit.test.js +110 -14
- package/types.d.ts +24 -4
- package/utils.js +176 -41
package/utils.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
const fs = require('fs');
|
|
2
2
|
const Chance = require('chance');
|
|
3
3
|
const readline = require('readline');
|
|
4
|
-
const { comma, uid } = require('ak-tools');
|
|
4
|
+
const { comma, uid, clone } = require('ak-tools');
|
|
5
5
|
const { spawn } = require('child_process');
|
|
6
6
|
const dayjs = require('dayjs');
|
|
7
7
|
const utc = require('dayjs/plugin/utc');
|
|
@@ -11,10 +11,12 @@ dayjs.extend(utc);
|
|
|
11
11
|
require('dotenv').config();
|
|
12
12
|
|
|
13
13
|
/** @typedef {import('./types').Config} Config */
|
|
14
|
+
/** @typedef {import('./types').EventConfig} EventConfig */
|
|
14
15
|
/** @typedef {import('./types').ValueValid} ValueValid */
|
|
15
16
|
/** @typedef {import('./types').EnrichedArray} EnrichArray */
|
|
16
17
|
/** @typedef {import('./types').EnrichArrayOptions} EnrichArrayOptions */
|
|
17
18
|
/** @typedef {import('./types').Person} Person */
|
|
19
|
+
/** @typedef {import('./types').Funnel} Funnel */
|
|
18
20
|
|
|
19
21
|
let globalChance;
|
|
20
22
|
let chanceInitialized = false;
|
|
@@ -223,6 +225,7 @@ function integer(min = 1, max = 100) {
|
|
|
223
225
|
|
|
224
226
|
function pickAWinner(items, mostChosenIndex) {
|
|
225
227
|
const chance = getChance();
|
|
228
|
+
if (!mostChosenIndex) mostChosenIndex = integer(0, items.length);
|
|
226
229
|
if (mostChosenIndex > items.length) mostChosenIndex = items.length;
|
|
227
230
|
return function () {
|
|
228
231
|
const weighted = [];
|
|
@@ -250,6 +253,48 @@ function pickAWinner(items, mostChosenIndex) {
|
|
|
250
253
|
}
|
|
251
254
|
|
|
252
255
|
|
|
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
|
+
|
|
253
298
|
/*
|
|
254
299
|
----
|
|
255
300
|
GENERATORS
|
|
@@ -268,6 +313,17 @@ function boxMullerRandom() {
|
|
|
268
313
|
return Math.sqrt(-2.0 * Math.log(u)) * Math.cos(2.0 * Math.PI * v);
|
|
269
314
|
};
|
|
270
315
|
|
|
316
|
+
function optimizedBoxMuller() {
|
|
317
|
+
const chance = getChance();
|
|
318
|
+
const u = Math.max(Math.min(chance.normal({ mean: .5, dev: .25 }), 1), 0);
|
|
319
|
+
const v = Math.max(Math.min(chance.normal({ mean: .5, dev: .25 }), 1), 0);
|
|
320
|
+
const result = Math.sqrt(-2.0 * Math.log(u)) * Math.cos(2.0 * Math.PI * v);
|
|
321
|
+
//ensure we didn't get infinity
|
|
322
|
+
if (result === Infinity || result === -Infinity) return chance.floating({ min: 0, max: 1 });
|
|
323
|
+
return result;
|
|
324
|
+
|
|
325
|
+
}
|
|
326
|
+
|
|
271
327
|
/**
|
|
272
328
|
* applies a skew to a value;
|
|
273
329
|
* Skew=0.5: When the skew is 0.5, the distribution becomes more compressed, with values clustering closer to the mean.
|
|
@@ -377,12 +433,14 @@ function weighFunnels(acc, funnel) {
|
|
|
377
433
|
* @param {number} skew=1
|
|
378
434
|
* @param {number} size=100
|
|
379
435
|
*/
|
|
380
|
-
function weightedRange(min, max, skew = 1, size =
|
|
436
|
+
function weightedRange(min, max, skew = 1, size = 50) {
|
|
437
|
+
if (size > 2000) size = 2000;
|
|
381
438
|
const mean = (max + min) / 2;
|
|
382
439
|
const sd = (max - min) / 4;
|
|
383
440
|
const array = [];
|
|
384
441
|
while (array.length < size) {
|
|
385
|
-
const normalValue = boxMullerRandom();
|
|
442
|
+
// const normalValue = boxMullerRandom();
|
|
443
|
+
const normalValue = optimizedBoxMuller();
|
|
386
444
|
const skewedValue = applySkew(normalValue, skew);
|
|
387
445
|
const mappedValue = mapToRange(skewedValue, mean, sd);
|
|
388
446
|
if (mappedValue >= min && mappedValue <= max) {
|
|
@@ -458,6 +516,76 @@ function shuffleOutside(array) {
|
|
|
458
516
|
return [outsideShuffled[0], ...middleFixed, outsideShuffled[1]];
|
|
459
517
|
}
|
|
460
518
|
|
|
519
|
+
/**
|
|
520
|
+
* @param {EventConfig[]} funnel
|
|
521
|
+
* @param {EventConfig[]} possibles
|
|
522
|
+
*/
|
|
523
|
+
function interruptArray(funnel, possibles, percent = 50) {
|
|
524
|
+
if (!Array.isArray(funnel)) return funnel;
|
|
525
|
+
if (!Array.isArray(possibles)) return funnel;
|
|
526
|
+
if (!funnel.length) return funnel;
|
|
527
|
+
if (!possibles.length) return funnel;
|
|
528
|
+
const ignorePositions = [0, funnel.length - 1];
|
|
529
|
+
const chance = getChance();
|
|
530
|
+
loopSteps: for (const [index, event] of funnel.entries()) {
|
|
531
|
+
if (ignorePositions.includes(index)) continue loopSteps;
|
|
532
|
+
if (chance.bool({ likelihood: percent })) {
|
|
533
|
+
funnel[index] = chance.pickone(possibles);
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
return funnel;
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
/*
|
|
541
|
+
----
|
|
542
|
+
VALIDATORS
|
|
543
|
+
----
|
|
544
|
+
*/
|
|
545
|
+
|
|
546
|
+
|
|
547
|
+
/**
|
|
548
|
+
* @param {EventConfig[] | string[]} events
|
|
549
|
+
*/
|
|
550
|
+
function validateEventConfig(events) {
|
|
551
|
+
if (!Array.isArray(events)) throw new Error("events must be an array");
|
|
552
|
+
const cleanEventConfig = [];
|
|
553
|
+
for (const event of events) {
|
|
554
|
+
if (typeof event === "string") {
|
|
555
|
+
/** @type {EventConfig} */
|
|
556
|
+
const eventTemplate = {
|
|
557
|
+
event,
|
|
558
|
+
isFirstEvent: false,
|
|
559
|
+
properties: {},
|
|
560
|
+
weight: integer(1, 5)
|
|
561
|
+
};
|
|
562
|
+
cleanEventConfig.push(eventTemplate);
|
|
563
|
+
}
|
|
564
|
+
if (typeof event === "object") {
|
|
565
|
+
cleanEventConfig.push(event);
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
return cleanEventConfig;
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
function validateTime(chosenTime, earliestTime, latestTime) {
|
|
572
|
+
if (!earliestTime) earliestTime = global.NOW - (60 * 60 * 24 * 30); // 30 days ago
|
|
573
|
+
if (!latestTime) latestTime = global.NOW;
|
|
574
|
+
|
|
575
|
+
if (typeof chosenTime === 'number') {
|
|
576
|
+
if (chosenTime > 0) {
|
|
577
|
+
if (chosenTime > earliestTime) {
|
|
578
|
+
if (chosenTime < latestTime) {
|
|
579
|
+
return true;
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
return false;
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
|
|
461
589
|
/*
|
|
462
590
|
----
|
|
463
591
|
META
|
|
@@ -474,33 +602,43 @@ function enrichArray(arr = [], opts = {}) {
|
|
|
474
602
|
const { hook = a => a, type = "", ...rest } = opts;
|
|
475
603
|
|
|
476
604
|
function transformThenPush(item) {
|
|
477
|
-
if (item === null) return
|
|
478
|
-
if (item === undefined) return
|
|
605
|
+
if (item === null) return false;
|
|
606
|
+
if (item === undefined) return false;
|
|
479
607
|
if (typeof item === 'object') {
|
|
480
|
-
if (Object.keys(item).length === 0) return
|
|
608
|
+
if (Object.keys(item).length === 0) return false;
|
|
481
609
|
}
|
|
610
|
+
|
|
611
|
+
//hook is passed an array
|
|
482
612
|
if (Array.isArray(item)) {
|
|
483
613
|
for (const i of item) {
|
|
484
614
|
try {
|
|
485
615
|
const enriched = hook(i, type, rest);
|
|
486
|
-
arr.push(
|
|
616
|
+
if (Array.isArray(enriched)) enriched.forEach(e => arr.push(e));
|
|
617
|
+
else arr.push(enriched);
|
|
618
|
+
|
|
487
619
|
}
|
|
488
620
|
catch (e) {
|
|
489
621
|
console.error(`\n\nyour hook had an error\n\n`, e);
|
|
490
622
|
arr.push(i);
|
|
623
|
+
return false;
|
|
491
624
|
}
|
|
492
625
|
|
|
493
626
|
}
|
|
494
|
-
return
|
|
627
|
+
return true;
|
|
495
628
|
}
|
|
629
|
+
|
|
630
|
+
//hook is passed a single item
|
|
496
631
|
else {
|
|
497
632
|
try {
|
|
498
633
|
const enriched = hook(item, type, rest);
|
|
499
|
-
|
|
634
|
+
if (Array.isArray(enriched)) enriched.forEach(e => arr.push(e));
|
|
635
|
+
else arr.push(enriched);
|
|
636
|
+
return true;
|
|
500
637
|
}
|
|
501
638
|
catch (e) {
|
|
502
639
|
console.error(`\n\nyour hook had an error\n\n`, e);
|
|
503
|
-
|
|
640
|
+
arr.push(item);
|
|
641
|
+
return false;
|
|
504
642
|
}
|
|
505
643
|
}
|
|
506
644
|
|
|
@@ -674,50 +812,39 @@ function TimeSoup(earliestTime, latestTime, peaks = 5, deviation = 2, mean = 0)
|
|
|
674
812
|
}
|
|
675
813
|
|
|
676
814
|
|
|
677
|
-
function validateTime(chosenTime, earliestTime, latestTime) {
|
|
678
|
-
if (!earliestTime) earliestTime = global.NOW - (60 * 60 * 24 * 30); // 30 days ago
|
|
679
|
-
if (!latestTime) latestTime = global.NOW;
|
|
680
|
-
|
|
681
|
-
if (typeof chosenTime === 'number') {
|
|
682
|
-
if (chosenTime > 0) {
|
|
683
|
-
if (chosenTime > earliestTime) {
|
|
684
|
-
if (chosenTime < latestTime) {
|
|
685
|
-
return true;
|
|
686
|
-
}
|
|
687
|
-
|
|
688
|
-
}
|
|
689
|
-
}
|
|
690
|
-
}
|
|
691
|
-
return false;
|
|
692
|
-
}
|
|
693
815
|
|
|
694
816
|
|
|
695
817
|
/**
|
|
818
|
+
* @param {string} userId
|
|
696
819
|
* @param {number} bornDaysAgo=30
|
|
820
|
+
* @param {boolean} isAnonymous
|
|
697
821
|
* @return {Person}
|
|
698
822
|
*/
|
|
699
|
-
function person(bornDaysAgo = 30) {
|
|
823
|
+
function person(userId, bornDaysAgo = 30, isAnonymous = false) {
|
|
700
824
|
const chance = getChance();
|
|
701
825
|
//names and photos
|
|
826
|
+
const l = chance.letter;
|
|
702
827
|
let gender = chance.pickone(['male', 'female']);
|
|
703
828
|
if (!gender) gender = "female";
|
|
704
829
|
// @ts-ignore
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
830
|
+
let first = chance.first({ gender });
|
|
831
|
+
let last = chance.last();
|
|
832
|
+
let name = `${first} ${last}`;
|
|
833
|
+
let email = `${first[0]}.${last}@${chance.domain()}.com`;
|
|
834
|
+
let avatarPrefix = `https://randomuser.me/api/portraits`;
|
|
835
|
+
let randomAvatarNumber = chance.integer({
|
|
711
836
|
min: 1,
|
|
712
837
|
max: 99
|
|
713
838
|
});
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
839
|
+
let avPath = gender === 'male' ? `/men/${randomAvatarNumber}.jpg` : `/women/${randomAvatarNumber}.jpg`;
|
|
840
|
+
let avatar = avatarPrefix + avPath;
|
|
841
|
+
let created = dayjs.unix(global.NOW).subtract(bornDaysAgo, 'day').format('YYYY-MM-DD');
|
|
717
842
|
// const created = date(bornDaysAgo, true)();
|
|
718
843
|
|
|
844
|
+
|
|
719
845
|
/** @type {Person} */
|
|
720
846
|
const user = {
|
|
847
|
+
distinct_id: userId,
|
|
721
848
|
name,
|
|
722
849
|
email,
|
|
723
850
|
avatar,
|
|
@@ -726,6 +853,13 @@ function person(bornDaysAgo = 30) {
|
|
|
726
853
|
sessionIds: []
|
|
727
854
|
};
|
|
728
855
|
|
|
856
|
+
if (isAnonymous) {
|
|
857
|
+
user.name = "Anonymous User";
|
|
858
|
+
user.email = `${l()}${l()}****${l()}${l()}@${l()}**${l()}*.com`;
|
|
859
|
+
delete user.avatar;
|
|
860
|
+
|
|
861
|
+
}
|
|
862
|
+
|
|
729
863
|
//anon Ids
|
|
730
864
|
if (global.MP_SIMULATION_CONFIG?.anonIds) {
|
|
731
865
|
const clusterSize = integer(2, 10);
|
|
@@ -819,7 +953,7 @@ module.exports = {
|
|
|
819
953
|
|
|
820
954
|
initChance,
|
|
821
955
|
getChance,
|
|
822
|
-
|
|
956
|
+
validateTime,
|
|
823
957
|
boxMullerRandom,
|
|
824
958
|
applySkew,
|
|
825
959
|
mapToRange,
|
|
@@ -832,18 +966,19 @@ module.exports = {
|
|
|
832
966
|
pickAWinner,
|
|
833
967
|
weighArray,
|
|
834
968
|
weighFunnels,
|
|
835
|
-
|
|
969
|
+
validateEventConfig,
|
|
836
970
|
shuffleArray,
|
|
837
971
|
shuffleExceptFirst,
|
|
838
972
|
shuffleExceptLast,
|
|
839
973
|
fixFirstAndLast,
|
|
840
974
|
shuffleMiddle,
|
|
841
975
|
shuffleOutside,
|
|
842
|
-
|
|
976
|
+
interruptArray,
|
|
843
977
|
generateUser,
|
|
844
978
|
enrichArray,
|
|
845
|
-
|
|
979
|
+
optimizedBoxMuller,
|
|
846
980
|
buildFileNames,
|
|
847
981
|
streamJSON,
|
|
848
|
-
streamCSV
|
|
982
|
+
streamCSV,
|
|
983
|
+
inferFunnels
|
|
849
984
|
};
|