make-mp-data 1.1.19 → 1.2.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.
@@ -1,5 +1,6 @@
1
1
  {
2
2
  "cSpell.words": [
3
+ "timesoup",
3
4
  "unparse"
4
5
  ],
5
6
  "jest.runMode": "on-demand",
package/cli.js CHANGED
@@ -35,7 +35,7 @@ DATA MODEL: https://github.com/ak--47/make-mp-data/blob/main/default.js
35
35
  type: 'string'
36
36
  })
37
37
  .option("seed", {
38
- demandOption: false,
38
+ demandOption: false,
39
39
  alias: 's',
40
40
  describe: 'randomness seed; used to create distinct_ids',
41
41
  type: 'string'
@@ -72,6 +72,24 @@ DATA MODEL: https://github.com/ak--47/make-mp-data/blob/main/default.js
72
72
  describe: 'either US or EU',
73
73
  type: 'string'
74
74
  })
75
+ .options("complex", {
76
+ demandOption: false,
77
+ default: false,
78
+ describe: 'use complex data model (model all entities)',
79
+ alias: 'c',
80
+ type: 'boolean',
81
+ coerce: (value) => {
82
+ if (typeof value === 'boolean') return value;
83
+ if (value === 'true') {
84
+ return true;
85
+ }
86
+ if (value === 'false') {
87
+ return false;
88
+ }
89
+
90
+ return true;
91
+ }
92
+ })
75
93
  .option("writeToDisk", {
76
94
  demandOption: false,
77
95
  default: true,
package/index.js CHANGED
@@ -14,23 +14,8 @@ const Chance = require("chance");
14
14
  const chance = new Chance();
15
15
  const { touch, comma, bytesHuman, mkdir } = require("ak-tools");
16
16
  const Papa = require("papaparse");
17
- const {
18
- weightedRange,
19
- pick,
20
- day,
21
- integer,
22
- makeProducts,
23
- date,
24
- progress,
25
- choose,
26
- range,
27
- exhaust,
28
- openFinder,
29
- applySkew,
30
- boxMullerRandom,
31
- getUniqueKeys,
32
- person
33
- } = require("./utils.js");
17
+ const u = require("./utils.js");
18
+ const AKsTimeSoup = require("./timesoup.js");
34
19
  const dayjs = require("dayjs");
35
20
  const utc = require("dayjs/plugin/utc");
36
21
  dayjs.extend(utc);
@@ -42,20 +27,6 @@ let VERBOSE = false;
42
27
  /** @typedef {import('./types.d.ts').Config} Config */
43
28
  /** @typedef {import('./types.d.ts').EventConfig} EventConfig */
44
29
 
45
-
46
- const PEAK_DAYS = [
47
- dayjs().subtract(2, "day").unix(),
48
- dayjs().subtract(3, "day").unix(),
49
- dayjs().subtract(5, "day").unix(),
50
- dayjs().subtract(7, "day").unix(),
51
- dayjs().subtract(11, "day").unix(),
52
- dayjs().subtract(13, "day").unix(),
53
- dayjs().subtract(17, "day").unix(),
54
- dayjs().subtract(19, "day").unix(),
55
- dayjs().subtract(23, "day").unix(),
56
- dayjs().subtract(29, "day").unix(),
57
- ];
58
-
59
30
  /**
60
31
  * generates fake mixpanel data
61
32
  * @param {Config} config
@@ -72,7 +43,7 @@ async function main(config) {
72
43
  favoriteColor: ["red", "green", "blue", "yellow"],
73
44
  spiritAnimal: chance.animal,
74
45
  },
75
- scdProps = { NPS: weightedRange(0, 10, 150, 1.6) },
46
+ scdProps = { NPS: u.weightedRange(0, 10, 150, 1.6) },
76
47
  groupKeys = [],
77
48
  groupProps = {},
78
49
  lookupTables = [],
@@ -86,7 +57,7 @@ async function main(config) {
86
57
  } = config;
87
58
  VERBOSE = verbose;
88
59
  config.simulationName = makeName();
89
- global.config = config;
60
+ global.MP_SIMULATION_CONFIG = config;
90
61
  const uuidChance = new Chance(seed);
91
62
  log(`------------------SETUP------------------`);
92
63
  log(`\nyour data simulation will heretofore be known as: \n\n\t${config.simulationName.toUpperCase()}...\n`);
@@ -97,18 +68,18 @@ async function main(config) {
97
68
  //the function which generates $distinct_id + $anonymous_ids, $session_ids, and $created, skewing towards the present
98
69
  function generateUser() {
99
70
  const distinct_id = uuidChance.guid();
100
- let z = boxMullerRandom();
71
+ let z = u.boxMullerRandom();
101
72
  const skew = chance.normal({ mean: 10, dev: 3 });
102
- z = applySkew(z, skew);
73
+ z = u.applySkew(z, skew);
103
74
 
104
75
  // Scale and shift the normally distributed value to fit the range of days
105
- const maxZ = integer(2, 4);
76
+ const maxZ = u.integer(2, 4);
106
77
  const scaledZ = (z / maxZ + 1) / 2;
107
78
  const daysAgoBorn = Math.round(scaledZ * (numDays - 1)) + 1;
108
79
 
109
80
  return {
110
81
  distinct_id,
111
- ...person(daysAgoBorn),
82
+ ...u.person(daysAgoBorn),
112
83
  };
113
84
  }
114
85
 
@@ -134,16 +105,16 @@ async function main(config) {
134
105
  const avgEvPerUser = Math.floor(numEvents / numUsers);
135
106
 
136
107
  //user loop
137
- log(`---------------SIMULATION----------------`, `\n\n`);
108
+ log(`---------------SIMULATION----------------`, "\n\n");
138
109
  for (let i = 1; i < numUsers + 1; i++) {
139
- progress("users", i);
110
+ u.progress("users", i);
140
111
  const user = generateUser();
141
112
  const { distinct_id, $created, anonymousIds, sessionIds } = user;
142
113
  userProfilesData.push(makeProfile(userProps, user));
143
114
  const mutations = chance.integer({ min: 1, max: 10 });
144
115
  scdTableData.push(makeSCD(scdProps, distinct_id, mutations, $created));
145
116
  const numEventsThisUser = Math.round(
146
- chance.normal({ mean: avgEvPerUser, dev: avgEvPerUser / integer(3, 7) })
117
+ chance.normal({ mean: avgEvPerUser, dev: avgEvPerUser / u.integer(3, 7) })
147
118
  );
148
119
 
149
120
  if (firstEvents.length) {
@@ -187,7 +158,7 @@ async function main(config) {
187
158
  const groupCardinality = groupPair[1];
188
159
  const groupProfiles = [];
189
160
  for (let i = 1; i < groupCardinality + 1; i++) {
190
- progress("groups", i);
161
+ u.progress("groups", i);
191
162
  const group = {
192
163
  [groupKey]: i,
193
164
  ...makeProfile(groupProps[groupKey]),
@@ -204,7 +175,7 @@ async function main(config) {
204
175
  const { key, entries, attributes } = lookupTable;
205
176
  const data = [];
206
177
  for (let i = 1; i < entries + 1; i++) {
207
- progress("lookups", i);
178
+ u.progress("lookups", i);
208
179
  const item = {
209
180
  [key]: i,
210
181
  ...makeProfile(attributes),
@@ -236,17 +207,19 @@ async function main(config) {
236
207
  log(`-----------------WRITES------------------`, `\n\n`);
237
208
  //write the files
238
209
  if (writeToDisk) {
239
- if (verbose) log(`writing files... for ${config.simulationName}`);
240
- for (const pair of pairs) {
210
+ if (verbose) log(`writing files... for ${config.simulationName}\n`);
211
+ loopFiles: for (const pair of pairs) {
241
212
  const [paths, data] = pair;
213
+ if (!data.length) continue loopFiles;
242
214
  for (const path of paths) {
243
215
  let datasetsToWrite;
244
216
  if (data?.[0]?.["key"]) datasetsToWrite = data.map((d) => d.data);
245
217
  else datasetsToWrite = [data];
246
218
  for (const writeData of datasetsToWrite) {
247
- if (format === "csv") {
219
+ //if it's a lookup table, it's always a CSV
220
+ if (format === "csv" || path.includes("-LOOKUP.csv")) {
248
221
  log(`writing ${path}`);
249
- const columns = getUniqueKeys(writeData);
222
+ const columns = u.getUniqueKeys(writeData);
250
223
  //papa parse needs nested JSON stringified
251
224
  writeData.forEach((e) => {
252
225
  for (const key in e) {
@@ -348,6 +321,7 @@ function makeProfile(props, defaults) {
348
321
  }
349
322
 
350
323
  function makeSCD(props, distinct_id, mutations, $created) {
324
+ if (JSON.stringify(props) === "{}") return [];
351
325
  const scdEntries = [];
352
326
  let lastInserted = dayjs($created);
353
327
  const deltaDays = dayjs().diff(lastInserted, "day");
@@ -356,12 +330,12 @@ function makeSCD(props, distinct_id, mutations, $created) {
356
330
  if (lastInserted.isAfter(dayjs())) break;
357
331
  const scd = makeProfile(props, { distinct_id });
358
332
  scd.startTime = lastInserted.toISOString();
359
- lastInserted = lastInserted.add(integer(1, 1000), "seconds");
333
+ lastInserted = lastInserted.add(u.integer(1, 1000), "seconds");
360
334
  scd.insertTime = lastInserted.toISOString();
361
335
  scdEntries.push({ ...scd });
362
336
  lastInserted = lastInserted
363
- .add(integer(0, deltaDays), "day")
364
- .subtract(integer(1, 1000), "seconds");
337
+ .add(u.integer(0, deltaDays), "day")
338
+ .subtract(u.integer(1, 1000), "seconds");
365
339
  }
366
340
 
367
341
  return scdEntries;
@@ -394,11 +368,11 @@ function makeEvent(distinct_id, anonymousIds, sessionIds, earliestTime, events,
394
368
 
395
369
  //event time
396
370
  if (isFirstEvent) event.time = dayjs.unix(earliestTime).toISOString();
397
- if (!isFirstEvent) event.time = AKsTimeSoup(earliestTime, NOW, PEAK_DAYS);
371
+ if (!isFirstEvent) event.time = AKsTimeSoup(earliestTime, NOW);
398
372
 
399
373
  // anonymous and session ids
400
- if (global?.config.anonIds) event.$device_id = chance.pickone(anonymousIds);
401
- if (global?.config.sessionIds) event.$session_id = chance.pickone(sessionIds);
374
+ if (global.MP_SIMULATION_CONFIG?.anonIds) event.$device_id = chance.pickone(anonymousIds);
375
+ if (global.MP_SIMULATION_CONFIG?.sessionIds) event.$session_id = chance.pickone(sessionIds);
402
376
 
403
377
  //sometimes have a $user_id
404
378
  if (!isFirstEvent && chance.bool({ likelihood: 42 })) event.$user_id = distinct_id;
@@ -411,7 +385,7 @@ function makeEvent(distinct_id, anonymousIds, sessionIds, earliestTime, events,
411
385
  //iterate through custom properties
412
386
  for (const key in props) {
413
387
  try {
414
- event[key] = choose(props[key]);
388
+ event[key] = u.choose(props[key]);
415
389
  } catch (e) {
416
390
  console.error(`error with ${key} in ${chosenEvent.event} event`, e);
417
391
  debugger;
@@ -423,7 +397,7 @@ function makeEvent(distinct_id, anonymousIds, sessionIds, earliestTime, events,
423
397
  const groupKey = groupPair[0];
424
398
  const groupCardinality = groupPair[1];
425
399
 
426
- event[groupKey] = pick(weightedRange(1, groupCardinality))
400
+ event[groupKey] = u.pick(u.weightedRange(1, groupCardinality));
427
401
  }
428
402
 
429
403
  //make $insert_id
@@ -441,9 +415,9 @@ function buildFileNames(config) {
441
415
  if (config.writeToDisk) writeDir = mkdir("./data");
442
416
 
443
417
  const writePaths = {
444
- eventFiles: [path.join(writeDir, `events-${simName}.${extension}`)],
445
- userFiles: [path.join(writeDir, `users-${simName}.${extension}`)],
446
- scdFiles: [path.join(writeDir, `scd-${simName}.${extension}`)],
418
+ eventFiles: [path.join(writeDir, `${simName}-EVENTS.${extension}`)],
419
+ userFiles: [path.join(writeDir, `${simName}-USERS.${extension}`)],
420
+ scdFiles: [path.join(writeDir, `${simName}-SCD.${extension}`)],
447
421
  groupFiles: [],
448
422
  lookupFiles: [],
449
423
  folder: writeDir,
@@ -452,90 +426,28 @@ function buildFileNames(config) {
452
426
  for (const groupPair of groupKeys) {
453
427
  const groupKey = groupPair[0];
454
428
  writePaths.groupFiles.push(
455
- path.join(writeDir, `group-${groupKey}-${simName}.${extension}`)
429
+ path.join(writeDir, `${simName}-${groupKey}-GROUP.${extension}`)
456
430
  );
457
431
  }
458
432
 
459
433
  for (const lookupTable of lookupTables) {
460
434
  const { key } = lookupTable;
461
435
  writePaths.lookupFiles.push(
462
- path.join(writeDir, `lookup-${key}-${simName}.${extension}`)
436
+ //lookups are always CSVs
437
+ path.join(writeDir, `${simName}-${key}-LOOKUP.csv`)
463
438
  );
464
439
  }
465
440
 
466
441
  return writePaths;
467
442
  }
468
443
 
469
- /**
470
- * essentially, a timestamp generator with a twist
471
- * @param {number} earliestTime - The earliest timestamp in Unix format.
472
- * @param {number} latestTime - The latest timestamp in Unix format.
473
- * @param {Array} peakDays - Array of Unix timestamps representing the start of peak days.
474
- * @returns {number} - The generated event timestamp in Unix format.
475
- */
476
- function AKsTimeSoup(earliestTime, latestTime = NOW, peakDays = PEAK_DAYS) {
477
- let chosenTime;
478
- let eventTime;
479
- let validTime = false;
480
-
481
- if (typeof earliestTime !== "number") {
482
- if (parseInt(earliestTime) > 0) earliestTime = parseInt(earliestTime);
483
- if (dayjs(earliestTime).isValid()) earliestTime = dayjs(earliestTime).unix();
484
- }
485
-
486
- while (!validTime) {
487
-
488
- // Define business hours
489
- const peakStartHour = 4; // 4 AM
490
- const peakEndHour = 23; // 11 PM
491
- const likelihoodOfPeakDay = chance.integer({ min: integer(5, 42), max: integer(43, 69) }); // Randomize likelihood with CHAOS!~~
492
-
493
- // Select a day, with a preference for peak days
494
- let selectedDay;
495
- if (chance.bool({ likelihood: likelihoodOfPeakDay })) { // Randomized likelihood to pick a peak day
496
- selectedDay = peakDays.length > 0 ? chance.pickone(peakDays) : integer(earliestTime, latestTime);
497
- } else {
498
- // Introduce minor peaks by allowing some events to still occur during business hours
499
- selectedDay = chance.bool({ likelihood: integer(1, 42) })
500
- ? chance.pickone(peakDays)
501
- : integer(earliestTime, latestTime);
502
- }
503
-
504
- // Normalize selectedDay to the start of the day
505
- selectedDay = dayjs.unix(selectedDay).startOf('day').unix();
506
-
507
- // Generate a random time within business hours with a higher concentration in the middle of the period
508
- const businessStart = dayjs.unix(selectedDay).hour(peakStartHour).minute(0).second(0).unix();
509
- const businessEnd = dayjs.unix(selectedDay).hour(peakEndHour).minute(0).second(0).unix();
510
-
511
- if (selectedDay === peakDays[0]) {
512
- // Use a skewed distribution for peak days
513
- eventTime = chance.normal({ mean: (businessEnd + businessStart) / integer(1, 4), dev: (businessEnd - businessStart) / integer(2, 8) });
514
- } else {
515
- // For non-peak days, use a uniform distribution to add noise
516
- eventTime = integer(integer(businessStart, businessEnd), integer(businessStart, businessEnd));
517
- }
518
-
519
- // usually, ensure the event time is within business hours
520
- if (chance.bool({ likelihood: 42 })) eventTime = Math.min(Math.max(eventTime, businessStart), businessEnd);
521
-
522
- if (eventTime > 0) validTime = true;
523
- const parsedTime = dayjs.unix(eventTime).toISOString();
524
- if (!parsedTime.startsWith('20')) validTime = false;
525
-
526
- }
527
- chosenTime = dayjs.unix(eventTime).toISOString();
528
- if (eventTime < 0) debugger;
529
- if (!chosenTime.startsWith('20')) debugger;
530
- return chosenTime;
531
- }
532
444
 
533
445
 
534
446
 
535
447
  // this is for CLI
536
448
  if (require.main === module) {
537
449
  const args = cliParams();
538
- const { token, seed, format, numDays, numUsers, numEvents, region, writeToDisk } = args;
450
+ const { token, seed, format, numDays, numUsers, numEvents, region, writeToDisk, complex = false } = args;
539
451
  const suppliedConfig = args._[0];
540
452
 
541
453
  //if the user specifics an separate config file
@@ -543,9 +455,18 @@ if (require.main === module) {
543
455
  if (suppliedConfig) {
544
456
  log(`using ${suppliedConfig} for data\n`);
545
457
  config = require(path.resolve(suppliedConfig));
546
- } else {
547
- log(`... using default configuration ...\n`);
548
- config = require("./default.js");
458
+ }
459
+ else {
460
+ if (complex) {
461
+ log(`... using default COMPLEX configuration [everything] ...\n`);
462
+ log(`... for more simple data, don't use the --complex flag ...\n`);
463
+ config = require("./examples/complex.js");
464
+ }
465
+ else {
466
+ log(`... using default SIMPLE configuration [events + users] ...\n`);
467
+ log(`... for more complex data, use the --complex flag ...\n`);
468
+ config = require("./examples/simple.js");
469
+ }
549
470
  }
550
471
 
551
472
  //override config with cli params
@@ -592,9 +513,10 @@ if (require.main === module) {
592
513
  })
593
514
  .finally(() => {
594
515
  log("have a wonderful day :)");
595
- openFinder(path.resolve("./data"));
516
+ u.openFinder(path.resolve("./data"));
596
517
  });
597
518
  } else {
519
+ main.utils = { ...u };
598
520
  main.timeSoup = AKsTimeSoup;
599
521
  module.exports = main;
600
522
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "make-mp-data",
3
- "version": "1.1.19",
3
+ "version": "1.2.0",
4
4
  "description": "builds all mixpanel primitives for a given project",
5
5
  "main": "index.js",
6
6
  "types": "types.d.ts",
@@ -9,7 +9,7 @@
9
9
  "prune": "rm ./data/*",
10
10
  "go": "sh ./scripts/go.sh",
11
11
  "post": "npm publish",
12
- "test": "jest",
12
+ "test": "jest --runInBand",
13
13
  "deps": "sh ./scripts/deps.sh"
14
14
  },
15
15
  "repository": {
package/tests/e2e.test.js CHANGED
@@ -9,65 +9,93 @@ const { execSync } = require("child_process");
9
9
  const u = require('ak-tools');
10
10
 
11
11
  const simple = require('../examples/simple');
12
+ const complex = require('../examples/complex');
13
+ const deep = require('../examples/deepNest');
12
14
 
13
15
  const timeout = 60000;
14
16
  const testToken = process.env.TEST_TOKEN;
15
17
 
18
+ describe('module', () => {
16
19
 
17
- describe('e2e', () => {
18
-
19
- test('works as module', async () => {
20
+ test('works as module (no config)', async () => {
20
21
  console.log('MODULE TEST');
21
- const results = await generate({ verbose: false, writeToDisk: false, numEvents: 1100, numUsers: 100, seed: "deal with it" });
22
+ const results = await generate({ verbose: true, writeToDisk: false, numEvents: 1100, numUsers: 100, seed: "deal with it" });
22
23
  const { eventData, groupProfilesData, lookupTableData, scdTableData, userProfilesData } = results;
23
24
  expect(eventData.length).toBeGreaterThan(980);
24
25
  expect(groupProfilesData.length).toBe(0);
25
26
  expect(lookupTableData.length).toBe(0);
26
27
  expect(scdTableData.length).toBeGreaterThan(200);
28
+ expect(userProfilesData.length).toBe(100);
29
+
30
+ }, timeout);
31
+
32
+ test('works as module (simple)', async () => {
33
+ console.log('MODULE TEST: SIMPLE');
34
+ const results = await generate({ ...simple, verbose: true, writeToDisk: false, numEvents: 1100, numUsers: 100, seed: "deal with it" });
35
+ const { eventData, groupProfilesData, lookupTableData, scdTableData, userProfilesData } = results;
36
+ expect(eventData.length).toBeGreaterThan(980);
37
+ expect(groupProfilesData.length).toBe(0);
38
+ expect(lookupTableData.length).toBe(0);
39
+ expect(scdTableData.length).toBe(0);
27
40
  expect(userProfilesData.length).toBe(100);
28
41
 
29
42
  }, timeout);
30
43
 
31
- test('works as CLI', async () => {
32
- console.log('CLI TEST');
33
- const run = execSync(`node ./index.js --numEvents 1000 --numUsers 100 --seed "deal with it" --verbose false`);
34
- expect(run.toString().trim().includes('have a wonderful day :)')).toBe(true);
35
- const csvs = (await u.ls('./data')).filter(a => a.includes('.csv'));
36
- expect(csvs.length).toBe(5);
44
+ test('works as module (complex)', async () => {
45
+ console.log('MODULE TEST: COMPLEX');
46
+ const results = await generate({ ...complex, verbose: true, writeToDisk: false, numEvents: 1100, numUsers: 100, seed: "deal with it" });
47
+ const { eventData, groupProfilesData, lookupTableData, scdTableData, userProfilesData } = results;
48
+ expect(eventData.length).toBeGreaterThan(980);
49
+ expect(groupProfilesData[0]?.data?.length).toBe(350);
50
+ expect(lookupTableData.length).toBe(1);
51
+ expect(lookupTableData[0].data.length).toBe(1000);
52
+ expect(scdTableData.length).toBeGreaterThan(200);
53
+ expect(userProfilesData.length).toBe(100);
54
+
37
55
  }, timeout);
38
56
 
39
- test('sends data to mixpanel', async () => {
40
- console.log('NETWORK TEST');
41
- const results = await generate({verbose: false, writeToDisk: false, numEvents: 1100, numUsers: 100, seed: "deal with it", token: testToken });
42
- const { events, users, groups } = results.import;
43
- expect(events.success).toBeGreaterThan(980);
44
- expect(users.success).toBe(100);
45
- expect(groups.length).toBe(0);
57
+ test('works as module (deep nest)', async () => {
58
+ console.log('MODULE TEST: DEEP NEST');
59
+ const results = await generate({ ...deep, verbose: true, writeToDisk: false, numEvents: 1100, numUsers: 100, seed: "deal with it" });
60
+ const { eventData, groupProfilesData, lookupTableData, scdTableData, userProfilesData } = results;
61
+ expect(eventData.length).toBeGreaterThan(980);
62
+ expect(groupProfilesData.length).toBe(0);
63
+ expect(lookupTableData.length).toBe(0);
64
+ expect(scdTableData.length).toBeGreaterThan(200);
65
+ expect(userProfilesData.length).toBe(100);
66
+
46
67
  }, timeout);
47
68
 
48
- test('every record is valid', async () => {
49
- console.log('VALIDATION TEST');
50
- const results = await generate({verbose: false, writeToDisk: false, numEvents: 10000, numUsers: 500 });
51
- const { eventData, userProfilesData } = results;
52
- const areEventsValid = eventData.every(validateEvent);
53
- const areUsersValid = userProfilesData.every(validateUser);
54
69
 
55
- const invalidEvents = eventData.filter(e => !validateEvent(e));
56
- const invalidUsers = userProfilesData.filter(u => !validateUser(u));
70
+ });
57
71
 
58
- expect(areEventsValid).toBe(true);
59
- expect(areUsersValid).toBe(true);
72
+ describe('cli', () => {
73
+ test('works as CLI (complex)', async () => {
74
+ console.log('COMPLEX CLI TEST');
75
+ const run = execSync(`node ./index.js --numEvents 1000 --numUsers 100 --seed "deal with it" --complex`);
76
+ expect(run.toString().trim().includes('have a wonderful day :)')).toBe(true);
77
+ const csvs = (await u.ls('./data')).filter(a => a.includes('.csv'));
78
+ expect(csvs.length).toBe(5);
79
+ clearData();
60
80
  }, timeout);
61
81
 
62
- test('every date is valid', async () => {
63
- console.log('DATE TEST');
64
- const results = await generate({ ...simple, writeToDisk: false, verbose: false });
65
- const { eventData } = results;
66
- const invalidDates = eventData.filter(e => !validateTime(e.time));
67
- expect(eventData.every(e => validateTime(e.time))).toBe(true);
82
+ test('works as CLI (simple)', async () => {
83
+ console.log('simple CLI TEST');
84
+ const run = execSync(`node ./index.js --numEvents 1000 --numUsers 100 --seed "deal with it"`);
85
+ expect(run.toString().trim().includes('have a wonderful day :)')).toBe(true);
86
+ const csvs = (await u.ls('./data')).filter(a => a.includes('.csv'));
87
+ expect(csvs.length).toBe(2);
88
+ clearData();
89
+ }, timeout);
68
90
 
69
-
70
- }, timeout)
91
+ test('works as CLI (custom)', async () => {
92
+ console.log('custom CLI TEST');
93
+ const run = execSync(`node ./index.js ./examples/deepNest.js`);
94
+ expect(run.toString().trim().includes('have a wonderful day :)')).toBe(true);
95
+ const csvs = (await u.ls('./data')).filter(a => a.includes('.csv'));
96
+ expect(csvs.length).toBe(3);
97
+ clearData();
98
+ }, timeout);
71
99
 
72
100
  });
73
101
 
@@ -102,16 +130,47 @@ describe('options + tweaks', () => {
102
130
  expect(anonIds.length).toBe(0);
103
131
  }, timeout);
104
132
 
133
+ test('sends data to mixpanel', async () => {
134
+ console.log('NETWORK TEST');
135
+ const results = await generate({ verbose: true, writeToDisk: false, numEvents: 1100, numUsers: 100, seed: "deal with it", token: testToken });
136
+ const { events, users, groups } = results.import;
137
+ expect(events.success).toBeGreaterThan(980);
138
+ expect(users.success).toBe(100);
139
+ expect(groups.length).toBe(0);
140
+ }, timeout);
105
141
 
106
- });
142
+ test('every record is valid', async () => {
143
+ console.log('VALIDATION TEST');
144
+ const results = await generate({ verbose: true, writeToDisk: false, numEvents: 10000, numUsers: 500 });
145
+ const { eventData, userProfilesData } = results;
146
+ const areEventsValid = eventData.every(validateEvent);
147
+ const areUsersValid = userProfilesData.every(validateUser);
107
148
 
149
+ const invalidEvents = eventData.filter(e => !validateEvent(e));
150
+ const invalidUsers = userProfilesData.filter(u => !validateUser(u));
108
151
 
152
+ expect(areEventsValid).toBe(true);
153
+ expect(areUsersValid).toBe(true);
154
+ }, timeout);
109
155
 
110
- afterEach(() => {
156
+ test('every date is valid', async () => {
157
+ console.log('DATE TEST');
158
+ const results = await generate({ ...simple, writeToDisk: false, verbose: true });
159
+ const { eventData } = results;
160
+ const invalidDates = eventData.filter(e => !validateTime(e.time));
161
+ expect(eventData.every(e => validateTime(e.time))).toBe(true);
162
+
163
+ }, timeout);
111
164
 
112
165
  });
113
166
 
114
167
  afterAll(() => {
168
+ clearData();
169
+ });
170
+
171
+ //helpers
172
+
173
+ function clearData() {
115
174
  try {
116
175
  console.log('clearing...');
117
176
  execSync(`npm run prune`);
@@ -120,9 +179,7 @@ afterAll(() => {
120
179
  catch (err) {
121
180
  console.log('error clearing files');
122
181
  }
123
- });
124
-
125
- //helpers
182
+ }
126
183
 
127
184
  function validateEvent(event) {
128
185
  if (!event.event) return false;
@@ -7,6 +7,7 @@ const { timeSoup } = generate;
7
7
  require('dotenv').config();
8
8
 
9
9
 
10
+
10
11
  describe('timeSoup', () => {
11
12
  test('always positive dates', () => {
12
13
  const dates = [];
@@ -21,3 +22,157 @@ describe('timeSoup', () => {
21
22
 
22
23
  });
23
24
  });
25
+
26
+
27
+
28
+ const { applySkew, boxMullerRandom, choose, date, dates, day, exhaust, generateEmoji, getUniqueKeys, integer, makeHashTags, makeProducts, mapToRange, person, pick, range, weighList, weightedRange } = require('../utils');
29
+
30
+ describe('utils', () => {
31
+
32
+ test('pick: works', () => {
33
+ const array = [1, 2, 3];
34
+ const item = pick(array);
35
+ expect(array).toContain(item);
36
+ });
37
+
38
+ test('pick: null', () => {
39
+ expect(pick(123)).toBe(123);
40
+ });
41
+
42
+
43
+
44
+ test('integer: diff', () => {
45
+ const min = 5;
46
+ const max = 10;
47
+ const result = integer(min, max);
48
+ expect(result).toBeGreaterThanOrEqual(min);
49
+ expect(result).toBeLessThanOrEqual(max);
50
+ });
51
+
52
+ test('integer: same', () => {
53
+ expect(integer(7, 7)).toBe(7);
54
+ });
55
+
56
+
57
+
58
+ test('hashtags', () => {
59
+ const hashtags = makeHashTags();
60
+ expect(hashtags).toBeInstanceOf(Array);
61
+ expect(hashtags).not.toHaveLength(0);
62
+ hashtags.forEach(tag => {
63
+ expect(tag).toMatch(/^#/);
64
+ });
65
+ });
66
+
67
+
68
+
69
+ test('person: fields', () => {
70
+ const generatedPerson = person();
71
+ expect(generatedPerson).toHaveProperty('$name');
72
+ expect(generatedPerson).toHaveProperty('$email');
73
+ expect(generatedPerson).toHaveProperty('$avatar');
74
+ expect(generatedPerson).toHaveProperty('anonymousIds');
75
+ expect(generatedPerson.anonymousIds).toBeInstanceOf(Array);
76
+ });
77
+
78
+
79
+ test('date: past date', () => {
80
+ const pastDate = date(10, true, 'YYYY-MM-DD')();
81
+ expect(dayjs(pastDate, 'YYYY-MM-DD').isValid()).toBeTruthy();
82
+ expect(dayjs(pastDate).isBefore(dayjs())).toBeTruthy();
83
+ });
84
+
85
+ test('date: future date', () => {
86
+ const futureDate = date(10, false, 'YYYY-MM-DD')();
87
+ expect(dayjs(futureDate, 'YYYY-MM-DD').isValid()).toBeTruthy();
88
+ expect(dayjs(futureDate).isAfter(dayjs())).toBeTruthy();
89
+ });
90
+
91
+ test('dates: returns pairs of dates', () => {
92
+ const datePairs = dates(10, 3, 'YYYY-MM-DD');
93
+ expect(datePairs).toBeInstanceOf(Array);
94
+ expect(datePairs).toHaveLength(3);
95
+ datePairs.forEach(pair => {
96
+ expect(pair).toHaveLength(2);
97
+ });
98
+ });
99
+
100
+ test('choose: choose from array', () => {
101
+ const options = ['apple', 'banana', 'cherry'];
102
+ const choice = choose(options);
103
+ expect(options).toContain(choice);
104
+ });
105
+
106
+ test('choose: execute function', () => {
107
+ const result = choose(() => 'test');
108
+ expect(result).toBe('test');
109
+ });
110
+
111
+ test('exhaust: exhaust array elements', () => {
112
+ const arr = [1, 2, 3];
113
+ const exhaustFn = exhaust([...arr]);
114
+ expect(exhaustFn()).toBe(1);
115
+ expect(exhaustFn()).toBe(2);
116
+ expect(exhaustFn()).toBe(3);
117
+ expect(exhaustFn()).toBeUndefined();
118
+ });
119
+
120
+ test('generateEmoji: returns string of emojis', () => {
121
+ const emojis = generateEmoji(5)();
122
+ expect(typeof emojis).toBe('string');
123
+ expect(emojis.split(', ').length).toBeLessThanOrEqual(5);
124
+ });
125
+
126
+ test('getUniqueKeys: find unique keys', () => {
127
+ const objects = [{ a: 1, b: 2 }, { a: 3, c: 4 }, { a: 5, b: 6 }];
128
+ const uniqueKeys = getUniqueKeys(objects);
129
+ expect(uniqueKeys).toEqual(expect.arrayContaining(['a', 'b', 'c']));
130
+ });
131
+
132
+
133
+ test('date: generates a valid date', () => {
134
+ const result = date();
135
+ expect(dayjs(result()).isValid()).toBe(true);
136
+ });
137
+
138
+ test('dates: generates an array of date pairs', () => {
139
+ const result = dates();
140
+ expect(result).toBeInstanceOf(Array);
141
+ expect(result.length).toBe(5); // Assuming default numPairs is 5
142
+ result.forEach(pair => {
143
+ expect(pair).toBeInstanceOf(Array);
144
+ expect(pair.length).toBe(2);
145
+ expect(dayjs(pair[0]()).isValid()).toBe(true);
146
+ expect(dayjs(pair[1]()).isValid()).toBe(true);
147
+ });
148
+ });
149
+
150
+ test('day: generates a day within range', () => {
151
+ const start = '2020-01-01';
152
+ const end = '2020-01-30';
153
+ const result = day(start, end);
154
+ const dayResult = result(0, 9);
155
+ expect(dayjs(dayResult.day).isAfter(dayjs(dayResult.start))).toBe(true);
156
+ expect(dayjs(dayResult.day).isBefore(dayjs(dayResult.end))).toBe(true);
157
+ });
158
+
159
+ test('exhaust: sequentially removes items from array', () => {
160
+ const arr = [1, 2, 3];
161
+ const next = exhaust(arr);
162
+ expect(next()).toBe(1);
163
+ expect(next()).toBe(2);
164
+ expect(next()).toBe(3);
165
+ expect(next()).toBe(undefined); // or whatever your implementation does after array is exhausted
166
+ });
167
+
168
+ test('generateEmoji: generates correct format and length', () => {
169
+ const result = generateEmoji();
170
+ const emojis = result();
171
+ expect(typeof emojis).toBe('string');
172
+ const emojiArray = emojis.split(', ');
173
+ expect(emojiArray.length).toBeLessThanOrEqual(10); // Assuming max default is 10
174
+
175
+ });
176
+
177
+
178
+ });
package/timesoup.js ADDED
@@ -0,0 +1,92 @@
1
+ const Chance = require("chance");
2
+ const chance = new Chance();
3
+ const dayjs = require("dayjs");
4
+ const utc = require("dayjs/plugin/utc");
5
+ dayjs.extend(utc);
6
+ const { integer } = require('./utils');
7
+ const NOW = dayjs().unix();
8
+
9
+ const PEAK_DAYS = [
10
+ dayjs().subtract(2, "day").unix(),
11
+ dayjs().subtract(3, "day").unix(),
12
+ dayjs().subtract(5, "day").unix(),
13
+ dayjs().subtract(7, "day").unix(),
14
+ dayjs().subtract(11, "day").unix(),
15
+ dayjs().subtract(13, "day").unix(),
16
+ dayjs().subtract(17, "day").unix(),
17
+ dayjs().subtract(19, "day").unix(),
18
+ dayjs().subtract(23, "day").unix(),
19
+ dayjs().subtract(29, "day").unix(),
20
+ ];
21
+
22
+
23
+ /**
24
+ * essentially, a timestamp generator with a twist
25
+ * @param {number} earliestTime - The earliest timestamp in Unix format.
26
+ * @param {number} latestTime - The latest timestamp in Unix format.
27
+ * @param {Array} peakDays - Array of Unix timestamps representing the start of peak days.
28
+ * @returns {number} - The generated event timestamp in Unix format.
29
+ */
30
+ function AKsTimeSoup(earliestTime, latestTime = NOW, peakDays = PEAK_DAYS) {
31
+ let chosenTime;
32
+ let eventTime;
33
+ let validTime = false;
34
+
35
+ if (typeof earliestTime !== "number") {
36
+ if (parseInt(earliestTime) > 0) earliestTime = parseInt(earliestTime);
37
+ if (dayjs(earliestTime).isValid()) earliestTime = dayjs(earliestTime).unix();
38
+ }
39
+
40
+ while (!validTime) {
41
+
42
+ // Define business hours
43
+ const peakStartHour = 4; // 4 AM
44
+ const peakEndHour = 23; // 11 PM
45
+ const likelihoodOfPeakDay = chance.integer({ min: integer(5, 42), max: integer(43, 69) }); // Randomize likelihood with CHAOS!~~
46
+
47
+ // Select a day, with a preference for peak days
48
+ let selectedDay;
49
+ if (chance.bool({ likelihood: likelihoodOfPeakDay })) { // Randomized likelihood to pick a peak day
50
+ selectedDay = peakDays.length > 0 ? chance.pickone(peakDays) : integer(earliestTime, latestTime);
51
+ } else {
52
+ // Introduce minor peaks by allowing some events to still occur during business hours
53
+ selectedDay = chance.bool({ likelihood: integer(1, 42) })
54
+ ? chance.pickone(peakDays)
55
+ : integer(earliestTime, latestTime);
56
+ }
57
+
58
+ // Normalize selectedDay to the start of the day
59
+ selectedDay = dayjs.unix(selectedDay).startOf('day').unix();
60
+
61
+ // Generate a random time within business hours with a higher concentration in the middle of the period
62
+ const businessStart = dayjs.unix(selectedDay).hour(peakStartHour).minute(0).second(0).unix();
63
+ const businessEnd = dayjs.unix(selectedDay).hour(peakEndHour).minute(0).second(0).unix();
64
+
65
+ if (selectedDay === peakDays[0]) {
66
+ // Use a skewed distribution for peak days
67
+ eventTime = chance.normal({ mean: (businessEnd + businessStart) / integer(1, 4), dev: (businessEnd - businessStart) / integer(2, 8) });
68
+ } else {
69
+ // For non-peak days, use a uniform distribution to add noise
70
+ eventTime = integer(integer(businessStart, businessEnd), integer(businessStart, businessEnd));
71
+ }
72
+
73
+ // usually, ensure the event time is within business hours
74
+ if (chance.bool({ likelihood: 42 })) eventTime = Math.min(Math.max(eventTime, businessStart), businessEnd);
75
+
76
+ if (eventTime > 0) validTime = true;
77
+ const parsedTime = dayjs.unix(eventTime).toISOString();
78
+ if (!parsedTime.startsWith('20')) validTime = false;
79
+
80
+ }
81
+ chosenTime = dayjs.unix(eventTime).toISOString();
82
+
83
+ //should never get here
84
+ if (eventTime < 0) debugger;
85
+ if (!chosenTime.startsWith('20')) debugger;
86
+
87
+
88
+ return chosenTime;
89
+ }
90
+
91
+
92
+ module.exports = AKsTimeSoup;
package/utils.js CHANGED
@@ -9,16 +9,20 @@ dayjs.extend(utc);
9
9
 
10
10
  function pick(items) {
11
11
  if (!Array.isArray(items)) {
12
- try {
13
- const choice = chance.pickone(this);
14
- return choice;
15
- }
16
- catch (e) {
17
- return null;
12
+ if (typeof items === 'function') {
13
+ const selection = items();
14
+ if (Array.isArray(selection)) {
15
+ return chance.pickone(selection);
16
+ }
17
+ else {
18
+ return selection;
19
+ }
18
20
  }
21
+ return items;
22
+
19
23
  }
20
24
  return chance.pickone(items);
21
- }
25
+ };
22
26
 
23
27
  function date(inTheLast = 30, isPast = true, format = 'YYYY-MM-DD') {
24
28
  const now = dayjs.utc();
@@ -48,7 +52,7 @@ function date(inTheLast = 30, isPast = true, format = 'YYYY-MM-DD') {
48
52
  if (!format) return now?.toISOString();
49
53
  }
50
54
  };
51
- }
55
+ };
52
56
 
53
57
  function dates(inTheLast = 30, numPairs = 5, format = 'YYYY-MM-DD') {
54
58
  const pairs = [];
@@ -57,7 +61,7 @@ function dates(inTheLast = 30, numPairs = 5, format = 'YYYY-MM-DD') {
57
61
  }
58
62
  return pairs;
59
63
 
60
- }
64
+ };
61
65
 
62
66
  function day(start, end) {
63
67
  const format = 'YYYY-MM-DD';
@@ -74,7 +78,7 @@ function day(start, end) {
74
78
  };
75
79
  };
76
80
 
77
- }
81
+ };
78
82
 
79
83
  function choose(value) {
80
84
  if (typeof value === 'function') {
@@ -85,13 +89,13 @@ function choose(value) {
85
89
  }
86
90
 
87
91
  return value;
88
- }
92
+ };
89
93
 
90
94
  function exhaust(arr) {
91
95
  return function () {
92
96
  return arr.shift();
93
97
  };
94
- }
98
+ };
95
99
 
96
100
 
97
101
  function integer(min, max) {
@@ -114,7 +118,7 @@ function integer(min, max) {
114
118
  }
115
119
 
116
120
  return 0;
117
- }
121
+ };
118
122
 
119
123
  function makeHashTags() {
120
124
  const popularHashtags = [
@@ -138,7 +142,7 @@ function makeHashTags() {
138
142
  hashtags.push(chance.pickone(popularHashtags));
139
143
  }
140
144
  return hashtags;
141
- }
145
+ };
142
146
 
143
147
  function makeProducts() {
144
148
  let categories = ["Device Accessories", "eBooks", "Automotive", "Baby Products", "Beauty", "Books", "Camera & Photo", "Cell Phones & Accessories", "Collectible Coins", "Consumer Electronics", "Entertainment Collectibles", "Fine Art", "Grocery & Gourmet Food", "Health & Personal Care", "Home & Garden", "Independent Design", "Industrial & Scientific", "Accessories", "Major Appliances", "Music", "Musical Instruments", "Office Products", "Outdoors", "Personal Computers", "Pet Supplies", "Software", "Sports", "Sports Collectibles", "Tools & Home Improvement", "Toys & Games", "Video, DVD & Blu-ray", "Video Games", "Watches"];
@@ -172,7 +176,7 @@ function makeProducts() {
172
176
  }
173
177
 
174
178
  return data;
175
- }
179
+ };
176
180
 
177
181
  // Box-Muller transform to generate standard normally distributed values
178
182
  function boxMullerRandom() {
@@ -180,7 +184,7 @@ function boxMullerRandom() {
180
184
  while (u === 0) u = Math.random();
181
185
  while (v === 0) v = Math.random();
182
186
  return Math.sqrt(-2.0 * Math.log(u)) * Math.cos(2.0 * Math.PI * v);
183
- }
187
+ };
184
188
 
185
189
  // Apply skewness to the value
186
190
  function applySkew(value, skew) {
@@ -188,12 +192,12 @@ function applySkew(value, skew) {
188
192
  // Adjust the value based on skew
189
193
  let sign = value < 0 ? -1 : 1;
190
194
  return sign * Math.pow(Math.abs(value), skew);
191
- }
195
+ };
192
196
 
193
197
  // Map standard normal value to our range
194
198
  function mapToRange(value, mean, sd) {
195
199
  return Math.round(value * sd + mean);
196
- }
200
+ };
197
201
 
198
202
  function weightedRange(min, max, size = 100, skew = 1) {
199
203
  const mean = (max + min) / 2;
@@ -214,14 +218,12 @@ function weightedRange(min, max, size = 100, skew = 1) {
214
218
  }
215
219
 
216
220
  return array;
217
- }
221
+ };
218
222
 
219
223
  function progress(thing, p) {
220
224
  readline.cursorTo(process.stdout, 0);
221
225
  process.stdout.write(`${thing} processed ... ${comma(p)}`);
222
- }
223
-
224
-
226
+ };
225
227
 
226
228
  function range(a, b, step = 1) {
227
229
  step = !step ? 1 : step;
@@ -241,7 +243,7 @@ function openFinder(path, callback) {
241
243
  p.kill();
242
244
  return callback(err);
243
245
  });
244
- }
246
+ };
245
247
 
246
248
  function getUniqueKeys(data) {
247
249
  const keysSet = new Set();
@@ -249,7 +251,7 @@ function getUniqueKeys(data) {
249
251
  Object.keys(item).forEach(key => keysSet.add(key));
250
252
  });
251
253
  return Array.from(keysSet);
252
- }
254
+ };
253
255
 
254
256
  //makes a random-sized array of emojis
255
257
  function generateEmoji(max = 10, array = false) {
@@ -264,74 +266,8 @@ function generateEmoji(max = 10, array = false) {
264
266
  if (!array) return arr.join(', ');
265
267
  return "🤷";
266
268
  };
267
- }
268
-
269
- function generateName() {
270
- var adjs = [
271
- "autumn", "hidden", "bitter", "misty", "silent", "empty", "dry", "dark",
272
- "summer", "icy", "delicate", "quiet", "white", "cool", "spring", "winter",
273
- "patient", "twilight", "dawn", "crimson", "wispy", "weathered", "blue",
274
- "billowing", "broken", "cold", "damp", "falling", "frosty", "green",
275
- "long", "late", "lingering", "bold", "little", "morning", "muddy", "old",
276
- "red", "rough", "still", "small", "sparkling", "throbbing", "shy",
277
- "wandering", "withered", "wild", "black", "young", "holy", "solitary",
278
- "fragrant", "aged", "snowy", "proud", "floral", "restless", "divine",
279
- "polished", "ancient", "purple", "lively", "nameless", "gentle", "gleaming", "furious", "luminous", "obscure", "poised", "shimmering", "swirling",
280
- "sombre", "steamy", "whispering", "jagged", "melodic", "moonlit", "starry", "forgotten",
281
- "peaceful", "restive", "rustling", "sacred", "ancient", "haunting", "solitary", "mysterious",
282
- "silver", "dusky", "earthy", "golden", "hallowed", "misty", "roaring", "serene", "vibrant",
283
- "stalwart", "whimsical", "timid", "tranquil", "vast", "youthful", "zephyr", "raging",
284
- "sapphire", "turbulent", "whirling", "sleepy", "ethereal", "tender", "unseen", "wistful"
285
- ];
286
-
287
- var nouns = [
288
- "waterfall", "river", "breeze", "moon", "rain", "wind", "sea", "morning",
289
- "snow", "lake", "sunset", "pine", "shadow", "leaf", "dawn", "glitter",
290
- "forest", "hill", "cloud", "meadow", "sun", "glade", "bird", "brook",
291
- "butterfly", "bush", "dew", "dust", "field", "fire", "flower", "firefly",
292
- "feather", "grass", "haze", "mountain", "night", "pond", "darkness",
293
- "snowflake", "silence", "sound", "sky", "shape", "surf", "thunder",
294
- "violet", "water", "wildflower", "wave", "water", "resonance", "sun",
295
- "wood", "dream", "cherry", "tree", "fog", "frost", "voice", "paper",
296
- "frog", "smoke", "star", "glow", "wave", "riverbed", "cliff", "deluge", "prairie", "creek", "ocean",
297
- "peak", "valley", "starlight", "quartz", "woodland", "marsh", "earth", "canopy",
298
- "petal", "stone", "orb", "gale", "bay", "canyon", "watercourse", "vista", "raindrop",
299
- "boulder", "grove", "plateau", "sand", "mist", "tide", "blossom", "leaf", "flame",
300
- "shade", "coil", "grotto", "pinnacle", "scallop", "serenity", "abyss", "skyline",
301
- "drift", "echo", "nebula", "horizon", "crest", "wreath", "twilight", "balm", "glimmer"
302
- ];
303
-
304
- var verbs = [
305
- "dancing", "whispering", "flowing", "shimmering", "swirling", "echoing", "sparkling", "glistening",
306
- "cascading", "drifting", "glowing", "rippling", "quivering", "singing", "twinkling", "radiating",
307
- "enveloping", "enchanting", "captivating", "embracing", "embracing", "illuminating", "pulsating", "gliding",
308
- "soaring", "wandering", "meandering", "dazzling", "cuddling", "embracing", "caressing", "twisting",
309
- "twirling", "tumbling", "surging", "glimmering", "gushing", "splashing", "rolling", "splintering",
310
- "splintering", "crescendoing", "whirling", "bursting", "shining", "gushing", "emerging", "revealing",
311
- "emerging", "unfolding", "unveiling", "emerging", "surrounding", "unveiling", "materializing", "revealing"
312
- ];
313
-
314
- var adverbs = [
315
- "gracefully", "softly", "smoothly", "gently", "tenderly", "quietly", "serenely", "peacefully",
316
- "delicately", "effortlessly", "subtly", "tranquilly", "majestically", "silently", "calmly", "harmoniously",
317
- "elegantly", "luminously", "ethereally", "mysteriously", "sublimely", "radiantly", "dreamily", "ethereally",
318
- "mesmerizingly", "hypnotically", "mystically", "enigmatically", "spellbindingly", "enchantingly", "fascinatingly",
319
- "bewitchingly", "captivatingly", "entrancingly", "alluringly", "rapturously", "seductively", "charismatically",
320
- "seductively", "envelopingly", "ensnaringly", "entrancingly", "intoxicatingly", "irresistibly", "transcendentally",
321
- "envelopingly", "rapturously", "intimately", "intensely", "tangibly", "vividly", "intensely", "deeply"
322
- ];
323
-
324
-
325
- // ? http://stackoverflow.com/a/17516862/103058
326
- var adj = adjs[Math.floor(Math.random() * adjs.length)];
327
- var noun = nouns[Math.floor(Math.random() * nouns.length)];
328
- var verb = verbs[Math.floor(Math.random() * verbs.length)];
329
- var adverb = adverbs[Math.floor(Math.random() * adverbs.length)];
330
-
331
-
332
- return adj + '-' + noun + '-' + verb + '-' + adverb;
269
+ };
333
270
 
334
- }
335
271
 
