make-mp-data 1.0.13 → 1.0.15
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/cli.js +13 -0
- package/default.js +2 -2
- package/index.js +131 -51
- package/package.json +1 -1
- package/utils.js +4 -2
package/cli.js
CHANGED
|
@@ -60,6 +60,19 @@ DOCS: https://github.com/ak--47/make-mp-data`)
|
|
|
60
60
|
describe: 'either US or EU',
|
|
61
61
|
type: 'string'
|
|
62
62
|
})
|
|
63
|
+
.option("writeToDisk", {
|
|
64
|
+
demandOption: false,
|
|
65
|
+
default: true,
|
|
66
|
+
describe: 'write data to disk',
|
|
67
|
+
alias: 'w',
|
|
68
|
+
type: 'boolean',
|
|
69
|
+
coerce: (value) => {
|
|
70
|
+
if (typeof value === 'string') {
|
|
71
|
+
return value.toLowerCase() === 'true';
|
|
72
|
+
}
|
|
73
|
+
return value;
|
|
74
|
+
}
|
|
75
|
+
})
|
|
63
76
|
.help()
|
|
64
77
|
.wrap(null)
|
|
65
78
|
.argv;
|
package/default.js
CHANGED
|
@@ -6,8 +6,8 @@ const config = {
|
|
|
6
6
|
token: "",
|
|
7
7
|
seed: "foo bar baz",
|
|
8
8
|
numDays: 30, //how many days worth of data
|
|
9
|
-
numEvents:
|
|
10
|
-
numUsers:
|
|
9
|
+
numEvents: 100000, //how many events
|
|
10
|
+
numUsers: 1000, //how many users
|
|
11
11
|
format: 'csv', //csv or json
|
|
12
12
|
region: "US",
|
|
13
13
|
|
package/index.js
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
#! /usr/bin/env node
|
|
2
2
|
|
|
3
|
-
|
|
4
3
|
/*
|
|
5
4
|
make fake mixpanel data easily!
|
|
6
5
|
by AK
|
|
@@ -25,21 +24,32 @@ const {
|
|
|
25
24
|
choose,
|
|
26
25
|
range,
|
|
27
26
|
exhaust,
|
|
28
|
-
openFinder
|
|
27
|
+
openFinder,
|
|
28
|
+
applySkew,
|
|
29
|
+
boxMullerRandom,
|
|
29
30
|
} = require("./utils.js");
|
|
30
31
|
const dayjs = require("dayjs");
|
|
31
32
|
const utc = require("dayjs/plugin/utc");
|
|
32
|
-
const cliParams = require('./cli.js');
|
|
33
|
-
|
|
34
|
-
|
|
35
33
|
dayjs.extend(utc);
|
|
34
|
+
const cliParams = require("./cli.js");
|
|
36
35
|
Array.prototype.pickOne = pick;
|
|
37
|
-
const
|
|
38
|
-
|
|
36
|
+
const NOW = dayjs().unix();
|
|
37
|
+
|
|
38
|
+
const PEAK_DAYS = [
|
|
39
|
+
dayjs().subtract(2, "day").unix(),
|
|
40
|
+
dayjs().subtract(3, "day").unix(),
|
|
41
|
+
dayjs().subtract(5, "day").unix(),
|
|
42
|
+
dayjs().subtract(7, "day").unix(),
|
|
43
|
+
dayjs().subtract(11, "day").unix(),
|
|
44
|
+
dayjs().subtract(13, "day").unix(),
|
|
45
|
+
dayjs().subtract(17, "day").unix(),
|
|
46
|
+
dayjs().subtract(19, "day").unix(),
|
|
47
|
+
dayjs().subtract(23, "day").unix(),
|
|
48
|
+
dayjs().subtract(29, "day").unix(),
|
|
49
|
+
];
|
|
39
50
|
|
|
40
51
|
//our main program
|
|
41
52
|
async function main(config) {
|
|
42
|
-
|
|
43
53
|
let {
|
|
44
54
|
seed = "every time a rug is micturated upon in this fair city...",
|
|
45
55
|
numEvents = 100000,
|
|
@@ -58,15 +68,32 @@ async function main(config) {
|
|
|
58
68
|
format = "csv",
|
|
59
69
|
token = null,
|
|
60
70
|
region = "US",
|
|
61
|
-
writeToDisk = false
|
|
71
|
+
writeToDisk = false,
|
|
62
72
|
} = config;
|
|
63
|
-
|
|
73
|
+
|
|
74
|
+
//ensure we have a token or are writing to disk
|
|
75
|
+
if (require.main === module) {
|
|
76
|
+
if (!token) {
|
|
77
|
+
if (!writeToDisk) {
|
|
78
|
+
writeToDisk = true;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
64
83
|
const uuidChance = new Chance(seed);
|
|
65
84
|
|
|
66
|
-
//the function which generates $distinct_id
|
|
85
|
+
//the function which generates $distinct_id + $created
|
|
67
86
|
function uuid() {
|
|
68
87
|
const distinct_id = uuidChance.guid();
|
|
69
|
-
|
|
88
|
+
let z = boxMullerRandom();
|
|
89
|
+
const skew = chance.normal({ mean: 10, dev: 3 });
|
|
90
|
+
z = applySkew(z, skew);
|
|
91
|
+
|
|
92
|
+
// Scale and shift the normally distributed value to fit the range of days
|
|
93
|
+
const maxZ = integer(2, 4);
|
|
94
|
+
const scaledZ = (z / maxZ + 1) / 2; // Scale to a 0-1 range
|
|
95
|
+
const daysAgoBorn = Math.round(scaledZ * (numDays - 1)) + 1; // Scale to 1-numDays range
|
|
96
|
+
|
|
70
97
|
return {
|
|
71
98
|
distinct_id,
|
|
72
99
|
...person(daysAgoBorn),
|
|
@@ -84,7 +111,6 @@ async function main(config) {
|
|
|
84
111
|
}, [])
|
|
85
112
|
.filter((e) => !e.isFirstEvent);
|
|
86
113
|
|
|
87
|
-
|
|
88
114
|
const firstEvents = events.filter((e) => e.isFirstEvent);
|
|
89
115
|
const eventData = [];
|
|
90
116
|
const userProfilesData = [];
|
|
@@ -146,7 +172,7 @@ async function main(config) {
|
|
|
146
172
|
const group = {
|
|
147
173
|
[groupKey]: i,
|
|
148
174
|
...makeProfile(groupProps[groupKey]),
|
|
149
|
-
$distinct_id: i
|
|
175
|
+
$distinct_id: i,
|
|
150
176
|
};
|
|
151
177
|
groupProfiles.push(group);
|
|
152
178
|
}
|
|
@@ -168,7 +194,8 @@ async function main(config) {
|
|
|
168
194
|
}
|
|
169
195
|
lookupTableData.push({ key, data });
|
|
170
196
|
}
|
|
171
|
-
const { eventFiles, userFiles, scdFiles, groupFiles, lookupFiles, folder } =
|
|
197
|
+
const { eventFiles, userFiles, scdFiles, groupFiles, lookupFiles, folder } =
|
|
198
|
+
buildFileNames(config);
|
|
172
199
|
const pairs = [
|
|
173
200
|
[eventFiles, eventData],
|
|
174
201
|
[userFiles, userProfilesData],
|
|
@@ -178,13 +205,20 @@ async function main(config) {
|
|
|
178
205
|
];
|
|
179
206
|
console.log("\n");
|
|
180
207
|
|
|
181
|
-
if (!writeToDisk && !token)
|
|
208
|
+
if (!writeToDisk && !token)
|
|
209
|
+
return {
|
|
210
|
+
eventData,
|
|
211
|
+
userProfilesData,
|
|
212
|
+
scdTableData,
|
|
213
|
+
groupProfilesData,
|
|
214
|
+
lookupTableData,
|
|
215
|
+
};
|
|
182
216
|
//write the files
|
|
183
217
|
for (const pair of pairs) {
|
|
184
218
|
const [paths, data] = pair;
|
|
185
219
|
for (const path of paths) {
|
|
186
220
|
let datasetsToWrite;
|
|
187
|
-
if (data?.[0]?.["key"]) datasetsToWrite = data.map(d => d.data);
|
|
221
|
+
if (data?.[0]?.["key"]) datasetsToWrite = data.map((d) => d.data);
|
|
188
222
|
else datasetsToWrite = [data];
|
|
189
223
|
for (const writeData of datasetsToWrite) {
|
|
190
224
|
if (format === "csv") {
|
|
@@ -210,37 +244,47 @@ async function main(config) {
|
|
|
210
244
|
strict: false,
|
|
211
245
|
dryRun: false,
|
|
212
246
|
abridged: false,
|
|
213
|
-
region
|
|
214
|
-
|
|
247
|
+
region,
|
|
215
248
|
};
|
|
216
249
|
//send to mixpanel
|
|
217
250
|
if (token) {
|
|
218
251
|
if (eventData) {
|
|
219
252
|
console.log(`importing events to mixpanel...`);
|
|
220
|
-
const imported = await mp(creds, eventData, {
|
|
253
|
+
const imported = await mp(creds, eventData, {
|
|
254
|
+
recordType: "event",
|
|
255
|
+
...importOpts,
|
|
256
|
+
});
|
|
221
257
|
console.log(`\tsent ${comma(imported.success)} events\n`);
|
|
222
258
|
importResults.events = imported;
|
|
223
259
|
}
|
|
224
260
|
if (userProfilesData) {
|
|
225
261
|
console.log(`importing user profiles to mixpanel...`);
|
|
226
|
-
const imported = await mp(creds, userProfilesData, {
|
|
262
|
+
const imported = await mp(creds, userProfilesData, {
|
|
263
|
+
recordType: "user",
|
|
264
|
+
...importOpts,
|
|
265
|
+
});
|
|
227
266
|
console.log(`\tsent ${comma(imported.success)} user profiles\n`);
|
|
228
267
|
importResults.users = imported;
|
|
229
268
|
}
|
|
230
269
|
if (groupProfilesData) {
|
|
231
|
-
|
|
232
270
|
for (const groupProfiles of groupProfilesData) {
|
|
233
271
|
const groupKey = groupProfiles.key;
|
|
234
272
|
const data = groupProfiles.data;
|
|
235
273
|
console.log(`importing ${groupKey} profiles to mixpanel...`);
|
|
236
|
-
const imported = await mp({ token, groupKey }, data, {
|
|
274
|
+
const imported = await mp({ token, groupKey }, data, {
|
|
275
|
+
recordType: "group",
|
|
276
|
+
...importOpts,
|
|
277
|
+
});
|
|
237
278
|
console.log(`\tsent ${comma(imported.success)} ${groupKey} profiles\n`);
|
|
238
279
|
importResults.groups.push(imported);
|
|
239
280
|
}
|
|
240
281
|
}
|
|
241
282
|
console.log(`\n\n`);
|
|
242
283
|
}
|
|
243
|
-
return {
|
|
284
|
+
return {
|
|
285
|
+
import: importResults,
|
|
286
|
+
files: [eventFiles, userFiles, scdFiles, groupFiles, lookupFiles, folder],
|
|
287
|
+
};
|
|
244
288
|
}
|
|
245
289
|
|
|
246
290
|
function makeProfile(props, defaults) {
|
|
@@ -291,7 +335,7 @@ function makeEvent(distinct_id, earliestTime, events, superProps, groupKeys, isF
|
|
|
291
335
|
};
|
|
292
336
|
|
|
293
337
|
if (isFirstEvent) event.time = earliestTime;
|
|
294
|
-
if (!isFirstEvent) event.time =
|
|
338
|
+
if (!isFirstEvent) event.time = AKsTimeSoup(earliestTime, NOW, PEAK_DAYS);
|
|
295
339
|
|
|
296
340
|
const props = { ...chosenEvent.properties, ...superProps };
|
|
297
341
|
|
|
@@ -318,8 +362,8 @@ function buildFileNames(config) {
|
|
|
318
362
|
const { format = "csv", groupKeys = [], lookupTables = [] } = config;
|
|
319
363
|
const extension = format === "csv" ? "csv" : "json";
|
|
320
364
|
const current = dayjs.utc().format("MM-DD-HH");
|
|
321
|
-
let writeDir =
|
|
322
|
-
if (config.writeToDisk)
|
|
365
|
+
let writeDir = "./";
|
|
366
|
+
if (config.writeToDisk) writeDir = mkdir("./data");
|
|
323
367
|
|
|
324
368
|
const writePaths = {
|
|
325
369
|
eventFiles: [path.join(writeDir, `events-${current}.${extension}`)],
|
|
@@ -347,19 +391,57 @@ function buildFileNames(config) {
|
|
|
347
391
|
return writePaths;
|
|
348
392
|
}
|
|
349
393
|
|
|
394
|
+
/**
|
|
395
|
+
* timestamp generator with a twist
|
|
396
|
+
* @param {number} earliestTime - The earliest timestamp in Unix format.
|
|
397
|
+
* @param {number} latestTime - The latest timestamp in Unix format.
|
|
398
|
+
* @param {Array} peakDays - Array of Unix timestamps representing the start of peak days.
|
|
399
|
+
* @returns {number} - The generated event timestamp in Unix format.
|
|
400
|
+
*/
|
|
401
|
+
function AKsTimeSoup(earliestTime, latestTime = NOW, peakDays = PEAK_DAYS) {
|
|
402
|
+
// Define business hours
|
|
403
|
+
const peakStartHour = 4; // 4 AM
|
|
404
|
+
const peakEndHour = 23; // 11 PM
|
|
405
|
+
const likelihoodOfPeakDay = chance.integer({ min: integer(5, 42), max: integer(43, 69) }); // Randomize likelihood with CHAOS!~~
|
|
406
|
+
|
|
407
|
+
// Select a day, with a preference for peak days
|
|
408
|
+
let selectedDay;
|
|
409
|
+
if (chance.bool({ likelihood: likelihoodOfPeakDay })) { // Randomized likelihood to pick a peak day
|
|
410
|
+
selectedDay = peakDays.length > 0 ? chance.pickone(peakDays) : integer(earliestTime, latestTime);
|
|
411
|
+
} else {
|
|
412
|
+
// Introduce minor peaks by allowing some events to still occur during business hours
|
|
413
|
+
selectedDay = chance.bool({ likelihood: integer(1, 42) })
|
|
414
|
+
? chance.pickone(peakDays)
|
|
415
|
+
: integer(earliestTime, latestTime);
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
// Normalize selectedDay to the start of the day
|
|
419
|
+
selectedDay = dayjs.unix(selectedDay).startOf('day').unix();
|
|
420
|
+
|
|
421
|
+
// Generate a random time within business hours with a higher concentration in the middle of the period
|
|
422
|
+
const businessStart = dayjs.unix(selectedDay).hour(peakStartHour).minute(0).second(0).unix();
|
|
423
|
+
const businessEnd = dayjs.unix(selectedDay).hour(peakEndHour).minute(0).second(0).unix();
|
|
424
|
+
let eventTime;
|
|
425
|
+
if (selectedDay === peakDays[0]) {
|
|
426
|
+
// Use a skewed distribution for peak days
|
|
427
|
+
eventTime = chance.normal({ mean: (businessEnd + businessStart) / integer(1, 4), dev: (businessEnd - businessStart) / integer(2, 8) });
|
|
428
|
+
} else {
|
|
429
|
+
// For non-peak days, use a uniform distribution to add noise
|
|
430
|
+
eventTime = integer(integer(businessStart, businessEnd), integer(businessStart, businessEnd));
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
// usually, ensure the event time is within business hours
|
|
434
|
+
if (chance.bool({ likelihood: 42 })) eventTime = Math.min(Math.max(eventTime, businessStart), businessEnd);
|
|
435
|
+
|
|
436
|
+
return eventTime;
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
|
|
440
|
+
|
|
350
441
|
// this is for CLI
|
|
351
442
|
if (require.main === module) {
|
|
352
|
-
|
|
353
443
|
const args = cliParams();
|
|
354
|
-
const {
|
|
355
|
-
token,
|
|
356
|
-
seed,
|
|
357
|
-
format,
|
|
358
|
-
numDays,
|
|
359
|
-
numUsers,
|
|
360
|
-
numEvents,
|
|
361
|
-
region
|
|
362
|
-
} = args;
|
|
444
|
+
const { token, seed, format, numDays, numUsers, numEvents, region, writeToDisk } = args;
|
|
363
445
|
const suppliedConfig = args._[0];
|
|
364
446
|
|
|
365
447
|
//if the user specifics an separate config file
|
|
@@ -380,7 +462,7 @@ if (require.main === module) {
|
|
|
380
462
|
if (numUsers) config.numUsers = numUsers;
|
|
381
463
|
if (numEvents) config.numEvents = numEvents;
|
|
382
464
|
if (region) config.region = region;
|
|
383
|
-
|
|
465
|
+
if (writeToDisk) config.writeToDisk = writeToDisk;
|
|
384
466
|
|
|
385
467
|
main(config)
|
|
386
468
|
.then((data) => {
|
|
@@ -388,18 +470,22 @@ if (require.main === module) {
|
|
|
388
470
|
const { events, groups, users } = data.import;
|
|
389
471
|
const files = data.files;
|
|
390
472
|
const folder = files.pop();
|
|
391
|
-
const groupBytes = groups.reduce((acc, group) => {
|
|
392
|
-
|
|
473
|
+
const groupBytes = groups.reduce((acc, group) => {
|
|
474
|
+
return acc + group.bytes;
|
|
475
|
+
}, 0);
|
|
476
|
+
const groupSuccess = groups.reduce((acc, group) => {
|
|
477
|
+
return acc + group.success;
|
|
478
|
+
}, 0);
|
|
393
479
|
const bytes = events.bytes + groupBytes + users.bytes;
|
|
394
480
|
const stats = {
|
|
395
481
|
events: comma(events.success || 0),
|
|
396
482
|
users: comma(users.success || 0),
|
|
397
483
|
groups: comma(groupSuccess || 0),
|
|
398
|
-
bytes: bytesHuman(bytes || 0)
|
|
484
|
+
bytes: bytesHuman(bytes || 0),
|
|
399
485
|
};
|
|
400
486
|
if (bytes > 0) console.table(stats);
|
|
401
487
|
console.log(`\nfiles written to ${folder}...`);
|
|
402
|
-
console.log("\t" + files.flat().join(
|
|
488
|
+
console.log("\t" + files.flat().join("\n\t"));
|
|
403
489
|
console.log(`\n------------------SUMMARY------------------\n\n\n`);
|
|
404
490
|
})
|
|
405
491
|
.catch((e) => {
|
|
@@ -409,13 +495,10 @@ if (require.main === module) {
|
|
|
409
495
|
debugger;
|
|
410
496
|
})
|
|
411
497
|
.finally(() => {
|
|
412
|
-
console.log(
|
|
498
|
+
console.log("have a wonderful day :)");
|
|
413
499
|
openFinder(path.resolve("./data"));
|
|
414
500
|
});
|
|
415
|
-
|
|
416
|
-
}
|
|
417
|
-
|
|
418
|
-
else {
|
|
501
|
+
} else {
|
|
419
502
|
module.exports = {
|
|
420
503
|
generate: main,
|
|
421
504
|
weightedRange,
|
|
@@ -429,9 +512,6 @@ else {
|
|
|
429
512
|
choose,
|
|
430
513
|
range,
|
|
431
514
|
exhaust,
|
|
432
|
-
openFinder
|
|
515
|
+
openFinder,
|
|
433
516
|
};
|
|
434
517
|
}
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
package/package.json
CHANGED
package/utils.js
CHANGED
|
@@ -20,7 +20,7 @@ function pick() {
|
|
|
20
20
|
function date(inTheLast = 30, isPast = true, format = 'YYYY-MM-DD') {
|
|
21
21
|
const now = dayjs.utc();
|
|
22
22
|
return function () {
|
|
23
|
-
const when = chance.integer({ min: 0, max: inTheLast });
|
|
23
|
+
const when = chance.integer({ min: 0, max: Math.abs(inTheLast) });
|
|
24
24
|
let then;
|
|
25
25
|
if (isPast) then = now.subtract(when, 'day');
|
|
26
26
|
if (!isPast) then = now.add(when, 'day');
|
|
@@ -227,5 +227,7 @@ module.exports = {
|
|
|
227
227
|
choose,
|
|
228
228
|
range,
|
|
229
229
|
exhaust,
|
|
230
|
-
openFinder
|
|
230
|
+
openFinder,
|
|
231
|
+
applySkew,
|
|
232
|
+
boxMullerRandom,
|
|
231
233
|
};
|