make-mp-data 1.0.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/.gitattributes ADDED
@@ -0,0 +1,2 @@
1
+ # Auto detect text files and perform LF normalization
2
+ * text=auto
package/.prettierrc ADDED
File without changes
@@ -0,0 +1,19 @@
1
+ {
2
+ // Use IntelliSense to learn about possible attributes.
3
+ // Hover to view descriptions of existing attributes.
4
+ // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
5
+ "version": "0.2.0",
6
+ "configurations": [
7
+ {
8
+ "type": "node",
9
+ "request": "launch",
10
+ "name": "go",
11
+ "runtimeExecutable": "nodemon",
12
+ "program": "${file}",
13
+ "restart": true,
14
+ "console": "integratedTerminal",
15
+ "internalConsoleOptions": "neverOpen",
16
+ "args": ["--ignore", "./data/"]
17
+ }
18
+ ]
19
+ }
@@ -0,0 +1,5 @@
1
+ {
2
+ "cSpell.words": [
3
+ "unparse"
4
+ ]
5
+ }
package/README.md ADDED
@@ -0,0 +1,30 @@
1
+
2
+
3
+ # Make Mixpanel Data
4
+ a quick and dirty CLI in node.js to generate fake data for mixpanel.
5
+
6
+ ## tldr;
7
+
8
+ ```bash
9
+ npx make-mp-data --token 1234
10
+ ```
11
+ - makes events, users, groups (sends them to your project)
12
+ - makes lookup tables and SCD type 2 exported as CSVs
13
+
14
+ (note: if you want group analytics, add a `company_id` group key to your project before running)
15
+
16
+ ## customization
17
+
18
+ ```bash
19
+ npx make-mp-data [dataModel.js] [options]
20
+ ```
21
+ ex.
22
+
23
+ ```bash
24
+ npx make-mp-data ecommSpec.js --token 1234 --numDays 30 --numUsers 1000 --numEvents 1000000
25
+ ```
26
+
27
+ see `--help` for a full list of options
28
+
29
+ see `./examples/` for a few `dataModel.js`` examples
30
+
package/cli.js ADDED
@@ -0,0 +1,74 @@
1
+ const yargs = require('yargs');
2
+ const { version } = require('./package.json');
3
+
4
+ const hero = String.raw`
5
+
6
+ ┏┳┓┏┓ ┏┫┏┓╋┏┓ ┏┓┏┓┏┓┏┓┏┓┏┓╋┏┓┏┓
7
+ ┛┗┗┣┛ ┗┻┗┻┗┗┻ ┗┫┗ ┛┗┗ ┛ ┗┻┗┗┛┛
8
+ ┛ ┛
9
+ makes all the things for mixpanel (v${version || 1})
10
+ by ak@mixpanel.com
11
+ `;
12
+
13
+ console.log(hero);
14
+
15
+ function cliParams() {
16
+ const args = yargs(process.argv.splice(2))
17
+ .scriptName("make-mp-data")
18
+ .usage(`\nusage:\nnpx $0 [dataModel.js] [options]
19
+ ex:
20
+ npx $0 --token 1234 --events 1000000
21
+ npx $0 myDataOutline.js --token 1234 --days 90
22
+
23
+ DOCS: https://github.com/ak--47/make-mp-data`)
24
+ .command('$0', 'model mixpanel data', () => { })
25
+ .option("token", {
26
+ demandOption: false,
27
+ describe: 'project token; if supplied data will be sent to mixpanel',
28
+ type: 'string'
29
+ })
30
+ .option("seed", {
31
+ demandOption: false,
32
+ describe: 'randomness seed; used to create distinct_ids',
33
+ type: 'string'
34
+ })
35
+ .option("format", {
36
+ demandOption: false,
37
+ default: 'csv',
38
+ describe: 'csv or json',
39
+ type: 'string'
40
+ })
41
+ .option("numDays", {
42
+ demandOption: false,
43
+ describe: 'number of days in past to model',
44
+ type: 'number',
45
+ })
46
+ .option("numUsers", {
47
+ demandOption: false,
48
+ describe: 'number of users to model',
49
+ type: 'number',
50
+ })
51
+ .option("numEvents", {
52
+ demandOption: false,
53
+ describe: 'number of events to model',
54
+ type: 'number',
55
+ })
56
+ .option("region", {
57
+ demandOption: false,
58
+ default: 'US',
59
+ describe: 'either US or EU',
60
+ type: 'string'
61
+ })
62
+ .help()
63
+ .wrap(null)
64
+ .argv;
65
+
66
+ // if (args._.length === 0 && !args.type?.toLowerCase()?.includes('export')) {
67
+ // yargs.showHelp();
68
+ // process.exit();
69
+ // }
70
+ return args;
71
+ }
72
+
73
+
74
+ module.exports = cliParams;
package/data/.gitkeep ADDED
File without changes
package/default.js ADDED
@@ -0,0 +1,152 @@
1
+ const Chance = require('chance');
2
+ const chance = new Chance();
3
+ const { weightedRange, makeProducts, date } = require('./utils.js');
4
+
5
+ const config = {
6
+ token: "",
7
+ seed: "foo bar baz",
8
+ numDays: 30, //how many days worth of data
9
+ numEvents: 250000, //how many events
10
+ numUsers: 1500, //how many users
11
+ format: 'csv', //csv or json
12
+ region: "US",
13
+
14
+ events: [
15
+ {
16
+ "event": "checkout",
17
+ "weight": 2,
18
+ "properties": {
19
+ amount: weightedRange(5, 500, 1000, .25),
20
+ currency: ["USD", "CAD", "EUR", "BTC", "ETH", "JPY"],
21
+ cart: makeProducts,
22
+ }
23
+ },
24
+ {
25
+ "event": "add to cart",
26
+ "weight": 4,
27
+ "properties": {
28
+ isFeaturedItem: [true, false, false],
29
+ amount: weightedRange(5, 500, 1000, .25),
30
+ rating: weightedRange(1, 5),
31
+ reviews: weightedRange(0, 35),
32
+ product_id: weightedRange(1, 1000)
33
+ }
34
+ },
35
+ {
36
+ "event": "page view",
37
+ "weight": 10,
38
+ "properties": {
39
+ path: ["/", "/", "/help", "/account", "/watch", "/listen", "/product", "/people", "/peace"],
40
+ }
41
+ },
42
+ {
43
+ "event": "view item",
44
+ "weight": 8,
45
+ "properties": {
46
+ product_id: weightedRange(1, 1000),
47
+ colors: ["light", "dark", "custom", "dark"]
48
+ }
49
+ },
50
+ {
51
+ "event": "save item",
52
+ "weight": 5,
53
+ "properties": {
54
+ product_id: weightedRange(1, 1000),
55
+ colors: ["light", "dark", "custom", "dark"]
56
+ }
57
+ },
58
+ {
59
+ "event": "sign up",
60
+ "isFirstEvent": true,
61
+ "weight": 0,
62
+ "properties": {
63
+ variant: ["A", "B", "C", "Control"],
64
+ experiment: ["no password", "social sign in", "new tutorial"],
65
+ }
66
+ }
67
+ ],
68
+ superProps: {
69
+ platform: ["web", "mobile", "kiosk"],
70
+
71
+ },
72
+ /*
73
+ user properties work the same as event properties
74
+ each key should be an array or function reference
75
+ */
76
+ userProps: {
77
+ title: chance.profession.bind(chance),
78
+ luckyNumber: weightedRange(42, 420),
79
+ servicesUsed: [["foo"], ["foo", "bar"], ["foo", "bar", "baz"], ["foo", "bar", "baz", "qux"], ["baz", "qux"], ["qux"]],
80
+ spiritAnimal: chance.animal.bind(chance)
81
+ },
82
+
83
+ scdProps: {
84
+ plan: ["free", "free", "free", "basic", "basic", "premium", "enterprise"],
85
+ MRR: weightedRange(0, 10000, 1000, .15),
86
+ NPS: weightedRange(0, 10, 150, 2),
87
+ marketingOptIn: [true, true, false],
88
+ dateOfRenewal: date(100, false),
89
+
90
+ },
91
+
92
+ /*
93
+ for group analytics keys, we need an array of arrays [[],[],[]]
94
+ each pair represents a group_key and the number of profiles for that key
95
+ */
96
+ groupKeys: [
97
+ ['company_id', 250],
98
+
99
+ ],
100
+ groupProps: {
101
+ company_id: {
102
+ $name: () => { return chance.company(); },
103
+ $email: () => { return `CSM ${chance.pickone(["AK", "Jessica", "Michelle", "Dana", "Brian", "Dave"])}`; },
104
+ "# of employees": weightedRange(3, 10000),
105
+ "sector": ["tech", "finance", "healthcare", "education", "government", "non-profit"],
106
+ "segment": ["enterprise", "SMB", "mid-market"],
107
+ }
108
+ },
109
+
110
+ lookupTables: [
111
+ {
112
+ key: "product_id",
113
+ entries: 1000,
114
+ attributes: {
115
+ category: [
116
+ "Books",
117
+ "Movies",
118
+ "Music",
119
+ "Games",
120
+ "Electronics",
121
+ "Computers",
122
+ "Smart Home",
123
+ "Home",
124
+ "Garden & Tools",
125
+ "Pet Supplies",
126
+ "Food & Grocery",
127
+ "Beauty",
128
+ "Health",
129
+ "Toys",
130
+ "Kids",
131
+ "Baby",
132
+ "Handmade",
133
+ "Sports",
134
+ "Outdoors",
135
+ "Automotive",
136
+ "Industrial",
137
+ "Entertainment",
138
+ "Art"
139
+ ],
140
+ "demand": ["high", "medium", "medium", "low"],
141
+ "supply": ["high", "medium", "medium", "low"],
142
+ "manufacturer": chance.company.bind(chance),
143
+ "price": weightedRange(5, 500, 1000, .25),
144
+ "rating": weightedRange(1, 5),
145
+ "reviews": weightedRange(0, 35)
146
+ }
147
+
148
+ }
149
+ ],
150
+ };
151
+
152
+ module.exports = config;
package/index.js ADDED
@@ -0,0 +1,395 @@
1
+ /*
2
+ make fake mixpanel data easily!
3
+ by AK
4
+ ak@mixpanel.com
5
+ */
6
+
7
+ const mp = require("mixpanel-import");
8
+ const path = require("path");
9
+ const Chance = require("chance");
10
+ const chance = new Chance();
11
+ const { touch, mkdir, comma, bytesHuman } = require("ak-tools");
12
+ const Papa = require("papaparse");
13
+ const {
14
+ integer,
15
+ pick,
16
+ weightedRange,
17
+ progress,
18
+ person,
19
+ choose,
20
+ openFinder
21
+ } = require("./utils.js");
22
+ const dayjs = require("dayjs");
23
+ const utc = require("dayjs/plugin/utc");
24
+ const cliParams = require('./cli.js');
25
+
26
+
27
+ dayjs.extend(utc);
28
+ Array.prototype.pickOne = pick;
29
+ const now = dayjs().unix();
30
+ const dayInSec = 86400;
31
+
32
+
33
+
34
+ const args = cliParams();
35
+ const {
36
+ token,
37
+ seed,
38
+ format,
39
+ numDays,
40
+ numUsers,
41
+ numEvents,
42
+ region
43
+ } = args;
44
+ const suppliedConfig = args._[0];
45
+
46
+ //if the user specifics an separate config file
47
+ let config = null;
48
+ if (suppliedConfig) {
49
+ console.log(`using ${suppliedConfig} for data\n`);
50
+ config = require(path.resolve(suppliedConfig));
51
+ } else {
52
+ console.log(`... using default configuration ...\n`);
53
+ config = require(path.resolve("./default.js"));
54
+ }
55
+
56
+ //override config with cli params
57
+ if (token) config.token = token;
58
+ if (seed) config.seed = seed;
59
+ if (format) config.format = format;
60
+ if (numDays) config.numDays = numDays;
61
+ if (numUsers) config.numUsers = numUsers;
62
+ if (numEvents) config.numEvents = numEvents;
63
+ if (region) config.region = region;
64
+
65
+
66
+ //our main program
67
+ async function main(config) {
68
+
69
+ const {
70
+ seed = "every time a rug is micturated upon in this fair city...",
71
+ numEvents = 100000,
72
+ numUsers = 1000,
73
+ numDays = 30,
74
+ events = [{ event: "foo" }, { event: "bar" }, { event: "baz" }],
75
+ superProps = { platform: ["web", "iOS", "Android"] },
76
+ userProps = {
77
+ favoriteColor: ["red", "green", "blue", "yellow"],
78
+ spiritAnimal: chance.animal,
79
+ },
80
+ scdProps = { NPS: weightedRange(0, 10, 150, 1.6) },
81
+ groupKeys = [],
82
+ groupProps = {},
83
+ lookupTables = [],
84
+ format = "csv",
85
+ token = null,
86
+ region = "US",
87
+ } = config;
88
+
89
+ const uuidChance = new Chance(seed);
90
+
91
+ //the function which generates $distinct_id
92
+ function uuid() {
93
+ const distinct_id = uuidChance.guid();
94
+ const daysAgoBorn = chance.integer({ min: 1, max: numDays });
95
+ return {
96
+ distinct_id,
97
+ ...person(daysAgoBorn),
98
+ };
99
+ }
100
+
101
+ // weigh events for random selection
102
+ const weightedEvents = events
103
+ .reduce((acc, event) => {
104
+ for (let i = 0; i < event.weight; i++) {
105
+ acc.push(event);
106
+ }
107
+ return acc;
108
+ }, [])
109
+ .filter((e) => e.weight > 0)
110
+ .filter((e) => !e.isFirstEvent);
111
+
112
+ const firstEvents = events.filter((e) => e.isFirstEvent);
113
+ const eventData = [];
114
+ const userProfilesData = [];
115
+ const scdTableData = [];
116
+ const groupProfilesData = [];
117
+ const lookupTableData = [];
118
+ const avgEvPerUser = Math.floor(numEvents / numUsers);
119
+
120
+ //user loop
121
+ for (let i = 1; i < numUsers + 1; i++) {
122
+ progress("users", i);
123
+ const user = uuid();
124
+ const { distinct_id, $created } = user;
125
+ userProfilesData.push(makeProfile(userProps, user));
126
+ const mutations = chance.integer({ min: 1, max: 20 });
127
+ scdTableData.push(makeSCD(scdProps, distinct_id, mutations, $created));
128
+ const numEventsThisUser = Math.round(
129
+ chance.normal({ mean: avgEvPerUser, dev: avgEvPerUser / 4 })
130
+ );
131
+
132
+ if (firstEvents) {
133
+ eventData.push(
134
+ makeEvent(
135
+ distinct_id,
136
+ dayjs($created).unix(),
137
+ firstEvents,
138
+ superProps,
139
+ groupKeys,
140
+ true
141
+ )
142
+ );
143
+ }
144
+
145
+ //event loop
146
+ for (let j = 0; j < numEventsThisUser; j++) {
147
+ eventData.push(
148
+ makeEvent(
149
+ distinct_id,
150
+ dayjs($created).unix(),
151
+ weightedEvents,
152
+ superProps,
153
+ groupKeys
154
+ )
155
+ );
156
+ }
157
+ }
158
+ console.log("\n");
159
+
160
+ // make group profiles
161
+ for (const groupPair of groupKeys) {
162
+ const groupKey = groupPair[0];
163
+ const groupCardinality = groupPair[1];
164
+ const groupProfiles = [];
165
+ for (let i = 1; i < groupCardinality + 1; i++) {
166
+ progress("groups", i);
167
+ const group = {
168
+ [groupKey]: i,
169
+ ...makeProfile(groupProps[groupKey]),
170
+ $distinct_id: i
171
+ };
172
+ groupProfiles.push(group);
173
+ }
174
+ groupProfilesData.push({ key: groupKey, data: groupProfiles });
175
+ }
176
+ console.log("\n");
177
+
178
+ // make lookup tables
179
+ for (const lookupTable of lookupTables) {
180
+ const { key, entries, attributes } = lookupTable;
181
+ const data = [];
182
+ for (let i = 1; i < entries + 1; i++) {
183
+ progress("lookups", i);
184
+ const item = {
185
+ [key]: i,
186
+ ...makeProfile(attributes),
187
+ };
188
+ data.push(item);
189
+ }
190
+ lookupTableData.push({ key, data });
191
+ }
192
+ const { eventFiles, userFiles, scdFiles, groupFiles, lookupFiles, folder } = buildFileNames(config);
193
+ const pairs = [
194
+ [eventFiles, eventData],
195
+ [userFiles, userProfilesData],
196
+ [scdFiles, scdTableData],
197
+ [groupFiles, groupProfilesData],
198
+ [lookupFiles, lookupTableData],
199
+ ];
200
+ console.log("\n");
201
+ //write the files
202
+ for (const pair of pairs) {
203
+ const [paths, data] = pair;
204
+ for (const path of paths) {
205
+ if (format === "csv") {
206
+ console.log(`writing ${path}`);
207
+ const csv = Papa.unparse(data, {});
208
+ await touch(path, csv);
209
+ console.log(`\tdone\n`);
210
+ } else {
211
+ await touch(path, data, true);
212
+ }
213
+ }
214
+ }
215
+
216
+ const importResults = { events: {}, users: {}, groups: [] };
217
+ /** @type {import('mixpanel-import').Creds} */
218
+ const creds = { token };
219
+ /** @type {import('mixpanel-import').Options} */
220
+ const importOpts = {
221
+ fixData: true,
222
+ verbose: false,
223
+ forceStream: true,
224
+ strict: false,
225
+ dryRun: false,
226
+ abridged: false,
227
+ region
228
+
229
+ };
230
+ //send to mixpanel
231
+ if (token) {
232
+ if (eventData) {
233
+ console.log(`importing events to mixpanel...`);
234
+ const imported = await mp(creds, eventData, { recordType: "event", ...importOpts });
235
+ console.log(`\tsent ${comma(imported.success)} events\n`);
236
+ importResults.events = imported;
237
+ }
238
+ if (userProfilesData) {
239
+ console.log(`importing user profiles to mixpanel...`);
240
+ const imported = await mp(creds, userProfilesData, { recordType: "user", ...importOpts });
241
+ console.log(`\tsent ${comma(imported.success)} user profiles\n`);
242
+ importResults.users = imported;
243
+ }
244
+ if (groupProfilesData) {
245
+
246
+ for (const groupProfiles of groupProfilesData) {
247
+ const groupKey = groupProfiles.key;
248
+ const data = groupProfiles.data;
249
+ console.log(`importing ${groupKey} profiles to mixpanel...`);
250
+ const imported = await mp({ token, groupKey }, data, { recordType: "group", ...importOpts });
251
+ console.log(`\tsent ${comma(imported.success)} ${groupKey} profiles\n`);
252
+ importResults.groups.push(imported);
253
+ }
254
+ }
255
+ console.log(`\n\n`);
256
+ }
257
+ return {import: importResults, files: [ eventFiles, userFiles, scdFiles, groupFiles, lookupFiles, folder ]};
258
+ }
259
+
260
+ function makeProfile(props, defaults) {
261
+ //build the spec
262
+ const profile = {
263
+ ...defaults,
264
+ };
265
+
266
+ for (const key in props) {
267
+ try {
268
+ profile[key] = choose(props[key]);
269
+ } catch (e) {
270
+ // debugger;
271
+ }
272
+ }
273
+
274
+ return profile;
275
+ }
276
+
277
+ function makeSCD(props, distinct_id, mutations, $created) {
278
+ const scdEntries = [];
279
+ let lastInserted = dayjs($created);
280
+ const deltaDays = dayjs().diff(lastInserted, "day");
281
+
282
+ for (let i = 0; i < mutations; i++) {
283
+ if (lastInserted.isAfter(dayjs())) break;
284
+ const scd = makeProfile(props, { distinct_id });
285
+ scd.startTime = lastInserted.toISOString();
286
+ lastInserted = lastInserted.add(integer(1, 1000), "seconds");
287
+ scd.insertTime = lastInserted.toISOString();
288
+ scdEntries.push({ ...scd });
289
+ lastInserted = lastInserted
290
+ .add(integer(0, deltaDays), "day")
291
+ .subtract(integer(1, 1000), "seconds");
292
+ }
293
+
294
+ return scdEntries;
295
+ }
296
+
297
+ function makeEvent(distinct_id, earliestTime, events, superProps, groupKeys, isFirstEvent = false) {
298
+ let chosenEvent = events.pickOne();
299
+ if (typeof chosenEvent === "string")
300
+ chosenEvent = { event: chosenEvent, properties: {} };
301
+ const event = {
302
+ event: chosenEvent.event,
303
+ distinct_id,
304
+ $source: "AK's fake data generator",
305
+ };
306
+
307
+ if (isFirstEvent) event.time = earliestTime;
308
+ if (!isFirstEvent) event.time = integer(earliestTime, now);
309
+
310
+ const props = { ...chosenEvent.properties, ...superProps };
311
+
312
+ //iterate through custom properties
313
+ for (const key in props) {
314
+ try {
315
+ event[key] = choose(props[key]);
316
+ } catch (e) {
317
+ debugger;
318
+ }
319
+ }
320
+
321
+ //iterate through groups
322
+ for (const groupPair of groupKeys) {
323
+ const groupKey = groupPair[0];
324
+ const groupCardinality = groupPair[1];
325
+ event[groupKey] = weightedRange(1, groupCardinality).pickOne();
326
+ }
327
+
328
+ return event;
329
+ }
330
+
331
+ function buildFileNames(config) {
332
+ const { format = "csv", groupKeys = [], lookupTables = [] } = config;
333
+ const extension = format === "csv" ? "csv" : "json";
334
+ const current = dayjs.utc().format("MM-DD-HH");
335
+ const cwd = path.resolve("./");
336
+ const dataDir = path.join(cwd, "data");
337
+ const writeDir = mkdir(dataDir);
338
+
339
+ const writePaths = {
340
+ eventFiles: [path.join(writeDir, `events-${current}.${extension}`)],
341
+ userFiles: [path.join(writeDir, `users-${current}.${extension}`)],
342
+ scdFiles: [path.join(writeDir, `scd-${current}.${extension}`)],
343
+ groupFiles: [],
344
+ lookupFiles: [],
345
+ folder: writeDir,
346
+ };
347
+
348
+ for (const groupPair of groupKeys) {
349
+ const groupKey = groupPair[0];
350
+ writePaths.groupFiles.push(
351
+ path.join(writeDir, `group-${groupKey}-${current}.${extension}`)
352
+ );
353
+ }
354
+
355
+ for (const lookupTable of lookupTables) {
356
+ const { key } = lookupTable;
357
+ writePaths.lookupFiles.push(
358
+ path.join(writeDir, `lookup-${key}-${current}.${extension}`)
359
+ );
360
+ }
361
+
362
+ return writePaths;
363
+ }
364
+
365
+ //that's all folks :)
366
+ main(config)
367
+ .then((data) => {
368
+ console.log(`------------------SUMMARY------------------`);
369
+ const { events, groups, users } = data.import;
370
+ const files = data.files;
371
+ const folder = files.pop()
372
+ const groupBytes = groups.reduce((acc, group) => { return acc + group.bytes; }, 0);
373
+ const groupSuccess = groups.reduce((acc, group) => { return acc + group.success; }, 0);
374
+ const bytes = events.bytes + groupBytes + users.bytes;
375
+ const stats = {
376
+ events: comma(events.success || 0),
377
+ users: comma(users.success || 0),
378
+ groups: comma(groupSuccess || 0),
379
+ bytes: bytesHuman(bytes || 0)
380
+ };
381
+ console.table(stats);
382
+ console.log(`\nfiles written to ${folder}...`);
383
+ console.log("\t" + files.flat().join('\t\n'));
384
+ console.log(`------------------SUMMARY------------------\n\n\n`);
385
+ })
386
+ .catch((e) => {
387
+ console.log(`------------------ERROR------------------`);
388
+ console.error(e);
389
+ console.log(`------------------ERROR------------------`);
390
+ debugger;
391
+ })
392
+ .finally(() => {
393
+ console.log('have a wonderful day :)');
394
+ openFinder(path.resolve("./data"));
395
+ });
package/package.json ADDED
@@ -0,0 +1,41 @@
1
+ {
2
+ "name": "make-mp-data",
3
+ "version": "1.0.0",
4
+ "description": "builds all mixpanel primitives for a given project",
5
+ "main": "index.js",
6
+ "scripts": {
7
+ "start": "node index.js",
8
+ "prune": "rm ./data/*",
9
+ "go": "sh ./go.sh",
10
+ "post": "npm publish"
11
+ },
12
+ "repository": {
13
+ "type": "git",
14
+ "url": "git+https://github.com/ak--47/make-mp-data.git"
15
+ },
16
+ "keywords": [
17
+ "mixpanel",
18
+ "stream",
19
+ "analytics",
20
+ "tracking",
21
+ "server",
22
+ "CLI",
23
+ "datamart",
24
+ "scd 2",
25
+ "dummy data",
26
+ "fake data"
27
+ ],
28
+ "author": "ak@mixpanel.com",
29
+ "license": "ISC",
30
+ "bugs": {
31
+ "url": "https://github.com/ak--47/make-mp-data/issues"
32
+ },
33
+ "homepage": "https://github.com/ak--47/make-mp-data#readme",
34
+ "dependencies": {
35
+ "ak-tools": "^1.0.51",
36
+ "chance": "^1.1.7",
37
+ "dayjs": "^1.11.10",
38
+ "mixpanel-import": "^2.5.33",
39
+ "yargs": "^17.7.2"
40
+ }
41
+ }
package/utils.js ADDED
@@ -0,0 +1,226 @@
1
+ const Chance = require('chance');
2
+ const chance = new Chance();
3
+ const readline = require('readline');
4
+ const { comma } = require('ak-tools');
5
+ const { spawn } = require('child_process');
6
+ const dayjs = require('dayjs');
7
+ const utc = require('dayjs/plugin/utc');
8
+ dayjs.extend(utc);
9
+
10
+ function pick() {
11
+ const choice = chance.pickone(this);
12
+ return choice;
13
+ }
14
+
15
+ function date(inTheLast = 30, isPast = true, format = 'YYYY-MM-DD') {
16
+ const now = dayjs.utc();
17
+ return function () {
18
+ const when = chance.integer({ min: 0, max: inTheLast });
19
+ let then;
20
+ if (isPast) then = now.subtract(when, 'day');
21
+ if (!isPast) then = now.add(when, 'day');
22
+ if (format) return then.format(format);
23
+ if (!format) return then.toISOString();
24
+ };
25
+ }
26
+
27
+ function dates(inTheLast = 30, numPairs = 5, format = 'YYYY-MM-DD') {
28
+ const pairs = [];
29
+ for (let i = 0; i < numPairs; i++) {
30
+ pairs.push([date(inTheLast, format), date(inTheLast, format)]);
31
+ }
32
+ return pairs;
33
+
34
+ }
35
+
36
+ function day(start, end) {
37
+ const format = 'YYYY-MM-DD';
38
+ return function (min, max) {
39
+ start = dayjs(start);
40
+ end = dayjs(end);
41
+ const diff = end.diff(start, 'day');
42
+ const delta = chance.integer({ min: min, max: diff });
43
+ const day = start.add(delta, 'day');
44
+ return {
45
+ start: start.format(format),
46
+ end: end.format(format),
47
+ day: day.format(format)
48
+ };
49
+ };
50
+
51
+ }
52
+
53
+ function choose(value) {
54
+ if (typeof value === 'function') {
55
+ return value();
56
+ }
57
+ if (Array.isArray(value)) {
58
+ return chance.pickone(value);
59
+ }
60
+
61
+ return value;
62
+ }
63
+
64
+ function exhaust(arr) {
65
+ return function () {
66
+ return arr.shift();
67
+ };
68
+ }
69
+
70
+
71
+ function integer(min, max) {
72
+ if (min > max) {
73
+ return chance.integer({
74
+ min: max,
75
+ max: min
76
+ });
77
+ }
78
+
79
+ return chance.integer({
80
+ min: min,
81
+ max: max
82
+ });
83
+ }
84
+
85
+ function makeProducts() {
86
+ 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"];
87
+ let slugs = ['/sale/', '/featured/', '/home/', '/search/', '/wishlist/', '/'];
88
+ let assetExtension = ['.png', '.jpg', '.jpeg', '.heic', '.mp4', '.mov', '.avi'];
89
+ let data = [];
90
+ let numOfItems = integer(1, 12);
91
+
92
+ for (var i = 0; i < numOfItems; i++) {
93
+
94
+ let category = chance.pickone(categories);
95
+ let slug = chance.pickone(slugs);
96
+ let asset = chance.pickone(assetExtension);
97
+ let product_id = chance.guid();
98
+ let price = integer(1, 300);
99
+ let quantity = integer(1, 5);
100
+
101
+ let item = {
102
+ product_id: product_id,
103
+ sku: integer(11111, 99999),
104
+ amount: price,
105
+ quantity: quantity,
106
+ value: price * quantity,
107
+ featured: chance.pickone([true, false]),
108
+ category: category,
109
+ urlSlug: slug + category,
110
+ asset: `${category}-${integer(1, 20)}${asset}`
111
+ };
112
+
113
+ data.push(item);
114
+ }
115
+
116
+ return data;
117
+ }
118
+
119
+ // Box-Muller transform to generate standard normally distributed values
120
+ function boxMullerRandom() {
121
+ let u = 0, v = 0;
122
+ while (u === 0) u = Math.random();
123
+ while (v === 0) v = Math.random();
124
+ return Math.sqrt(-2.0 * Math.log(u)) * Math.cos(2.0 * Math.PI * v);
125
+ }
126
+
127
+ // Apply skewness to the value
128
+ function applySkew(value, skew) {
129
+ if (skew === 1) return value;
130
+ // Adjust the value based on skew
131
+ let sign = value < 0 ? -1 : 1;
132
+ return sign * Math.pow(Math.abs(value), skew);
133
+ }
134
+
135
+ // Map standard normal value to our range
136
+ function mapToRange(value, mean, sd) {
137
+ return Math.round(value * sd + mean);
138
+ }
139
+
140
+ function weightedRange(min, max, size = 100, skew = 1) {
141
+ const mean = (max + min) / 2;
142
+ const sd = (max - min) / 4;
143
+ let array = [];
144
+
145
+ for (let i = 0; i < size; i++) {
146
+ let normalValue = boxMullerRandom();
147
+ let skewedValue = applySkew(normalValue, skew);
148
+ let mappedValue = mapToRange(skewedValue, mean, sd);
149
+
150
+ // Ensure the mapped value is within our min-max range
151
+ if (mappedValue >= min && mappedValue <= max) {
152
+ array.push(mappedValue);
153
+ } else {
154
+ i--; // If out of range, redo this iteration
155
+ }
156
+ }
157
+
158
+ return array;
159
+ }
160
+
161
+ function progress(thing, p) {
162
+ readline.cursorTo(process.stdout, 0);
163
+ process.stdout.write(`${thing} processed ... ${comma(p)}`);
164
+ }
165
+
166
+
167
+ function person(bornDaysAgo = 30) {
168
+ //names and photos
169
+ const gender = chance.pickone(['male', 'female']);
170
+ const first = chance.first({ gender });
171
+ const last = chance.last();
172
+ const $name = `${first} ${last}`;
173
+ const $email = `${first[0]}.${last}@${chance.domain()}.com`;
174
+ const avatarPrefix = `https://randomuser.me/api/portraits`;
175
+ const randomAvatarNumber = chance.integer({
176
+ min: 1,
177
+ max: 99
178
+ });
179
+ const avPath = gender === 'male' ? `/men/${randomAvatarNumber}.jpg` : `/women/${randomAvatarNumber}.jpg`;
180
+ const $avatar = avatarPrefix + avPath;
181
+ const $created = date(bornDaysAgo, true, null)();
182
+
183
+ return {
184
+ $name,
185
+ $email,
186
+ $avatar,
187
+ $created
188
+ };
189
+ }
190
+
191
+ function range(a, b, step = 1) {
192
+ step = !step ? 1 : step;
193
+ b = b / step;
194
+ for (var i = a; i <= b; i++) {
195
+ this.push(i * step);
196
+ }
197
+ return this;
198
+ };
199
+
200
+
201
+ //helper to open the finder
202
+ function openFinder(path, callback) {
203
+ path = path || '/';
204
+ let p = spawn('open', [path]);
205
+ p.on('error', (err) => {
206
+ p.kill();
207
+ return callback(err);
208
+ });
209
+ }
210
+
211
+
212
+
213
+ module.exports = {
214
+ weightedRange,
215
+ pick,
216
+ day,
217
+ integer,
218
+ makeProducts,
219
+ date,
220
+ progress,
221
+ person,
222
+ choose,
223
+ range,
224
+ exhaust,
225
+ openFinder
226
+ };