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.
- package/.vscode/settings.json +1 -0
- package/cli.js +19 -1
- package/index.js +49 -127
- package/package.json +2 -2
- package/tests/e2e.test.js +97 -40
- package/tests/unit.test.js +155 -0
- package/timesoup.js +92 -0
- package/utils.js +67 -101
- package/default.js +0 -177
package/.vscode/settings.json
CHANGED
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
|
-
|
|
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.
|
|
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----------------`,
|
|
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
|
|
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
|
|
371
|
+
if (!isFirstEvent) event.time = AKsTimeSoup(earliestTime, NOW);
|
|
398
372
|
|
|
399
373
|
// anonymous and session ids
|
|
400
|
-
if (global?.
|
|
401
|
-
if (global?.
|
|
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,
|
|
445
|
-
userFiles: [path.join(writeDir,
|
|
446
|
-
scdFiles: [path.join(writeDir,
|
|
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,
|
|
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
|
-
|
|
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
|
-
}
|
|
547
|
-
|
|
548
|
-
|
|
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.
|
|
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
|
-
|
|
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:
|
|
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
|
|
32
|
-
console.log('
|
|
33
|
-
const
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
expect(
|
|
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('
|
|
40
|
-
console.log('
|
|
41
|
-
const results = await generate({verbose:
|
|
42
|
-
const {
|
|
43
|
-
expect(
|
|
44
|
-
expect(
|
|
45
|
-
expect(
|
|
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
|
-
|
|
56
|
-
const invalidUsers = userProfilesData.filter(u => !validateUser(u));
|
|
70
|
+
});
|
|
57
71
|
|
|
58
|
-
|
|
59
|
-
|
|
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('
|
|
63
|
-
console.log('
|
|
64
|
-
const
|
|
65
|
-
|
|
66
|
-
const
|
|
67
|
-
expect(
|
|
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
|
-
|
|
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
|
-
|
|
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;
|
package/tests/unit.test.js
CHANGED
|
@@ -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
|
-
|
|
13
|
-
const
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
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
|
-
|
|
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
|
-
|
|
395
|
-
|
|
396
|
-
|
|
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;
|