make-mp-data 1.4.0 → 1.4.1

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/schemas/simple.js CHANGED
@@ -37,7 +37,7 @@ const config = {
37
37
  event: "checkout",
38
38
  weight: 2,
39
39
  properties: {
40
- amount: weightedRange(5, 500, .25, 1000),
40
+ amount: weightedRange(5, 500, .25),
41
41
  currency: ["USD", "CAD", "EUR", "BTC", "ETH", "JPY"],
42
42
  coupon: ["none", "none", "none", "none", "10%OFF", "20%OFF", "10%OFF", "20%OFF", "30%OFF", "40%OFF", "50%OFF"],
43
43
  numItems: weightedRange(1, 10),
@@ -48,7 +48,7 @@ const config = {
48
48
  event: "add to cart",
49
49
  weight: 4,
50
50
  properties: {
51
- amount: weightedRange(5, 500, .25, 1000),
51
+ amount: weightedRange(5, 500, .25),
52
52
  rating: weightedRange(1, 5),
53
53
  reviews: weightedRange(0, 35),
54
54
  isFeaturedItem: [true, false, false],
@@ -71,7 +71,7 @@ const config = {
71
71
  properties: {
72
72
  videoCategory: pickAWinner(videoCategories, integer(0, 9)),
73
73
  isFeaturedItem: [true, false, false],
74
- watchTimeSec: weightedRange(10, 600, .25, 1000),
74
+ watchTimeSec: weightedRange(10, 600, .25),
75
75
  quality: ["2160p", "1440p", "1080p", "720p", "480p", "360p", "240p"],
76
76
  format: ["mp4", "avi", "mov", "mpg"],
77
77
  uploader_id: chance.guid.bind(chance)
@@ -133,7 +133,7 @@ const config = {
133
133
  profit: { events: ["checkout"], values: [4, 2, 42, 420] },
134
134
  watchTimeSec: {
135
135
  events: ["watch video"],
136
- values: weightedRange(50, 1200, 6, 247)
136
+ values: weightedRange(50, 1200, 6)
137
137
  }
138
138
 
139
139
  },
package/scratch.mjs CHANGED
@@ -1,17 +1,19 @@
1
1
  import main from "./index.js";
2
2
  import amir from './customers/amir.js';
3
3
  import simple from './schemas/simple.js';
4
+ import funnels from './schemas/funnels.js';
5
+ import foobar from './schemas/foobar.js';
6
+ import complex from './schemas/complex.js';
7
+ import deepNest from './schemas/deepNest.js';
4
8
  import execSync from 'child_process';
5
9
 
6
10
 
7
11
  /** @type {main.Config} */
8
12
  const spec = {
9
- ...amir,
10
- numUsers: 1000,
11
- numEvents: 100000,
13
+ ...funnels,
12
14
  writeToDisk: false,
13
15
  verbose: true,
14
- makeChart: true,
16
+ makeChart: false,
15
17
  };
16
18
 
17
19
 
@@ -39,7 +39,11 @@ const { applySkew,
39
39
  buildFileNames,
40
40
  TimeSoup,
41
41
  getChance,
42
- initChance
42
+ initChance,
43
+ validateEventConfig,
44
+ validateTime,
45
+ interruptArray,
46
+ optimizedBoxMuller
43
47
  } = require('../utils');
44
48
 
45
49
 
@@ -59,7 +63,7 @@ describe('timesoup', () => {
59
63
  });
60
64
 
61
65
 
62
- describe('naming things', () => {
66
+ describe('names', () => {
63
67
 
64
68
  test('default config', () => {
65
69
  const config = { simulationName: 'testSim' };
@@ -182,7 +186,7 @@ describe('naming things', () => {
182
186
  });
183
187
 
184
188
 
185
- describe('determined random', () => {
189
+ describe('determinism', () => {
186
190
  test('initializes RNG with seed from environment variable', () => {
187
191
  process.env.SEED = 'test-seed';
188
192
  // @ts-ignore
@@ -206,8 +210,8 @@ describe('determined random', () => {
206
210
  });
207
211
 
208
212
 
209
- describe('generateUser', () => {
210
- test('creates a user with valid fields', () => {
213
+ describe('generation', () => {
214
+ test('users: can make', () => {
211
215
  const numDays = 30;
212
216
  const user = generateUser('uuid-123', numDays);
213
217
  expect(user).toHaveProperty('distinct_id');
@@ -219,7 +223,7 @@ describe('generateUser', () => {
219
223
  expect(user).toHaveProperty('sessionIds');
220
224
  });
221
225
 
222
- test('creates a user with a created date within the specified range', () => {
226
+ test('user: in time range', () => {
223
227
  const numDays = 30;
224
228
  const user = generateUser('uuid-123', numDays);
225
229
  const createdDate = dayjs(user.created, 'YYYY-MM-DD');
@@ -228,9 +232,86 @@ describe('generateUser', () => {
228
232
  });
229
233
  });
230
234
 
235
+ describe('validation', () => {
231
236
 
237
+ beforeAll(() => {
238
+ global.NOW = 1672531200; // fixed point in time for testing
239
+ });
240
+
241
+ test('events: throws non array', () => {
242
+ // @ts-ignore
243
+ expect(() => validateEventConfig("not an array")).toThrow("events must be an array");
244
+ });
245
+
246
+ test('events: strings', () => {
247
+ const events = ["event1", "event2"];
248
+ const result = validateEventConfig(events);
249
+
250
+ expect(result).toEqual([
251
+ { event: "event1", isFirstEvent: false, properties: {}, weight: expect.any(Number) },
252
+ { event: "event2", isFirstEvent: false, properties: {}, weight: expect.any(Number) },
253
+ ]);
254
+
255
+ result.forEach(event => {
256
+ expect(event.weight).toBeGreaterThanOrEqual(1);
257
+ expect(event.weight).toBeLessThanOrEqual(5);
258
+ });
259
+ });
260
+
261
+ test('events: objects', () => {
262
+ const events = [{ event: "event1", properties: { a: 1 } }, { event: "event2", properties: { b: 2 } }];
263
+ const result = validateEventConfig(events);
232
264
 
233
- describe('enrich array', () => {
265
+ expect(result).toEqual(events);
266
+ });
267
+
268
+ test('events: mix', () => {
269
+ const events = ["event1", { event: "event2", properties: { b: 2 } }];
270
+ // @ts-ignore
271
+ const result = validateEventConfig(events);
272
+
273
+ expect(result).toEqual([
274
+ { event: "event1", isFirstEvent: false, properties: {}, weight: expect.any(Number) },
275
+ { event: "event2", properties: { b: 2 } }
276
+ ]);
277
+
278
+ expect(result[0].weight).toBeGreaterThanOrEqual(1);
279
+ expect(result[0].weight).toBeLessThanOrEqual(5);
280
+ });
281
+
282
+ test('dates: between', () => {
283
+ const chosenTime = global.NOW - (60 * 60 * 24 * 15); // 15 days ago
284
+ const earliestTime = global.NOW - (60 * 60 * 24 * 30); // 30 days ago
285
+ const latestTime = global.NOW;
286
+ expect(validateTime(chosenTime, earliestTime, latestTime)).toBe(true);
287
+ });
288
+
289
+ test('dates: outside earliest', () => {
290
+ const chosenTime = global.NOW - (60 * 60 * 24 * 31); // 31 days ago
291
+ const earliestTime = global.NOW - (60 * 60 * 24 * 30); // 30 days ago
292
+ const latestTime = global.NOW;
293
+ expect(validateTime(chosenTime, earliestTime, latestTime)).toBe(false);
294
+ });
295
+
296
+ test('dates: outside latest', () => {
297
+ const chosenTime = -1;
298
+ const earliestTime = global.NOW - (60 * 60 * 24 * 30); // 30 days ago
299
+ const latestTime = global.NOW;
300
+ expect(validateTime(chosenTime, earliestTime, latestTime)).toBe(false);
301
+ });
302
+
303
+ test('dates: inference in', () => {
304
+ const chosenTime = global.NOW - (60 * 60 * 24 * 15); // 15 days ago
305
+ expect(validateTime(chosenTime)).toBe(true);
306
+ });
307
+
308
+ test('dates: inference out', () => {
309
+ const chosenTime = global.NOW - (60 * 60 * 24 * 31); // 31 days ago
310
+ expect(validateTime(chosenTime)).toBe(false);
311
+ });
312
+ });
313
+
314
+ describe('enrichment', () => {
234
315
  test('hook works', () => {
235
316
  const arr = [];
236
317
  const hook = (item) => item * 2;
@@ -238,7 +319,7 @@ describe('enrich array', () => {
238
319
  enrichedArray.hookPush(1);
239
320
  enrichedArray.hookPush(2);
240
321
  expect(enrichedArray.includes(2)).toBeTruthy();
241
- expect(enrichedArray.includes(4)).toBeTruthy();
322
+ expect(enrichedArray.includes(4)).toBeTruthy();
242
323
  });
243
324
 
244
325
  test('filter empties', () => {
@@ -247,7 +328,7 @@ describe('enrich array', () => {
247
328
  const enrichedArray = enrichArray(arr, { hook });
248
329
  enrichedArray.hookPush(null);
249
330
  enrichedArray.hookPush(undefined);
250
- enrichedArray.hookPush({});
331
+ enrichedArray.hookPush({});
251
332
  enrichedArray.hookPush({ a: 1 });
252
333
  enrichedArray.hookPush([1, 2]);
253
334
  expect(enrichedArray).toHaveLength(3);
@@ -256,12 +337,14 @@ describe('enrich array', () => {
256
337
  expect(enrichedArray.includes('[object Object]')).toBeTruthy();
257
338
  expect(enrichedArray.includes('1')).toBeTruthy();
258
339
  expect(enrichedArray.includes('2')).toBeTruthy();
259
-
340
+
260
341
  });
342
+
343
+
261
344
  });
262
345
 
263
346
 
264
- describe('utils', () => {
347
+ describe('utilities', () => {
265
348
 
266
349
  test('pick: works', () => {
267
350
  const array = [1, 2, 3];
@@ -343,13 +426,13 @@ describe('utils', () => {
343
426
  test('weightedRange: within range', () => {
344
427
  const values = weightedRange(5, 15);
345
428
  expect(values.every(v => v >= 5 && v <= 15)).toBe(true);
346
- expect(values.length).toBe(100);
429
+ expect(values.length).toBe(50);
347
430
  });
348
431
 
349
432
  test('applySkew: skews', () => {
350
433
  const value = boxMullerRandom();
351
434
  const skewedValue = applySkew(value, .25);
352
- expect(Math.abs(skewedValue)).toBeGreaterThanOrEqual(Math.abs(value));
435
+ expect(Math.abs(skewedValue)).toBeLessThanOrEqual(Math.abs(value));
353
436
  });
354
437
 
355
438
  test('mapToRange: works', () => {
@@ -537,5 +620,17 @@ describe('utils', () => {
537
620
  expect(stdDev).toBeCloseTo(1, 1);
538
621
  });
539
622
 
623
+ test('optimized box normal distribution', () => {
624
+ const values = [];
625
+ for (let i = 0; i < 10000; i++) {
626
+ values.push(optimizedBoxMuller());
627
+ }
628
+ const mean = values.reduce((sum, val) => sum + val, 0) / values.length;
629
+ const variance = values.reduce((sum, val) => sum + Math.pow(val - mean, 2), 0) / values.length;
630
+ const stdDev = Math.sqrt(variance);
631
+ expect(mean).toBeCloseTo(0, 1);
632
+ expect(stdDev).toBeCloseTo(1, 1);
633
+ });
634
+
540
635
 
541
636
  });
package/types.d.ts CHANGED
@@ -60,7 +60,7 @@ declare namespace main {
60
60
  }
61
61
 
62
62
  export interface EnrichedArray<T> extends Array<T> {
63
- hookPush: (item: T) => number;
63
+ hookPush: (item: T) => boolean;
64
64
  }
65
65
 
66
66
  export interface EventConfig {
@@ -85,13 +85,20 @@ declare namespace main {
85
85
  sequence: string[];
86
86
  weight?: number;
87
87
  isFirstFunnel?: boolean;
88
+ /**
89
+ * If true, the funnel will require the user to repeat the sequence of events in order to convert
90
+ * If false, the user does not need to repeat the sequence of events in order to convert
91
+ * ^ when false, users who repeat the repetitive steps are more likely to convert
92
+ */
93
+ requireRepeats?: boolean;
88
94
  order?:
89
95
  | "sequential"
90
96
  | "first-fixed"
91
97
  | "last-fixed"
92
98
  | "random"
93
99
  | "first-and-last-fixed"
94
- | "middle-fixed";
100
+ | "middle-fixed"
101
+ | "interrupted";
95
102
  conversionRate?: number;
96
103
  timeToConvert?: number;
97
104
  props?: Record<string, ValueValid>;
package/utils.js CHANGED
@@ -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;
@@ -268,6 +270,14 @@ function boxMullerRandom() {
268
270
  return Math.sqrt(-2.0 * Math.log(u)) * Math.cos(2.0 * Math.PI * v);
269
271
  };
270
272
 
273
+ function optimizedBoxMuller() {
274
+ const chance = getChance();
275
+ const u = Math.max(Math.min(chance.normal({ mean: .5, dev: .25 }), 1), 0);
276
+ const v = Math.max(Math.min(chance.normal({ mean: .5, dev: .25 }), 1), 0);
277
+ return Math.sqrt(-2.0 * Math.log(u)) * Math.cos(2.0 * Math.PI * v);
278
+
279
+ }
280
+
271
281
  /**
272
282
  * applies a skew to a value;
273
283
  * Skew=0.5: When the skew is 0.5, the distribution becomes more compressed, with values clustering closer to the mean.
@@ -377,12 +387,14 @@ function weighFunnels(acc, funnel) {
377
387
  * @param {number} skew=1
378
388
  * @param {number} size=100
379
389
  */
380
- function weightedRange(min, max, skew = 1, size = 100) {
390
+ function weightedRange(min, max, skew = 1, size = 50) {
391
+ if (size > 2000) size = 2000;
381
392
  const mean = (max + min) / 2;
382
393
  const sd = (max - min) / 4;
383
394
  const array = [];
384
395
  while (array.length < size) {
385
- const normalValue = boxMullerRandom();
396
+ // const normalValue = boxMullerRandom();
397
+ const normalValue = optimizedBoxMuller();
386
398
  const skewedValue = applySkew(normalValue, skew);
387
399
  const mappedValue = mapToRange(skewedValue, mean, sd);
388
400
  if (mappedValue >= min && mappedValue <= max) {
@@ -458,6 +470,76 @@ function shuffleOutside(array) {
458
470
  return [outsideShuffled[0], ...middleFixed, outsideShuffled[1]];
459
471
  }
460
472
 
473
+ /**
474
+ * @param {EventConfig[]} funnel
475
+ * @param {EventConfig[]} possibles
476
+ */
477
+ function interruptArray(funnel, possibles, percent = 50) {
478
+ if (!Array.isArray(funnel)) return funnel;
479
+ if (!Array.isArray(possibles)) return funnel;
480
+ if (!funnel.length) return funnel;
481
+ if (!possibles.length) return funnel;
482
+ const ignorePositions = [0, funnel.length - 1];
483
+ const chance = getChance();
484
+ loopSteps: for (const [index, event] of funnel.entries()) {
485
+ if (ignorePositions.includes(index)) continue loopSteps;
486
+ if (chance.bool({ likelihood: percent })) {
487
+ funnel[index] = chance.pickone(possibles);
488
+ }
489
+ }
490
+
491
+ return funnel;
492
+ }
493
+
494
+ /*
495
+ ----
496
+ VALIDATORS
497
+ ----
498
+ */
499
+
500
+
501
+ /**
502
+ * @param {EventConfig[] | string[]} events
503
+ */
504
+ function validateEventConfig(events) {
505
+ if (!Array.isArray(events)) throw new Error("events must be an array");
506
+ const cleanEventConfig = [];
507
+ for (const event of events) {
508
+ if (typeof event === "string") {
509
+ /** @type {EventConfig} */
510
+ const eventTemplate = {
511
+ event,
512
+ isFirstEvent: false,
513
+ properties: {},
514
+ weight: integer(1, 5)
515
+ };
516
+ cleanEventConfig.push(eventTemplate);
517
+ }
518
+ if (typeof event === "object") {
519
+ cleanEventConfig.push(event);
520
+ }
521
+ }
522
+ return cleanEventConfig;
523
+ }
524
+
525
+ function validateTime(chosenTime, earliestTime, latestTime) {
526
+ if (!earliestTime) earliestTime = global.NOW - (60 * 60 * 24 * 30); // 30 days ago
527
+ if (!latestTime) latestTime = global.NOW;
528
+
529
+ if (typeof chosenTime === 'number') {
530
+ if (chosenTime > 0) {
531
+ if (chosenTime > earliestTime) {
532
+ if (chosenTime < latestTime) {
533
+ return true;
534
+ }
535
+
536
+ }
537
+ }
538
+ }
539
+ return false;
540
+ }
541
+
542
+
461
543
  /*
462
544
  ----
463
545
  META
@@ -474,33 +556,43 @@ function enrichArray(arr = [], opts = {}) {
474
556
  const { hook = a => a, type = "", ...rest } = opts;
475
557
 
476
558
  function transformThenPush(item) {
477
- if (item === null) return 0;
478
- if (item === undefined) return 0;
559
+ if (item === null) return false;
560
+ if (item === undefined) return false;
479
561
  if (typeof item === 'object') {
480
- if (Object.keys(item).length === 0) return 0;
562
+ if (Object.keys(item).length === 0) return false;
481
563
  }
564
+
565
+ //hook is passed an array
482
566
  if (Array.isArray(item)) {
483
567
  for (const i of item) {
484
568
  try {
485
569
  const enriched = hook(i, type, rest);
486
- arr.push(enriched);
570
+ if (Array.isArray(enriched)) enriched.forEach(e => arr.push(e));
571
+ else arr.push(enriched);
572
+
487
573
  }
488
574
  catch (e) {
489
575
  console.error(`\n\nyour hook had an error\n\n`, e);
490
576
  arr.push(i);
577
+ return false;
491
578
  }
492
579
 
493
580
  }
494
- return -1;
581
+ return true;
495
582
  }
583
+
584
+ //hook is passed a single item
496
585
  else {
497
586
  try {
498
587
  const enriched = hook(item, type, rest);
499
- return arr.push(enriched);
588
+ if (Array.isArray(enriched)) enriched.forEach(e => arr.push(e));
589
+ else arr.push(enriched);
590
+ return true;
500
591
  }
501
592
  catch (e) {
502
593
  console.error(`\n\nyour hook had an error\n\n`, e);
503
- return arr.push(item);
594
+ arr.push(item);
595
+ return false;
504
596
  }
505
597
  }
506
598
 
@@ -674,22 +766,6 @@ function TimeSoup(earliestTime, latestTime, peaks = 5, deviation = 2, mean = 0)
674
766
  }
675
767
 
676
768
 
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
769
 
694
770
 
695
771
  /**
@@ -819,7 +895,7 @@ module.exports = {
819
895
 
820
896
  initChance,
821
897
  getChance,
822
-
898
+ validateTime,
823
899
  boxMullerRandom,
824
900
  applySkew,
825
901
  mapToRange,
@@ -832,17 +908,17 @@ module.exports = {
832
908
  pickAWinner,
833
909
  weighArray,
834
910
  weighFunnels,
835
-
911
+ validateEventConfig,
836
912
  shuffleArray,
837
913
  shuffleExceptFirst,
838
914
  shuffleExceptLast,
839
915
  fixFirstAndLast,
840
916
  shuffleMiddle,
841
917
  shuffleOutside,
842
-
918
+ interruptArray,
843
919
  generateUser,
844
920
  enrichArray,
845
-
921
+ optimizedBoxMuller,
846
922
  buildFileNames,
847
923
  streamJSON,
848
924
  streamCSV