make-mp-data 1.4.4 → 1.5.0
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/.vscode/launch.json +1 -1
- package/.vscode/settings.json +1 -0
- package/index.js +1410 -0
- package/package.json +12 -9
- package/schemas/adspend.js +96 -0
- package/schemas/anon.js +4 -4
- package/schemas/big.js +160 -0
- package/schemas/complex.js +32 -25
- package/schemas/foobar.js +22 -7
- package/schemas/funnels.js +12 -12
- package/schemas/mirror.js +129 -0
- package/schemas/simple.js +18 -32
- package/scratch.mjs +33 -11
- package/scripts/jsdoctest.js +1 -1
- package/scripts/new.sh +68 -0
- package/{core → src}/cli.js +10 -3
- package/{core → src}/utils.js +167 -197
- package/tests/benchmark/concurrency.mjs +52 -0
- package/tests/coverage/.gitkeep +0 -0
- package/tests/e2e.test.js +245 -58
- package/tests/int.test.js +618 -0
- package/tests/jest.config.js +11 -2
- package/tests/testSoup.mjs +3 -3
- package/tests/unit.test.js +351 -227
- package/tsconfig.json +1 -1
- package/types.d.ts +217 -97
- package/core/index.js +0 -1008
- package/schemas/deepNest.js +0 -106
- /package/{data → dungeons}/.gitkeep +0 -0
- /package/{core → src}/chart.js +0 -0
- /package/{core → src}/defaults.js +0 -0
package/{core → src}/utils.js
RENAMED
|
@@ -15,8 +15,8 @@ const { domainSuffix, domainPrefix } = require('./defaults');
|
|
|
15
15
|
/** @typedef {import('../types').Config} Config */
|
|
16
16
|
/** @typedef {import('../types').EventConfig} EventConfig */
|
|
17
17
|
/** @typedef {import('../types').ValueValid} ValueValid */
|
|
18
|
-
/** @typedef {import('../types').
|
|
19
|
-
/** @typedef {import('../types').
|
|
18
|
+
/** @typedef {import('../types').HookedArray} hookArray */
|
|
19
|
+
/** @typedef {import('../types').hookArrayOptions} hookArrayOptions */
|
|
20
20
|
/** @typedef {import('../types').Person} Person */
|
|
21
21
|
/** @typedef {import('../types').Funnel} Funnel */
|
|
22
22
|
|
|
@@ -34,14 +34,15 @@ RNG
|
|
|
34
34
|
/**
|
|
35
35
|
* the random number generator initialization function
|
|
36
36
|
* @param {string} seed
|
|
37
|
+
* @returns {Chance}
|
|
37
38
|
*/
|
|
38
39
|
function initChance(seed) {
|
|
39
40
|
if (process.env.SEED) seed = process.env.SEED; // Override seed with environment variable if available
|
|
40
41
|
if (!chanceInitialized) {
|
|
41
42
|
globalChance = new Chance(seed);
|
|
42
|
-
if (global.MP_SIMULATION_CONFIG) global.MP_SIMULATION_CONFIG.chance = globalChance;
|
|
43
43
|
chanceInitialized = true;
|
|
44
44
|
}
|
|
45
|
+
return globalChance;
|
|
45
46
|
}
|
|
46
47
|
|
|
47
48
|
/**
|
|
@@ -50,12 +51,11 @@ function initChance(seed) {
|
|
|
50
51
|
*/
|
|
51
52
|
function getChance() {
|
|
52
53
|
if (!chanceInitialized) {
|
|
53
|
-
const seed = process.env.SEED ||
|
|
54
|
+
const seed = process.env.SEED || "";
|
|
54
55
|
if (!seed) {
|
|
55
|
-
return new Chance();
|
|
56
|
+
return new Chance(); // this is a new RNG and therefore not deterministic
|
|
56
57
|
}
|
|
57
|
-
initChance(seed);
|
|
58
|
-
return globalChance;
|
|
58
|
+
return initChance(seed);
|
|
59
59
|
}
|
|
60
60
|
return globalChance;
|
|
61
61
|
}
|
|
@@ -242,64 +242,6 @@ function integer(min = 1, max = 100) {
|
|
|
242
242
|
};
|
|
243
243
|
|
|
244
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
|
-
*/
|
|
253
|
-
function pickAWinner(items, mostChosenIndex) {
|
|
254
|
-
const chance = getChance();
|
|
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
|
|
267
|
-
return function () {
|
|
268
|
-
const weighted = [];
|
|
269
|
-
for (let i = 0; i < 10; i++) {
|
|
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
|
|
275
|
-
if (chance.bool({ likelihood: 50 })) {
|
|
276
|
-
weighted.push(items[mostChosenIndex]);
|
|
277
|
-
} else {
|
|
278
|
-
const addOrSubtract = chance.bool({ likelihood: 50 }) ? -rand : rand;
|
|
279
|
-
let newIndex = mostChosenIndex + addOrSubtract;
|
|
280
|
-
|
|
281
|
-
// Ensure newIndex is within bounds
|
|
282
|
-
if (newIndex < 0) newIndex = 0;
|
|
283
|
-
if (newIndex >= items.length) newIndex = items.length - 1;
|
|
284
|
-
weighted.push(items[newIndex]);
|
|
285
|
-
}
|
|
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
|
|
296
|
-
else {
|
|
297
|
-
weighted.push(chance.pickone(items));
|
|
298
|
-
}
|
|
299
|
-
}
|
|
300
|
-
return weighted;
|
|
301
|
-
};
|
|
302
|
-
}
|
|
303
245
|
|
|
304
246
|
|
|
305
247
|
|
|
@@ -359,61 +301,16 @@ function mapToRange(value, mean, sd) {
|
|
|
359
301
|
* @param {number} step=1
|
|
360
302
|
*/
|
|
361
303
|
function range(a, b, step = 1) {
|
|
304
|
+
const arr = [];
|
|
362
305
|
step = !step ? 1 : step;
|
|
363
306
|
b = b / step;
|
|
364
307
|
for (var i = a; i <= b; i++) {
|
|
365
|
-
|
|
308
|
+
arr.push(i * step);
|
|
366
309
|
}
|
|
367
|
-
return
|
|
310
|
+
return arr;
|
|
368
311
|
};
|
|
369
312
|
|
|
370
313
|
|
|
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
314
|
|
|
418
315
|
/*
|
|
419
316
|
----
|
|
@@ -491,7 +388,7 @@ function weighFunnels(acc, funnel) {
|
|
|
491
388
|
* @param {number} skew=1
|
|
492
389
|
* @param {number} size=100
|
|
493
390
|
*/
|
|
494
|
-
function
|
|
391
|
+
function weighNumRange(min, max, skew = 1, size = 50) {
|
|
495
392
|
if (size > 2000) size = 2000;
|
|
496
393
|
const mean = (max + min) / 2;
|
|
497
394
|
const sd = (max - min) / 4;
|
|
@@ -508,13 +405,16 @@ function weightedRange(min, max, skew = 1, size = 50) {
|
|
|
508
405
|
return array;
|
|
509
406
|
}
|
|
510
407
|
|
|
408
|
+
/**
|
|
409
|
+
* arbitrarily weigh an array of values to create repeats
|
|
410
|
+
* @param {Array<any>} arr
|
|
411
|
+
*/
|
|
511
412
|
function weighArray(arr) {
|
|
512
|
-
|
|
513
413
|
// Calculate the upper bound based on the size of the array with added noise
|
|
514
414
|
const maxCopies = arr.length + integer(1, arr.length);
|
|
515
415
|
|
|
516
416
|
// Create an empty array to store the weighted elements
|
|
517
|
-
|
|
417
|
+
const weightedArray = [];
|
|
518
418
|
|
|
519
419
|
// Iterate over the input array and copy each element a random number of times
|
|
520
420
|
arr.forEach(element => {
|
|
@@ -527,6 +427,106 @@ function weighArray(arr) {
|
|
|
527
427
|
return weightedArray;
|
|
528
428
|
}
|
|
529
429
|
|
|
430
|
+
/**
|
|
431
|
+
* Creates a function that generates a weighted array of values.
|
|
432
|
+
*
|
|
433
|
+
* @overload
|
|
434
|
+
* @param {Array<{value: string, weight: number}>} items - An array of weighted objects or an array of strings.
|
|
435
|
+
* @returns {function(): Array<string>} A function that returns a weighted array of values when called.
|
|
436
|
+
*
|
|
437
|
+
* @overload
|
|
438
|
+
* @param {Array<string>} items - An array of strings.
|
|
439
|
+
* @returns {function(): Array<string>} A function that returns a weighted array with automatically assigned random weights to each string.
|
|
440
|
+
*/
|
|
441
|
+
|
|
442
|
+
function weighChoices(items) {
|
|
443
|
+
let weightedItems;
|
|
444
|
+
|
|
445
|
+
// If items are strings, assign unique random weights
|
|
446
|
+
if (items.every(item => typeof item === 'string')) {
|
|
447
|
+
const weights = shuffleArray(range(1, items.length));
|
|
448
|
+
weightedItems = items.map((item, index) => ({
|
|
449
|
+
value: item,
|
|
450
|
+
weight: weights[index]
|
|
451
|
+
}));
|
|
452
|
+
} else {
|
|
453
|
+
weightedItems = items;
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
return function generateWeightedArray() {
|
|
457
|
+
const weightedArray = [];
|
|
458
|
+
|
|
459
|
+
// Add each value to the array the number of times specified by its weight
|
|
460
|
+
weightedItems.forEach(({ value, weight }) => {
|
|
461
|
+
if (!weight) weight = 1;
|
|
462
|
+
for (let i = 0; i < weight; i++) {
|
|
463
|
+
weightedArray.push(value);
|
|
464
|
+
}
|
|
465
|
+
});
|
|
466
|
+
|
|
467
|
+
return weightedArray;
|
|
468
|
+
};
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
/**
|
|
472
|
+
* Creates a function that generates a weighted list of items
|
|
473
|
+
* with a higher likelihood of picking a specified index and clear second and third place indices.
|
|
474
|
+
*
|
|
475
|
+
* @param {Array} items - The list of items to pick from.
|
|
476
|
+
* @param {number} [mostChosenIndex] - The index of the item to be most favored.
|
|
477
|
+
* @returns {function} - A function that returns a weighted list of items.
|
|
478
|
+
*/
|
|
479
|
+
function pickAWinner(items, mostChosenIndex) {
|
|
480
|
+
const chance = getChance();
|
|
481
|
+
|
|
482
|
+
// Ensure mostChosenIndex is within the bounds of the items array
|
|
483
|
+
if (!items) return () => { return ""; };
|
|
484
|
+
if (!items.length) return () => { return ""; };
|
|
485
|
+
if (!mostChosenIndex) mostChosenIndex = chance.integer({ min: 0, max: items.length - 1 });
|
|
486
|
+
if (mostChosenIndex >= items.length) mostChosenIndex = items.length - 1;
|
|
487
|
+
|
|
488
|
+
// Calculate second and third most chosen indices
|
|
489
|
+
const secondMostChosenIndex = (mostChosenIndex + 1) % items.length;
|
|
490
|
+
const thirdMostChosenIndex = (mostChosenIndex + 2) % items.length;
|
|
491
|
+
|
|
492
|
+
// Return a function that generates a weighted list
|
|
493
|
+
return function () {
|
|
494
|
+
const weighted = [];
|
|
495
|
+
for (let i = 0; i < 10; i++) {
|
|
496
|
+
const rand = chance.d10(); // Random number between 1 and 10
|
|
497
|
+
|
|
498
|
+
// 35% chance to favor the most chosen index
|
|
499
|
+
if (chance.bool({ likelihood: 35 })) {
|
|
500
|
+
// 50% chance to slightly alter the index
|
|
501
|
+
if (chance.bool({ likelihood: 50 })) {
|
|
502
|
+
weighted.push(items[mostChosenIndex]);
|
|
503
|
+
} else {
|
|
504
|
+
const addOrSubtract = chance.bool({ likelihood: 50 }) ? -rand : rand;
|
|
505
|
+
let newIndex = mostChosenIndex + addOrSubtract;
|
|
506
|
+
|
|
507
|
+
// Ensure newIndex is within bounds
|
|
508
|
+
if (newIndex < 0) newIndex = 0;
|
|
509
|
+
if (newIndex >= items.length) newIndex = items.length - 1;
|
|
510
|
+
weighted.push(items[newIndex]);
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
// 25% chance to favor the second most chosen index
|
|
514
|
+
else if (chance.bool({ likelihood: 25 })) {
|
|
515
|
+
weighted.push(items[secondMostChosenIndex]);
|
|
516
|
+
}
|
|
517
|
+
// 15% chance to favor the third most chosen index
|
|
518
|
+
else if (chance.bool({ likelihood: 15 })) {
|
|
519
|
+
weighted.push(items[thirdMostChosenIndex]);
|
|
520
|
+
}
|
|
521
|
+
// Otherwise, pick a random item from the list
|
|
522
|
+
else {
|
|
523
|
+
weighted.push(chance.pickone(items));
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
return weighted;
|
|
527
|
+
};
|
|
528
|
+
}
|
|
529
|
+
|
|
530
530
|
/*
|
|
531
531
|
----
|
|
532
532
|
SHUFFLERS
|
|
@@ -627,7 +627,7 @@ function validateEventConfig(events) {
|
|
|
627
627
|
return cleanEventConfig;
|
|
628
628
|
}
|
|
629
629
|
|
|
630
|
-
function
|
|
630
|
+
function validTime(chosenTime, earliestTime, latestTime) {
|
|
631
631
|
if (!earliestTime) earliestTime = global.NOW - (60 * 60 * 24 * 30); // 30 days ago
|
|
632
632
|
if (!latestTime) latestTime = global.NOW;
|
|
633
633
|
|
|
@@ -644,6 +644,17 @@ function validateTime(chosenTime, earliestTime, latestTime) {
|
|
|
644
644
|
return false;
|
|
645
645
|
}
|
|
646
646
|
|
|
647
|
+
function validEvent(row) {
|
|
648
|
+
if (!row) return false;
|
|
649
|
+
if (!row.event) return false;
|
|
650
|
+
if (!row.time) return false;
|
|
651
|
+
if (!row.device_id && !row.user_id) return false;
|
|
652
|
+
if (!row.insert_id) return false;
|
|
653
|
+
if (!row.source) return false;
|
|
654
|
+
if (typeof row.time !== 'string') return false;
|
|
655
|
+
return true;
|
|
656
|
+
}
|
|
657
|
+
|
|
647
658
|
|
|
648
659
|
/*
|
|
649
660
|
----
|
|
@@ -651,76 +662,18 @@ META
|
|
|
651
662
|
----
|
|
652
663
|
*/
|
|
653
664
|
|
|
654
|
-
/**
|
|
655
|
-
* our meta programming function which lets you mutate items as they are pushed into an array
|
|
656
|
-
* @param {any[]} arr
|
|
657
|
-
* @param {EnrichArrayOptions} opts
|
|
658
|
-
* @returns {EnrichArray}}
|
|
659
|
-
*/
|
|
660
|
-
function enrichArray(arr = [], opts = {}) {
|
|
661
|
-
const { hook = a => a, type = "", ...rest } = opts;
|
|
662
|
-
|
|
663
|
-
function transformThenPush(item) {
|
|
664
|
-
if (item === null) return false;
|
|
665
|
-
if (item === undefined) return false;
|
|
666
|
-
if (typeof item === 'object') {
|
|
667
|
-
if (Object.keys(item).length === 0) return false;
|
|
668
|
-
}
|
|
669
665
|
|
|
670
|
-
//hook is passed an array
|
|
671
|
-
if (Array.isArray(item)) {
|
|
672
|
-
for (const i of item) {
|
|
673
|
-
try {
|
|
674
|
-
const enriched = hook(i, type, rest);
|
|
675
|
-
if (Array.isArray(enriched)) enriched.forEach(e => arr.push(e));
|
|
676
|
-
else arr.push(enriched);
|
|
677
|
-
|
|
678
|
-
}
|
|
679
|
-
catch (e) {
|
|
680
|
-
console.error(`\n\nyour hook had an error\n\n`, e);
|
|
681
|
-
arr.push(i);
|
|
682
|
-
return false;
|
|
683
|
-
}
|
|
684
|
-
|
|
685
|
-
}
|
|
686
|
-
return true;
|
|
687
|
-
}
|
|
688
|
-
|
|
689
|
-
//hook is passed a single item
|
|
690
|
-
else {
|
|
691
|
-
try {
|
|
692
|
-
const enriched = hook(item, type, rest);
|
|
693
|
-
if (Array.isArray(enriched)) enriched.forEach(e => arr.push(e));
|
|
694
|
-
else arr.push(enriched);
|
|
695
|
-
return true;
|
|
696
|
-
}
|
|
697
|
-
catch (e) {
|
|
698
|
-
console.error(`\n\nyour hook had an error\n\n`, e);
|
|
699
|
-
arr.push(item);
|
|
700
|
-
return false;
|
|
701
|
-
}
|
|
702
|
-
}
|
|
703
|
-
|
|
704
|
-
}
|
|
705
|
-
|
|
706
|
-
/** @type {EnrichArray} */
|
|
707
|
-
// @ts-ignore
|
|
708
|
-
const enrichedArray = arr;
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
enrichedArray.hookPush = transformThenPush;
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
return enrichedArray;
|
|
715
|
-
};
|
|
716
666
|
|
|
667
|
+
/**
|
|
668
|
+
* @param {Config} config
|
|
669
|
+
*/
|
|
717
670
|
function buildFileNames(config) {
|
|
718
671
|
const { format = "csv", groupKeys = [], lookupTables = [] } = config;
|
|
719
672
|
let extension = "";
|
|
720
673
|
extension = format === "csv" ? "csv" : "json";
|
|
721
674
|
// const current = dayjs.utc().format("MM-DD-HH");
|
|
722
|
-
|
|
723
|
-
let writeDir = "./";
|
|
675
|
+
let simName = config.simulationName;
|
|
676
|
+
let writeDir = typeof config.writeToDisk === 'string' ? config.writeToDisk : "./";
|
|
724
677
|
if (config.writeToDisk) {
|
|
725
678
|
const dataFolder = path.resolve("./data");
|
|
726
679
|
if (existsSync(dataFolder)) writeDir = dataFolder;
|
|
@@ -732,13 +685,17 @@ function buildFileNames(config) {
|
|
|
732
685
|
const writePaths = {
|
|
733
686
|
eventFiles: [path.join(writeDir, `${simName}-EVENTS.${extension}`)],
|
|
734
687
|
userFiles: [path.join(writeDir, `${simName}-USERS.${extension}`)],
|
|
735
|
-
adSpendFiles: [
|
|
688
|
+
adSpendFiles: [],
|
|
736
689
|
scdFiles: [],
|
|
737
690
|
mirrorFiles: [],
|
|
738
691
|
groupFiles: [],
|
|
739
692
|
lookupFiles: [],
|
|
740
693
|
folder: writeDir,
|
|
741
694
|
};
|
|
695
|
+
//add ad spend files
|
|
696
|
+
if (config?.hasAdSpend) {
|
|
697
|
+
writePaths.adSpendFiles.push(path.join(writeDir, `${simName}-AD-SPEND.${extension}`));
|
|
698
|
+
}
|
|
742
699
|
|
|
743
700
|
//add SCD files
|
|
744
701
|
const scdKeys = Object.keys(config?.scdProps || {});
|
|
@@ -781,7 +738,6 @@ function buildFileNames(config) {
|
|
|
781
738
|
* @param {[string, number][]} arrayOfArrays
|
|
782
739
|
*/
|
|
783
740
|
function progress(arrayOfArrays) {
|
|
784
|
-
// @ts-ignore
|
|
785
741
|
readline.cursorTo(process.stdout, 0);
|
|
786
742
|
let message = "";
|
|
787
743
|
for (const status of arrayOfArrays) {
|
|
@@ -819,8 +775,9 @@ CORE
|
|
|
819
775
|
*/
|
|
820
776
|
|
|
821
777
|
//the function which generates $distinct_id + $anonymous_ids, $session_ids, and created, skewing towards the present
|
|
822
|
-
function generateUser(user_id,
|
|
778
|
+
function generateUser(user_id, opts, amplitude = 1, frequency = 1, skew = 1) {
|
|
823
779
|
const chance = getChance();
|
|
780
|
+
const { numDays, isAnonymous, hasAvatar, hasAnonIds, hasSessionIds } = opts;
|
|
824
781
|
// Uniformly distributed `u`, then skew applied
|
|
825
782
|
let u = Math.pow(chance.random(), skew);
|
|
826
783
|
|
|
@@ -832,16 +789,18 @@ function generateUser(user_id, numDays, amplitude = 1, frequency = 1, skew = 1)
|
|
|
832
789
|
|
|
833
790
|
// Clamp values to ensure they are within the desired range
|
|
834
791
|
daysAgoBorn = Math.min(daysAgoBorn, numDays);
|
|
792
|
+
const props = person(user_id, daysAgoBorn, isAnonymous, hasAvatar, hasAnonIds, hasSessionIds);
|
|
835
793
|
|
|
836
794
|
const user = {
|
|
837
795
|
distinct_id: user_id,
|
|
838
|
-
...
|
|
796
|
+
...props,
|
|
839
797
|
};
|
|
840
798
|
|
|
841
799
|
|
|
842
800
|
return user;
|
|
843
801
|
}
|
|
844
802
|
|
|
803
|
+
let soupHits = 0;
|
|
845
804
|
/**
|
|
846
805
|
* build sign waves basically
|
|
847
806
|
* @param {number} [earliestTime]
|
|
@@ -867,9 +826,12 @@ function TimeSoup(earliestTime, latestTime, peaks = 5, deviation = 2, mean = 0)
|
|
|
867
826
|
let isValidTime = false;
|
|
868
827
|
do {
|
|
869
828
|
iterations++;
|
|
829
|
+
soupHits++;
|
|
870
830
|
offset = chance.normal({ mean: mean, dev: chunkSize / deviation });
|
|
871
|
-
isValidTime =
|
|
872
|
-
if (iterations >
|
|
831
|
+
isValidTime = validTime(chunkMid + offset, earliestTime, latestTime);
|
|
832
|
+
if (iterations > 25000) {
|
|
833
|
+
throw `${iterations} iterations... exceeded`;
|
|
834
|
+
}
|
|
873
835
|
} while (chunkMid + offset < chunkStart || chunkMid + offset > chunkEnd);
|
|
874
836
|
|
|
875
837
|
try {
|
|
@@ -890,15 +852,17 @@ function TimeSoup(earliestTime, latestTime, peaks = 5, deviation = 2, mean = 0)
|
|
|
890
852
|
* @param {string} userId
|
|
891
853
|
* @param {number} bornDaysAgo=30
|
|
892
854
|
* @param {boolean} isAnonymous
|
|
855
|
+
* @param {boolean} hasAvatar
|
|
856
|
+
* @param {boolean} hasAnonIds
|
|
857
|
+
* @param {boolean} hasSessionIds
|
|
893
858
|
* @return {Person}
|
|
894
859
|
*/
|
|
895
|
-
function person(userId, bornDaysAgo = 30, isAnonymous = false) {
|
|
860
|
+
function person(userId, bornDaysAgo = 30, isAnonymous = false, hasAvatar = false, hasAnonIds = false, hasSessionIds = false) {
|
|
896
861
|
const chance = getChance();
|
|
897
862
|
//names and photos
|
|
898
863
|
const l = chance.letter.bind(chance);
|
|
899
864
|
let gender = chance.pickone(['male', 'female']);
|
|
900
865
|
if (!gender) gender = "female";
|
|
901
|
-
// @ts-ignore
|
|
902
866
|
let first = chance.first({ gender });
|
|
903
867
|
let last = chance.last();
|
|
904
868
|
let name = `${first} ${last}`;
|
|
@@ -926,21 +890,23 @@ function person(userId, bornDaysAgo = 30, isAnonymous = false) {
|
|
|
926
890
|
user.name = "Anonymous User";
|
|
927
891
|
user.email = l() + l() + `*`.repeat(integer(3, 6)) + l() + `@` + l() + `*`.repeat(integer(3, 6)) + l() + `.` + choose(domainSuffix);
|
|
928
892
|
delete user.avatar;
|
|
929
|
-
|
|
930
893
|
}
|
|
931
894
|
|
|
895
|
+
if (!hasAvatar) delete user.avatar;
|
|
896
|
+
|
|
932
897
|
//anon Ids
|
|
933
|
-
if (
|
|
898
|
+
if (hasAnonIds) {
|
|
934
899
|
const clusterSize = integer(2, 10);
|
|
935
900
|
for (let i = 0; i < clusterSize; i++) {
|
|
936
901
|
const anonId = uid(42);
|
|
937
902
|
user.anonymousIds.push(anonId);
|
|
938
903
|
}
|
|
939
|
-
|
|
940
904
|
}
|
|
941
905
|
|
|
906
|
+
if (!hasAnonIds) delete user.anonymousIds;
|
|
907
|
+
|
|
942
908
|
//session Ids
|
|
943
|
-
if (
|
|
909
|
+
if (hasSessionIds) {
|
|
944
910
|
const sessionSize = integer(5, 30);
|
|
945
911
|
for (let i = 0; i < sessionSize; i++) {
|
|
946
912
|
const sessionId = [uid(5), uid(5), uid(5), uid(5)].join("-");
|
|
@@ -948,6 +914,8 @@ function person(userId, bornDaysAgo = 30, isAnonymous = false) {
|
|
|
948
914
|
}
|
|
949
915
|
}
|
|
950
916
|
|
|
917
|
+
if (!hasSessionIds) delete user.sessionIds;
|
|
918
|
+
|
|
951
919
|
return user;
|
|
952
920
|
};
|
|
953
921
|
|
|
@@ -1022,11 +990,14 @@ module.exports = {
|
|
|
1022
990
|
|
|
1023
991
|
initChance,
|
|
1024
992
|
getChance,
|
|
1025
|
-
|
|
993
|
+
|
|
994
|
+
validTime,
|
|
995
|
+
validEvent,
|
|
996
|
+
|
|
1026
997
|
boxMullerRandom,
|
|
1027
998
|
applySkew,
|
|
1028
999
|
mapToRange,
|
|
1029
|
-
|
|
1000
|
+
weighNumRange,
|
|
1030
1001
|
progress,
|
|
1031
1002
|
range,
|
|
1032
1003
|
openFinder,
|
|
@@ -1044,11 +1015,10 @@ module.exports = {
|
|
|
1044
1015
|
shuffleOutside,
|
|
1045
1016
|
interruptArray,
|
|
1046
1017
|
generateUser,
|
|
1047
|
-
enrichArray,
|
|
1048
1018
|
optimizedBoxMuller,
|
|
1049
1019
|
buildFileNames,
|
|
1050
1020
|
streamJSON,
|
|
1051
1021
|
streamCSV,
|
|
1052
|
-
|
|
1053
|
-
|
|
1022
|
+
datesBetween,
|
|
1023
|
+
weighChoices
|
|
1054
1024
|
};
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/*
|
|
2
|
+
----
|
|
3
|
+
TO DOs
|
|
4
|
+
----
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
//!feature: fixedTimeFunnel? if set this funnel will occur for all users at the same time ['cards charged', 'charge complete']
|
|
8
|
+
//!feature: churn ... is churnFunnel, possible to return, etc
|
|
9
|
+
//!feature: send SCD data to mixpanel (blocked on dev)
|
|
10
|
+
//!feature: send and map lookup tables to mixpanel (also blocked on dev)
|
|
11
|
+
//!bug: using --mc flag reverts to --complex for some reason
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
import main from "../../index.js";
|
|
15
|
+
import simple from '../../schemas/simple.js';
|
|
16
|
+
|
|
17
|
+
/** @typedef {import('../../types').Config} Config */
|
|
18
|
+
|
|
19
|
+
/** @type {Config} */
|
|
20
|
+
const noWrites = {
|
|
21
|
+
...simple,
|
|
22
|
+
numUsers: 10_000,
|
|
23
|
+
numEvents: 250_000,
|
|
24
|
+
writeToDisk: false,
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
/** @type {Config} */
|
|
28
|
+
const yesWrites = {
|
|
29
|
+
...noWrites,
|
|
30
|
+
writeToDisk: true
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
console.log('concurrency benchmarking');
|
|
34
|
+
|
|
35
|
+
const concurrency = [1, 2, 3, 4, 5];
|
|
36
|
+
|
|
37
|
+
const results = [];
|
|
38
|
+
for (const concurrent of concurrency) {
|
|
39
|
+
console.log(`concurrency: ${concurrent}`);
|
|
40
|
+
// @ts-ignore
|
|
41
|
+
const test = await main({ ...noWrites, concurrency: concurrent });
|
|
42
|
+
results.push({ human: test.time.human, concurrency: concurrent });
|
|
43
|
+
console.log(`\t\tdone: ${test.time.human}\n\n`);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const display = results.map((r) => {
|
|
47
|
+
return `concurrency: ${r.concurrency} | duration: ${r.human}`;
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
console.log(display.join('\n\n'));
|
|
51
|
+
|
|
52
|
+
debugger;
|
|
File without changes
|