336
272
  function person(bornDaysAgo = 30) {
337
273
  //names and photos
@@ -373,25 +309,55 @@ function person(bornDaysAgo = 30) {
373
309
  anonymousIds,
374
310
  sessionIds
375
311
  };
312
+ };
313
+
314
+
315
+ function weighList(items, mostChosenIndex) {
316
+ if (mostChosenIndex > items.length) mostChosenIndex = items.length;
317
+ return function () {
318
+ const weighted = [];
319
+ for (let i = 0; i < 10; i++) {
320
+ if (chance.bool({ likelihood: integer(10, 35) })) {
321
+ if (chance.bool({ likelihood: 50 })) {
322
+ weighted.push(items[mostChosenIndex]);
323
+ }
324
+ else {
325
+ const rand = chance.d10();
326
+ const addOrSubtract = chance.bool({ likelihood: 50 }) ? -rand : rand;
327
+ let newIndex = mostChosenIndex + addOrSubtract;
328
+ if (newIndex < 0) newIndex = 0;
329
+ if (newIndex > items.length) newIndex = items.length;
330
+ weighted.push(items[newIndex]);
331
+ }
332
+ }
333
+ else {
334
+ weighted.push(chance.pickone(items));
335
+ }
336
+ }
337
+ return weighted;
338
+
339
+ };
376
340
  }
