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 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: 250000, //how many events
10
- numUsers: 1500, //how many users
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 now = dayjs().unix();
38
- const dayInSec = 86400;
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
- if (require.main === module) writeToDisk = true;
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
- const daysAgoBorn = chance.integer({ min: 1, max: numDays });
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 } = buildFileNames(config);
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) return { eventData, userProfilesData, scdTableData, groupProfilesData, lookupTableData };
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, { recordType: "event", ...importOpts });
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, { recordType: "user", ...importOpts });
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, { recordType: "group", ...importOpts });
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 { import: importResults, files: [eventFiles, userFiles, scdFiles, groupFiles, lookupFiles, folder] };
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 = integer(earliestTime, now);
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) writeDir = mkdir('./data');
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) => { return acc + group.bytes; }, 0);
392
- const groupSuccess = groups.reduce((acc, group) => { return acc + group.success; }, 0);
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('\n\t'));
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('have a wonderful day :)');
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "make-mp-data",
3
- "version": "1.0.13",
3
+ "version": "1.0.15",
4
4
  "description": "builds all mixpanel primitives for a given project",
5
5
  "main": "index.js",
6
6
  "scripts": {
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
  };