make-mp-data 1.1.18 → 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 +53 -128
- package/package.json +2 -2
- package/tests/e2e.test.js +108 -32
- package/tests/unit.test.js +155 -0
- package/timesoup.js +92 -0
- package/utils.js +106 -133
- 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,50 +14,19 @@ 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
|
-
person,
|
|
26
|
-
choose,
|
|
27
|
-
range,
|
|
28
|
-
exhaust,
|
|
29
|
-
openFinder,
|
|
30
|
-
applySkew,
|
|
31
|
-
boxMullerRandom,
|
|
32
|
-
getUniqueKeys
|
|
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);
|
|
37
22
|
const cliParams = require("./cli.js");
|
|
38
23
|
const { makeName, md5 } = require('ak-tools');
|
|
39
|
-
|
|
40
|
-
Array.prototype.pickOne = pick;
|
|
41
24
|
const NOW = dayjs().unix();
|
|
42
25
|
let VERBOSE = false;
|
|
43
26
|
|
|
44
27
|
/** @typedef {import('./types.d.ts').Config} Config */
|
|
45
28
|
/** @typedef {import('./types.d.ts').EventConfig} EventConfig */
|
|
46
29
|
|
|
47
|
-
|
|
48
|
-
const PEAK_DAYS = [
|
|
49
|
-
dayjs().subtract(2, "day").unix(),
|
|
50
|
-
dayjs().subtract(3, "day").unix(),
|
|
51
|
-
dayjs().subtract(5, "day").unix(),
|
|
52
|
-
dayjs().subtract(7, "day").unix(),
|
|
53
|
-
dayjs().subtract(11, "day").unix(),
|
|
54
|
-
dayjs().subtract(13, "day").unix(),
|
|
55
|
-
dayjs().subtract(17, "day").unix(),
|
|
56
|
-
dayjs().subtract(19, "day").unix(),
|
|
57
|
-
dayjs().subtract(23, "day").unix(),
|
|
58
|
-
dayjs().subtract(29, "day").unix(),
|
|
59
|
-
];
|
|
60
|
-
|
|
61
30
|
/**
|
|
62
31
|
* generates fake mixpanel data
|
|
63
32
|
* @param {Config} config
|
|
@@ -74,7 +43,7 @@ async function main(config) {
|
|
|
74
43
|
favoriteColor: ["red", "green", "blue", "yellow"],
|
|
75
44
|
spiritAnimal: chance.animal,
|
|
76
45
|
},
|
|
77
|
-
scdProps = { NPS: weightedRange(0, 10, 150, 1.6) },
|
|
46
|
+
scdProps = { NPS: u.weightedRange(0, 10, 150, 1.6) },
|
|
78
47
|
groupKeys = [],
|
|
79
48
|
groupProps = {},
|
|
80
49
|
lookupTables = [],
|
|
@@ -88,7 +57,7 @@ async function main(config) {
|
|
|
88
57
|
} = config;
|
|
89
58
|
VERBOSE = verbose;
|
|
90
59
|
config.simulationName = makeName();
|
|
91
|
-
global.
|
|
60
|
+
global.MP_SIMULATION_CONFIG = config;
|
|
92
61
|
const uuidChance = new Chance(seed);
|
|
93
62
|
log(`------------------SETUP------------------`);
|
|
94
63
|
log(`\nyour data simulation will heretofore be known as: \n\n\t${config.simulationName.toUpperCase()}...\n`);
|
|
@@ -99,18 +68,18 @@ async function main(config) {
|
|
|
99
68
|
//the function which generates $distinct_id + $anonymous_ids, $session_ids, and $created, skewing towards the present
|
|
100
69
|
function generateUser() {
|
|
101
70
|
const distinct_id = uuidChance.guid();
|
|
102
|
-
let z = boxMullerRandom();
|
|
71
|
+
let z = u.boxMullerRandom();
|
|
103
72
|
const skew = chance.normal({ mean: 10, dev: 3 });
|
|
104
|
-
z = applySkew(z, skew);
|
|
73
|
+
z = u.applySkew(z, skew);
|
|
105
74
|
|
|
106
75
|
// Scale and shift the normally distributed value to fit the range of days
|
|
107
|
-
const maxZ = integer(2, 4);
|
|
76
|
+
const maxZ = u.integer(2, 4);
|
|
108
77
|
const scaledZ = (z / maxZ + 1) / 2;
|
|
109
78
|
const daysAgoBorn = Math.round(scaledZ * (numDays - 1)) + 1;
|
|
110
79
|
|
|
111
80
|
return {
|
|
112
81
|
distinct_id,
|
|
113
|
-
...person(daysAgoBorn),
|
|
82
|
+
...u.person(daysAgoBorn),
|
|
114
83
|
};
|
|
115
84
|
}
|
|
116
85
|
|
|
@@ -136,16 +105,16 @@ async function main(config) {
|
|
|
136
105
|
const avgEvPerUser = Math.floor(numEvents / numUsers);
|
|
137
106
|
|
|
138
107
|
//user loop
|
|
139
|
-
log(`---------------SIMULATION----------------`,
|
|
108
|
+
log(`---------------SIMULATION----------------`, "\n\n");
|
|
140
109
|
for (let i = 1; i < numUsers + 1; i++) {
|
|
141
|
-
progress("users", i);
|
|
110
|
+
u.progress("users", i);
|
|
142
111
|
const user = generateUser();
|
|
143
112
|
const { distinct_id, $created, anonymousIds, sessionIds } = user;
|
|
144
113
|
userProfilesData.push(makeProfile(userProps, user));
|
|
145
114
|
const mutations = chance.integer({ min: 1, max: 10 });
|
|
146
115
|
scdTableData.push(makeSCD(scdProps, distinct_id, mutations, $created));
|
|
147
116
|
const numEventsThisUser = Math.round(
|
|
148
|
-
chance.normal({ mean: avgEvPerUser, dev: avgEvPerUser / integer(3, 7) })
|
|
117
|
+
chance.normal({ mean: avgEvPerUser, dev: avgEvPerUser / u.integer(3, 7) })
|
|
149
118
|
);
|
|
150
119
|
|
|
151
120
|
if (firstEvents.length) {
|
|
@@ -189,7 +158,7 @@ async function main(config) {
|
|
|
189
158
|
const groupCardinality = groupPair[1];
|
|
190
159
|
const groupProfiles = [];
|
|
191
160
|
for (let i = 1; i < groupCardinality + 1; i++) {
|
|
192
|
-
progress("groups", i);
|
|
161
|
+
u.progress("groups", i);
|
|
193
162
|
const group = {
|
|
194
163
|
[groupKey]: i,
|
|
195
164
|
...makeProfile(groupProps[groupKey]),
|
|
@@ -206,7 +175,7 @@ async function main(config) {
|
|
|
206
175
|
const { key, entries, attributes } = lookupTable;
|
|
207
176
|
const data = [];
|
|
208
177
|
for (let i = 1; i < entries + 1; i++) {
|
|
209
|
-
progress("lookups", i);
|
|
178
|
+
u.progress("lookups", i);
|
|
210
179
|
const item = {
|
|
211
180
|
[key]: i,
|
|
212
181
|
...makeProfile(attributes),
|
|
@@ -238,17 +207,19 @@ async function main(config) {
|
|
|
238
207
|
log(`-----------------WRITES------------------`, `\n\n`);
|
|
239
208
|
//write the files
|
|
240
209
|
if (writeToDisk) {
|
|
241
|
-
if (verbose) log(`writing files... for ${config.simulationName}`);
|
|
242
|
-
for (const pair of pairs) {
|
|
210
|
+
if (verbose) log(`writing files... for ${config.simulationName}\n`);
|
|
211
|
+
loopFiles: for (const pair of pairs) {
|
|
243
212
|
const [paths, data] = pair;
|
|
213
|
+
if (!data.length) continue loopFiles;
|
|
244
214
|
for (const path of paths) {
|
|
245
215
|
let datasetsToWrite;
|
|
246
216
|
if (data?.[0]?.["key"]) datasetsToWrite = data.map((d) => d.data);
|
|
247
217
|
else datasetsToWrite = [data];
|
|
248
218
|
for (const writeData of datasetsToWrite) {
|
|
249
|
-
if
|
|
219
|
+
//if it's a lookup table, it's always a CSV
|
|
220
|
+
if (format === "csv" || path.includes("-LOOKUP.csv")) {
|
|
250
221
|
log(`writing ${path}`);
|
|
251
|
-
const columns = getUniqueKeys(writeData);
|
|
222
|
+
const columns = u.getUniqueKeys(writeData);
|
|
252
223
|
//papa parse needs nested JSON stringified
|
|
253
224
|
writeData.forEach((e) => {
|
|
254
225
|
for (const key in e) {
|
|
@@ -329,6 +300,9 @@ async function main(config) {
|
|
|
329
300
|
};
|
|
330
301
|
}
|
|
331
302
|
|
|
303
|
+
|
|
304
|
+
|
|
305
|
+
|
|
332
306
|
function makeProfile(props, defaults) {
|
|
333
307
|
//build the spec
|
|
334
308
|
const profile = {
|
|
@@ -347,6 +321,7 @@ function makeProfile(props, defaults) {
|
|
|
347
321
|
}
|
|
348
322
|
|
|
349
323
|
function makeSCD(props, distinct_id, mutations, $created) {
|
|
324
|
+
if (JSON.stringify(props) === "{}") return [];
|
|
350
325
|
const scdEntries = [];
|
|
351
326
|
let lastInserted = dayjs($created);
|
|
352
327
|
const deltaDays = dayjs().diff(lastInserted, "day");
|
|
@@ -355,12 +330,12 @@ function makeSCD(props, distinct_id, mutations, $created) {
|
|
|
355
330
|
if (lastInserted.isAfter(dayjs())) break;
|
|
356
331
|
const scd = makeProfile(props, { distinct_id });
|
|
357
332
|
scd.startTime = lastInserted.toISOString();
|
|
358
|
-
lastInserted = lastInserted.add(integer(1, 1000), "seconds");
|
|
333
|
+
lastInserted = lastInserted.add(u.integer(1, 1000), "seconds");
|
|
359
334
|
scd.insertTime = lastInserted.toISOString();
|
|
360
335
|
scdEntries.push({ ...scd });
|
|
361
336
|
lastInserted = lastInserted
|
|
362
|
-
.add(integer(0, deltaDays), "day")
|
|
363
|
-
.subtract(integer(1, 1000), "seconds");
|
|
337
|
+
.add(u.integer(0, deltaDays), "day")
|
|
338
|
+
.subtract(u.integer(1, 1000), "seconds");
|
|
364
339
|
}
|
|
365
340
|
|
|
366
341
|
return scdEntries;
|
|
@@ -378,7 +353,7 @@ function makeSCD(props, distinct_id, mutations, $created) {
|
|
|
378
353
|
* @param {Boolean} isFirstEvent=false
|
|
379
354
|
*/
|
|
380
355
|
function makeEvent(distinct_id, anonymousIds, sessionIds, earliestTime, events, superProps, groupKeys, isFirstEvent = false) {
|
|
381
|
-
let chosenEvent =
|
|
356
|
+
let chosenEvent = chance.pickone(events);
|
|
382
357
|
|
|
383
358
|
//allow for a string shorthand
|
|
384
359
|
if (typeof chosenEvent === "string") {
|
|
@@ -393,11 +368,11 @@ function makeEvent(distinct_id, anonymousIds, sessionIds, earliestTime, events,
|
|
|
393
368
|
|
|
394
369
|
//event time
|
|
395
370
|
if (isFirstEvent) event.time = dayjs.unix(earliestTime).toISOString();
|
|
396
|
-
if (!isFirstEvent) event.time = AKsTimeSoup(earliestTime, NOW
|
|
371
|
+
if (!isFirstEvent) event.time = AKsTimeSoup(earliestTime, NOW);
|
|
397
372
|
|
|
398
373
|
// anonymous and session ids
|
|
399
|
-
if (global?.
|
|
400
|
-
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);
|
|
401
376
|
|
|
402
377
|
//sometimes have a $user_id
|
|
403
378
|
if (!isFirstEvent && chance.bool({ likelihood: 42 })) event.$user_id = distinct_id;
|
|
@@ -410,7 +385,7 @@ function makeEvent(distinct_id, anonymousIds, sessionIds, earliestTime, events,
|
|
|
410
385
|
//iterate through custom properties
|
|
411
386
|
for (const key in props) {
|
|
412
387
|
try {
|
|
413
|
-
event[key] = choose(props[key]);
|
|
388
|
+
event[key] = u.choose(props[key]);
|
|
414
389
|
} catch (e) {
|
|
415
390
|
console.error(`error with ${key} in ${chosenEvent.event} event`, e);
|
|
416
391
|
debugger;
|
|
@@ -422,7 +397,7 @@ function makeEvent(distinct_id, anonymousIds, sessionIds, earliestTime, events,
|
|
|
422
397
|
const groupKey = groupPair[0];
|
|
423
398
|
const groupCardinality = groupPair[1];
|
|
424
399
|
|
|
425
|
-
event[groupKey] = weightedRange(1, groupCardinality)
|
|
400
|
+
event[groupKey] = u.pick(u.weightedRange(1, groupCardinality));
|
|
426
401
|
}
|
|
427
402
|
|
|
428
403
|
//make $insert_id
|
|
@@ -440,9 +415,9 @@ function buildFileNames(config) {
|
|
|
440
415
|
if (config.writeToDisk) writeDir = mkdir("./data");
|
|
441
416
|
|
|
442
417
|
const writePaths = {
|
|
443
|
-
eventFiles: [path.join(writeDir,
|
|
444
|
-
userFiles: [path.join(writeDir,
|
|
445
|
-
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}`)],
|
|
446
421
|
groupFiles: [],
|
|
447
422
|
lookupFiles: [],
|
|
448
423
|
folder: writeDir,
|
|
@@ -451,88 +426,28 @@ function buildFileNames(config) {
|
|
|
451
426
|
for (const groupPair of groupKeys) {
|
|
452
427
|
const groupKey = groupPair[0];
|
|
453
428
|
writePaths.groupFiles.push(
|
|
454
|
-
path.join(writeDir,
|
|
429
|
+
path.join(writeDir, `${simName}-${groupKey}-GROUP.${extension}`)
|
|
455
430
|
);
|
|
456
431
|
}
|
|
457
432
|
|
|
458
433
|
for (const lookupTable of lookupTables) {
|
|
459
434
|
const { key } = lookupTable;
|
|
460
435
|
writePaths.lookupFiles.push(
|
|
461
|
-
|
|
436
|
+
//lookups are always CSVs
|
|
437
|
+
path.join(writeDir, `${simName}-${key}-LOOKUP.csv`)
|
|
462
438
|
);
|
|
463
439
|
}
|
|
464
440
|
|
|
465
441
|
return writePaths;
|
|
466
442
|
}
|
|
467
443
|
|
|
468
|
-
/**
|
|
469
|
-
* essentially, a timestamp generator with a twist
|
|
470
|
-
* @param {number} earliestTime - The earliest timestamp in Unix format.
|
|
471
|
-
* @param {number} latestTime - The latest timestamp in Unix format.
|
|
472
|
-
* @param {Array} peakDays - Array of Unix timestamps representing the start of peak days.
|
|
473
|
-
* @returns {number} - The generated event timestamp in Unix format.
|
|
474
|
-
*/
|
|
475
|
-
function AKsTimeSoup(earliestTime, latestTime = NOW, peakDays = PEAK_DAYS) {
|
|
476
|
-
let chosenTime;
|
|
477
|
-
let eventTime;
|
|
478
|
-
let validTime = false;
|
|
479
|
-
|
|
480
|
-
if (typeof earliestTime !== "number") {
|
|
481
|
-
if (parseInt(earliestTime) > 0) earliestTime = parseInt(earliestTime);
|
|
482
|
-
if (dayjs(earliestTime).isValid()) earliestTime = dayjs(earliestTime).unix();
|
|
483
|
-
}
|
|
484
|
-
|
|
485
|
-
while (!validTime) {
|
|
486
|
-
|
|
487
|
-
// Define business hours
|
|
488
|
-
const peakStartHour = 4; // 4 AM
|
|
489
|
-
const peakEndHour = 23; // 11 PM
|
|
490
|
-
const likelihoodOfPeakDay = chance.integer({ min: integer(5, 42), max: integer(43, 69) }); // Randomize likelihood with CHAOS!~~
|
|
491
|
-
|
|
492
|
-
// Select a day, with a preference for peak days
|
|
493
|
-
let selectedDay;
|
|
494
|
-
if (chance.bool({ likelihood: likelihoodOfPeakDay })) { // Randomized likelihood to pick a peak day
|
|
495
|
-
selectedDay = peakDays.length > 0 ? chance.pickone(peakDays) : integer(earliestTime, latestTime);
|
|
496
|
-
} else {
|
|
497
|
-
// Introduce minor peaks by allowing some events to still occur during business hours
|
|
498
|
-
selectedDay = chance.bool({ likelihood: integer(1, 42) })
|
|
499
|
-
? chance.pickone(peakDays)
|
|
500
|
-
: integer(earliestTime, latestTime);
|
|
501
|
-
}
|
|
502
|
-
|
|
503
|
-
// Normalize selectedDay to the start of the day
|
|
504
|
-
selectedDay = dayjs.unix(selectedDay).startOf('day').unix();
|
|
505
|
-
|
|
506
|
-
// Generate a random time within business hours with a higher concentration in the middle of the period
|
|
507
|
-
const businessStart = dayjs.unix(selectedDay).hour(peakStartHour).minute(0).second(0).unix();
|
|
508
|
-
const businessEnd = dayjs.unix(selectedDay).hour(peakEndHour).minute(0).second(0).unix();
|
|
509
|
-
|
|
510
|
-
if (selectedDay === peakDays[0]) {
|
|
511
|
-
// Use a skewed distribution for peak days
|
|
512
|
-
eventTime = chance.normal({ mean: (businessEnd + businessStart) / integer(1, 4), dev: (businessEnd - businessStart) / integer(2, 8) });
|
|
513
|
-
} else {
|
|
514
|
-
// For non-peak days, use a uniform distribution to add noise
|
|
515
|
-
eventTime = integer(integer(businessStart, businessEnd), integer(businessStart, businessEnd));
|
|
516
|
-
}
|
|
517
|
-
|
|
518
|
-
// usually, ensure the event time is within business hours
|
|
519
|
-
if (chance.bool({ likelihood: 42 })) eventTime = Math.min(Math.max(eventTime, businessStart), businessEnd);
|
|
520
|
-
|
|
521
|
-
if (eventTime > 0) validTime = true;
|
|
522
|
-
const parsedTime = dayjs.unix(eventTime).toISOString();
|
|
523
|
-
if (!parsedTime.startsWith('20')) validTime = false;
|
|
524
|
-
|
|
525
|
-
}
|
|
526
|
-
chosenTime = dayjs.unix(eventTime).toISOString();
|
|
527
|
-
return chosenTime;
|
|
528
|
-
}
|
|
529
444
|
|
|
530
445
|
|
|
531
446
|
|
|
532
447
|
// this is for CLI
|
|
533
448
|
if (require.main === module) {
|
|
534
449
|
const args = cliParams();
|
|
535
|
-
const { token, seed, format, numDays, numUsers, numEvents, region, writeToDisk } = args;
|
|
450
|
+
const { token, seed, format, numDays, numUsers, numEvents, region, writeToDisk, complex = false } = args;
|
|
536
451
|
const suppliedConfig = args._[0];
|
|
537
452
|
|
|
538
453
|
//if the user specifics an separate config file
|
|
@@ -540,9 +455,18 @@ if (require.main === module) {
|
|
|
540
455
|
if (suppliedConfig) {
|
|
541
456
|
log(`using ${suppliedConfig} for data\n`);
|
|
542
457
|
config = require(path.resolve(suppliedConfig));
|
|
543
|
-
}
|
|
544
|
-
|
|
545
|
-
|
|
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
|
+
}
|
|
546
470
|
}
|
|
547
471
|
|
|
548
472
|
//override config with cli params
|
|
@@ -589,9 +513,10 @@ if (require.main === module) {
|
|
|
589
513
|
})
|
|
590
514
|
.finally(() => {
|
|
591
515
|
log("have a wonderful day :)");
|
|
592
|
-
openFinder(path.resolve("./data"));
|
|
516
|
+
u.openFinder(path.resolve("./data"));
|
|
593
517
|
});
|
|
594
518
|
} else {
|
|
519
|
+
main.utils = { ...u };
|
|
595
520
|
main.timeSoup = AKsTimeSoup;
|
|
596
521
|
module.exports = main;
|
|
597
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
|
@@ -8,55 +8,94 @@ require('dotenv').config();
|
|
|
8
8
|
const { execSync } = require("child_process");
|
|
9
9
|
const u = require('ak-tools');
|
|
10
10
|
|
|
11
|
+
const simple = require('../examples/simple');
|
|
12
|
+
const complex = require('../examples/complex');
|
|
13
|
+
const deep = require('../examples/deepNest');
|
|
14
|
+
|
|
11
15
|
const timeout = 60000;
|
|
12
16
|
const testToken = process.env.TEST_TOKEN;
|
|
13
17
|
|
|
18
|
+
describe('module', () => {
|
|
14
19
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
test('works as module', async () => {
|
|
20
|
+
test('works as module (no config)', async () => {
|
|
18
21
|
console.log('MODULE TEST');
|
|
19
|
-
const results = await generate({ 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" });
|
|
20
23
|
const { eventData, groupProfilesData, lookupTableData, scdTableData, userProfilesData } = results;
|
|
21
24
|
expect(eventData.length).toBeGreaterThan(980);
|
|
22
25
|
expect(groupProfilesData.length).toBe(0);
|
|
23
26
|
expect(lookupTableData.length).toBe(0);
|
|
24
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);
|
|
25
40
|
expect(userProfilesData.length).toBe(100);
|
|
26
41
|
|
|
27
42
|
}, timeout);
|
|
28
43
|
|
|
29
|
-
test('works as
|
|
30
|
-
console.log('
|
|
31
|
-
const
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
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
|
+
|
|
35
55
|
}, timeout);
|
|
36
56
|
|
|
37
|
-
test('
|
|
38
|
-
console.log('
|
|
39
|
-
const results = await generate({ writeToDisk: false, numEvents: 1100, numUsers: 100, seed: "deal with it"
|
|
40
|
-
const {
|
|
41
|
-
expect(
|
|
42
|
-
expect(
|
|
43
|
-
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
|
+
|
|
44
67
|
}, timeout);
|
|
45
68
|
|
|
46
|
-
test('every record is valid', async () => {
|
|
47
|
-
console.log('VALIDATION TEST');
|
|
48
|
-
const results = await generate({ writeToDisk: false, numEvents: 10000, numUsers: 500 });
|
|
49
|
-
const { eventData, userProfilesData } = results;
|
|
50
|
-
const areEventsValid = eventData.every(validateEvent);
|
|
51
|
-
const areUsersValid = userProfilesData.every(validateUser);
|
|
52
69
|
|
|
53
|
-
|
|
54
|
-
const invalidUsers = userProfilesData.filter(u => !validateUser(u));
|
|
70
|
+
});
|
|
55
71
|
|
|
56
|
-
|
|
57
|
-
|
|
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();
|
|
58
80
|
}, timeout);
|
|
59
81
|
|
|
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);
|
|
90
|
+
|
|
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);
|
|
60
99
|
|
|
61
100
|
});
|
|
62
101
|
|
|
@@ -91,16 +130,47 @@ describe('options + tweaks', () => {
|
|
|
91
130
|
expect(anonIds.length).toBe(0);
|
|
92
131
|
}, timeout);
|
|
93
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);
|
|
94
141
|
|
|
95
|
-
|
|
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);
|
|
96
148
|
|
|
149
|
+
const invalidEvents = eventData.filter(e => !validateEvent(e));
|
|
150
|
+
const invalidUsers = userProfilesData.filter(u => !validateUser(u));
|
|
97
151
|
|
|
152
|
+
expect(areEventsValid).toBe(true);
|
|
153
|
+
expect(areUsersValid).toBe(true);
|
|
154
|
+
}, timeout);
|
|
98
155
|
|
|
99
|
-
|
|
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);
|
|
100
164
|
|
|
101
165
|
});
|
|
102
166
|
|
|
103
167
|
afterAll(() => {
|
|
168
|
+
clearData();
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
//helpers
|
|
172
|
+
|
|
173
|
+
function clearData() {
|
|
104
174
|
try {
|
|
105
175
|
console.log('clearing...');
|
|
106
176
|
execSync(`npm run prune`);
|
|
@@ -109,9 +179,7 @@ afterAll(() => {
|
|
|
109
179
|
catch (err) {
|
|
110
180
|
console.log('error clearing files');
|
|
111
181
|
}
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
//helpers
|
|
182
|
+
}
|
|
115
183
|
|
|
116
184
|
function validateEvent(event) {
|
|
117
185
|
if (!event.event) return false;
|
|
@@ -128,4 +196,12 @@ function validateUser(user) {
|
|
|
128
196
|
if (!user.$email) return false;
|
|
129
197
|
if (!user.$created) return false;
|
|
130
198
|
return true;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
function validateTime(str) {
|
|
203
|
+
if (!str) return false;
|
|
204
|
+
if (str.startsWith('-')) return false;
|
|
205
|
+
if (!str.startsWith('20')) return false;
|
|
206
|
+
return true;
|
|
131
207
|
}
|