377
341
 
378
342
  module.exports = {
379
- weightedRange,
380
343
  pick,
344
+ date,
345
+ dates,
381
346
  day,
347
+ choose,
348
+ exhaust,
382
349
  integer,
350
+ makeHashTags,
383
351
  makeProducts,
384
- date,
352
+ boxMullerRandom,
353
+ applySkew,
354
+ mapToRange,
355
+ weightedRange,
385
356
  progress,
386
- choose,
387
357
  range,
388
- exhaust,
389
358
  openFinder,
390
- applySkew,
391
- boxMullerRandom,
392
- generateEmoji,
393
359
  getUniqueKeys,
394
- makeHashTags,
395
- generateName,
396
- person
360
+ generateEmoji,
361
+ person,
362
+ weighList
397
363
  };
package/default.js DELETED
@@ -1,177 +0,0 @@
1
- /**
2
- * This is the default configuration file for the data generator
3
- * notice how the config object is structured, and see it's type definition in ./types.d.ts
4
- * feel free to modify this file to customize the data you generate
5
- * see helper functions in utils.js for more ways to generate data
6
- */
7
-
8
-
9
- const Chance = require('chance');
10
- const chance = new Chance();
11
- const { weightedRange, makeProducts, date, generateEmoji, makeHashTags } = require('./utils.js');
12
-
13
- /** @type {import('./types.d.ts').Config} */
14
- const config = {
15
- token: "",
16
- seed: "foo bar baz",
17
- numDays: 30, //how many days worth of data
18
- numEvents: 100000, //how many events
19
- numUsers: 1000, //how many users
20
- format: 'csv', //csv or json
21
- region: "US",
22
- anonIds: true, //if true, anonymousIds are created for each user
23
- sessionIds: true, //if true, sessionIds are created for each user
24
-
25
- events: [
26
- {
27
- "event": "checkout",
28
- "weight": 2,
29
- "properties": {
30
- amount: weightedRange(5, 500, 1000, .25),
31
- currency: ["USD", "CAD", "EUR", "BTC", "ETH", "JPY"],
32
- cart: makeProducts,
33
- }
34
- },
35
- {
36
- "event": "add to cart",
37
- "weight": 4,
38
- "properties": {
39
- isFeaturedItem: [true, false, false],
40
- amount: weightedRange(5, 500, 1000, .25),
41
- rating: weightedRange(1, 5),
42
- reviews: weightedRange(0, 35),
43
- product_id: weightedRange(1, 1000)
44
- }
45
- },
46
- {
47
- "event": "page view",
48
- "weight": 10,
49
- "properties": {
50
- page: ["/", "/", "/help", "/account", "/watch", "/listen", "/product", "/people", "/peace"],
51
- utm_source: ["$organic", "$organic", "$organic", "$organic", "google", "google", "google", "facebook", "facebook", "twitter", "linkedin"],
52
- }
53
- },
54
- {
55
- "event": "watch video",
56
- "weight": 8,
57
- "properties": {
58
- category: ["funny", "educational", "inspirational", "music", "news", "sports", "cooking", "DIY", "travel", "gaming"],
59
- hashTags: makeHashTags,
60
- watchTimeSec: weightedRange(10, 600, 1000, .25),
61
- quality: ["2160p", "1440p", "1080p", "720p", "480p", "360p", "240p"],
62
- format: ["mp4", "avi", "mov", "mpg"],
63
- uploader_id: chance.guid.bind(chance)
64
-
65
- }
66
- },
67
- {
68
- "event": "view item",
69
- "weight": 8,
70
- "properties": {
71
- product_id: weightedRange(1, 1000),
72
- colors: ["light", "dark", "custom", "dark"]
73
- }
74
- },
75
- {
76
- "event": "save item",
77
- "weight": 5,
78
- "properties": {
79
- product_id: weightedRange(1, 1000),
80
- colors: ["light", "dark", "custom", "dark"]
81
- }
82
- },
83
- {
84
- "event": "sign up",
85
- "isFirstEvent": true,
86
- "weight": 0,
87
- "properties": {
88
- variant: ["A", "B", "C", "Control"],
89
- experiment: ["no password", "social sign in", "new tutorial"],
90
- }
91
- }
92
- ],
93
- superProps: {
94
- platform: ["web", "mobile", "web", "mobile", "web", "kiosk", "smartTV"],
95
- // emotions: generateEmoji(),
96
-
97
- },
98
- /*
99
- user properties work the same as event properties
100
- each key should be an array or function reference
101
- */
102
- userProps: {
103
- title: chance.profession.bind(chance),
104
- luckyNumber: weightedRange(42, 420),
105
- // vibe: generateEmoji(),
106
- spiritAnimal: chance.animal.bind(chance)
107
- },
108
-
109
- scdProps: {
110
- plan: ["free", "free", "free", "free", "basic", "basic", "basic", "premium", "premium", "enterprise"],
111
- MRR: weightedRange(0, 10000, 1000, .15),
112
- NPS: weightedRange(0, 10, 150, 2),
113
- marketingOptIn: [true, true, false],
114
- dateOfRenewal: date(100, false),
115
- },
116
-
117
- /*
118
- for group analytics keys, we need an array of arrays [[],[],[]]
119
- each pair represents a group_key and the number of profiles for that key
120
- */
121
- groupKeys: [
122
- ['company_id', 350],
123
-
124
- ],
125
- groupProps: {
126
- company_id: {
127
- $name: () => { return chance.company(); },
128
- $email: () => { return `CSM: ${chance.pickone(["AK", "Jessica", "Michelle", "Dana", "Brian", "Dave"])}`; },
129
- "# of employees": weightedRange(3, 10000),
130
- "sector": ["tech", "finance", "healthcare", "education", "government", "non-profit"],
131
- "segment": ["enterprise", "SMB", "mid-market"],
132
- }
133
- },
134
-
135
- lookupTables: [
136
- {
137
- key: "product_id",
138
- entries: 1000,
139
- attributes: {
140
- category: [
141
- "Books",
142
- "Movies",
143
- "Music",
144
- "Games",
145
- "Electronics",
146
- "Computers",
147
- "Smart Home",
148
- "Home",
149
- "Garden & Tools",
150
- "Pet Supplies",
151
- "Food & Grocery",
152
- "Beauty",
153
- "Health",
154
- "Toys",
155
- "Kids",
156
- "Baby",
157
- "Handmade",
158
- "Sports",
159
- "Outdoors",
160
- "Automotive",
161
- "Industrial",
162
- "Entertainment",
163
- "Art"
164
- ],
165
- "demand": ["high", "medium", "medium", "low"],
166
- "supply": ["high", "medium", "medium", "low"],
167
- "manufacturer": chance.company.bind(chance),
168
- "price": weightedRange(5, 500, 1000, .25),
169
- "rating": weightedRange(1, 5),
170
- "reviews": weightedRange(0, 35)
171
- }
172
-
173
- }
174
- ],
175
- };
176
-
177
- module.exports = config;