make-mp-data 3.0.6 → 3.1.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.
@@ -0,0 +1,428 @@
1
+ /**
2
+ * This is the default configuration file for the data generator in COMPLEX mode
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
+ import Chance from 'chance';
10
+ const chance = new Chance();
11
+ import dayjs from "dayjs";
12
+ import utc from "dayjs/plugin/utc.js";
13
+ dayjs.extend(utc);
14
+ import { weighNumRange, date, integer } from "../lib/utils/utils.js";
15
+ import * as u from 'ak-tools';
16
+
17
+ /** @type {import('../types.js').Dungeon} */
18
+ const config = {
19
+ token: "",
20
+ seed: "quite complexus",
21
+ numDays: 30, //how many days worth of data
22
+ numEvents: 100_000, //how many events
23
+ numUsers: 1000, //how many users
24
+ format: 'json', //csv or json
25
+ region: "US",
26
+ hasAnonIds: true, //if true, anonymousIds are created for each user
27
+ hasSessionIds: true, //if true, hasSessionIds are created for each user
28
+
29
+ hasLocation: true,
30
+ hasAndroidDevices: true,
31
+ hasIOSDevices: true,
32
+ hasDesktopDevices: true,
33
+ hasBrowser: true,
34
+ hasCampaigns: true,
35
+ isAnonymous: false,
36
+ hasAdSpend: true,
37
+
38
+ hasAvatar: true,
39
+
40
+ batchSize: 500_000,
41
+ concurrency: 1,
42
+
43
+ funnels: [],
44
+ events: [
45
+ {
46
+ "event": "checkout",
47
+ "weight": 2,
48
+ "properties": {
49
+ amount: weighNumRange(5, 500, .25),
50
+ currency: ["USD", "USD", "USD", "CAD", "EUR", "EUR", "BTC", "BTC", "ETH", "JPY"],
51
+ cart: makeProducts(12),
52
+ }
53
+ },
54
+ {
55
+ "event": "add to cart",
56
+ "weight": 4,
57
+ "properties": {
58
+ amount: weighNumRange(5, 500, .25),
59
+ qty: integer(1, 5),
60
+ product_id: weighNumRange(1, 1000, 1.4)
61
+ }
62
+ },
63
+ {
64
+ "event": "page view",
65
+ "weight": 10,
66
+ "properties": {
67
+ page: ["/", "/", "/", "/learn-more", "/pricing", "/contact", "/about", "/careers", "/sign-up", "/login", "/app", "/app", "/app", "/app"],
68
+ utm_source: ["$organic", "$organic", "$organic", "$organic", "google", "google", "google", "facebook", "facebook", "twitter", "linkedin"],
69
+ }
70
+ },
71
+ {
72
+ "event": "watch video",
73
+ "weight": 8,
74
+ "properties": {
75
+ category: ["funny", "educational", "inspirational", "music", "news", "sports", "cooking", "DIY", "travel", "gaming"],
76
+ hashTags: makeHashTags,
77
+ watchTimeSec: weighNumRange(10, 600, .25,),
78
+ quality: ["2160p", "1440p", "1080p", "720p", "480p", "360p", "240p"],
79
+ format: ["mp4", "avi", "mov", "mpg"],
80
+ video_id: weighNumRange(1, 50000, 1.4),
81
+
82
+ }
83
+ },
84
+ {
85
+ "event": "comment",
86
+ "weight": 2,
87
+ "properties": {
88
+ length: weighNumRange(1, 500, .25),
89
+ video_id: weighNumRange(1, 50000, 1.4),
90
+ has_replies: [true, false, false, false, false],
91
+ has_photo: [true, false, false, false, false],
92
+
93
+ }
94
+ },
95
+ {
96
+ "event": "save video",
97
+ "weight": 4,
98
+ "properties": {
99
+ video_id: weighNumRange(1, 50000, 1.4),
100
+ ui_control: ["toolbar", "menu", "keyboard"]
101
+
102
+
103
+ }
104
+ },
105
+ {
106
+ "event": "view item",
107
+ "weight": 8,
108
+ "properties": {
109
+ product_id: weighNumRange(1, 24, 3),
110
+ colors: ["light", "dark", "custom", "dark"]
111
+ }
112
+ },
113
+ {
114
+ "event": "save item",
115
+ "weight": 5,
116
+ "properties": {
117
+ product_id: weighNumRange(1, 1000, 12),
118
+ colors: ["light", "dark", "custom", "dark"]
119
+ }
120
+ },
121
+ {
122
+ "event": "support ticket",
123
+ "weight": 2,
124
+ "properties": {
125
+ product_id: weighNumRange(1, 1000, .6),
126
+ description: chance.sentence.bind(chance),
127
+ severity: ["low", "medium", "high"],
128
+ ticket_id: chance.guid.bind(chance)
129
+ }
130
+ },
131
+ {
132
+ "event": "sign up",
133
+ "isFirstEvent": true,
134
+ "weight": 0,
135
+ "properties": {
136
+ plan: ["free", "free", "free", "free", "basic", "basic", "basic", "premium", "premium", "enterprise"],
137
+ dateOfRenewal: date(100, false),
138
+ codewords: u.makeName,
139
+ }
140
+ }
141
+ ],
142
+ superProps: {
143
+ linked_device: deviceAttributes()
144
+ // emotions: generateEmoji(),
145
+
146
+ },
147
+ /*
148
+ user properties work the same as event properties
149
+ each key should be an array or function reference
150
+ */
151
+ userProps: {
152
+ title: chance.profession.bind(chance),
153
+ luckyNumber: weighNumRange(42, 420),
154
+ experiment: designExperiment(),
155
+ spiritAnimal: ["unicorn", "dragon", "phoenix", "sasquatch", "yeti", "kraken", "jackalope", "thunderbird", "mothman", "nessie", "chupacabra", "jersey devil", "bigfoot", "weindgo", "bunyip", "mokele-mbembe", "tatzelwurm", "megalodon"],
156
+ timezone: chance.timezone.bind(chance), // ["America/New_York", "America/Los_Angeles", "America/Chicago", "America/Denver", "America/Phoenix", "America/Anchorage", "Pacific/Honolulu"]
157
+ ip: chance.ip.bind(chance),
158
+ lastCart: makeProducts(5),
159
+
160
+ },
161
+
162
+ /** each generates it's own table */
163
+ scdProps: {
164
+ role: {
165
+ type: "user",
166
+ frequency: "week",
167
+ values: ["admin", "collaborator", "user", "view only", "no access"],
168
+ timing: 'fuzzy',
169
+ max: 10
170
+ },
171
+ NPS: {
172
+ type: "user",
173
+ frequency: "day",
174
+ values: weighNumRange(1, 10, 2, 150),
175
+ timing: 'fuzzy',
176
+ max: 10
177
+ },
178
+ MRR: {
179
+ type: "company_id",
180
+ frequency: "month",
181
+ values: weighNumRange(0, 10000, .15),
182
+ timing: 'fixed',
183
+ max: 10
184
+ },
185
+ AccountHealthScore: {
186
+ type: "company_id",
187
+ frequency: "week",
188
+ values: weighNumRange(1, 10, .15),
189
+ timing: 'fixed',
190
+ max: 40
191
+ },
192
+ plan: {
193
+ type: "company_id",
194
+ frequency: "month",
195
+ values: ["free", "basic", "premium", "enterprise"],
196
+ timing: 'fixed',
197
+ max: 10
198
+ }
199
+ },
200
+
201
+ mirrorProps: {
202
+ isBot: { events: "*", values: [false, false, false, false, true] },
203
+ profit: { events: ["checkout"], values: [4, 2, 42, 420] },
204
+ watchTimeSec: {
205
+ events: ["watch video"],
206
+ values: weighNumRange(50, 1200, 2)
207
+ }
208
+ },
209
+
210
+ /*
211
+ for group analytics keys, we need an array of arrays [[],[],[]]
212
+ each pair represents a group_key and the number of profiles for that key
213
+ */
214
+ groupKeys: [
215
+ ['company_id', 500, []],
216
+ ['room_id', 10000, ["save video", "comment", "watch video"]],
217
+
218
+ ],
219
+ groupProps: {
220
+ company_id: {
221
+ name: () => { return chance.company(); },
222
+ email: () => { return `CSM: ${chance.pickone(["AK", "Jessica", "Michelle", "Dana", "Brian", "Dave"])}`; },
223
+ "# of employees": weighNumRange(3, 10000),
224
+ "industry": ["tech", "finance", "healthcare", "education", "government", "non-profit"],
225
+ "segment": ["enterprise", "SMB", "mid-market"],
226
+ "products": [["core"], ["core"], ["core", "add-ons"], ["core", "pro-serve"], ["core", "add-ons", "pro-serve"], ["core", "BAA", "enterprise"], ["free"], ["free"], ["free", "addons"]],
227
+ },
228
+ room_id: {
229
+ name: () => { return `#${chance.word({ length: integer(4, 24), capitalize: true })}`; },
230
+ email: ["public", "private"],
231
+ "room provider": ["partner", "core", "core", "core"],
232
+ "room capacity": weighNumRange(3, 1000000),
233
+ "isPublic": [true, false, false, false, false],
234
+ "country": chance.country.bind(chance),
235
+ "isVerified": [true, true, false, false, false],
236
+ }
237
+ },
238
+ groupEvents: [{
239
+ attribute_to_user: false,
240
+ event: "card charged",
241
+ weight: 1,
242
+ frequency: 30,
243
+ group_key: "company_id",
244
+ group_size: 500,
245
+ properties: {
246
+ amount: weighNumRange(5, 500, .25),
247
+ currency: ["USD", "USD", "USD", "CAD", "EUR", "EUR", "BTC", "BTC", "ETH", "JPY"],
248
+ plan: ["basic", "premium", "enterprise"],
249
+ "payment method": []
250
+ }
251
+ }],
252
+
253
+ lookupTables: [
254
+ {
255
+ key: "product_id",
256
+ entries: 1000,
257
+ attributes: {
258
+ category: [
259
+ "Books", "Movies", "Music", "Games", "Electronics", "Computers", "Smart Home", "Home", "Garden & Tools", "Pet Supplies", "Food & Grocery", "Beauty", "Health", "Toys", "Kids", "Baby", "Handmade", "Sports", "Outdoors", "Automotive", "Industrial", "Entertainment", "Art"
260
+ ],
261
+ "demand": ["high", "medium", "medium", "low"],
262
+ "supply": ["high", "medium", "medium", "low"],
263
+ "manufacturer": chance.company.bind(chance),
264
+ "price": weighNumRange(5, 500, .25),
265
+ "rating": weighNumRange(1, 5),
266
+ "reviews": weighNumRange(0, 35)
267
+ }
268
+
269
+ },
270
+ {
271
+ key: "video_id",
272
+ entries: 50000,
273
+ attributes: {
274
+ isFlagged: [true, false, false, false, false],
275
+ copyright: ["all rights reserved", "creative commons", "creative commons", "public domain", "fair use"],
276
+ uploader_id: chance.guid.bind(chance),
277
+ "uploader influence": ["low", "low", "low", "medium", "medium", "high"],
278
+ thumbs: weighNumRange(0, 35),
279
+ rating: ["G", "PG", "PG-13", "R", "NC-17", "PG-13", "R", "NC-17", "R", "PG", "PG"]
280
+ }
281
+
282
+ }
283
+ ],
284
+
285
+ hook: function (record, type, meta) {
286
+ // event hook: weekend watch time boost — videos watched on weekends get 1.5x duration
287
+ if (type === "event") {
288
+ if (record.event === "watch video" && record.time) {
289
+ const day = dayjs(record.time).day();
290
+ if (day === 0 || day === 6) {
291
+ record.watchTimeSec = Math.round((record.watchTimeSec || 60) * 1.5);
292
+ record.is_weekend = true;
293
+ }
294
+ }
295
+ // support tickets on high-severity get escalated flag
296
+ if (record.event === "support ticket" && record.severity === "high") {
297
+ record.escalated = true;
298
+ }
299
+ }
300
+
301
+ // everything hook: simulate cart abandonment — users who "add to cart" but never "checkout" lose their last add-to-cart
302
+ if (type === "everything") {
303
+ const hasCheckout = record.some(e => e.event === "checkout");
304
+ const hasAddToCart = record.some(e => e.event === "add to cart");
305
+ if (hasAddToCart && !hasCheckout) {
306
+ // mark all their add-to-cart events as abandoned
307
+ for (const e of record) {
308
+ if (e.event === "add to cart") {
309
+ e.abandoned = true;
310
+ }
311
+ }
312
+ }
313
+ return record;
314
+ }
315
+
316
+ return record;
317
+ }
318
+ };
319
+
320
+
321
+
322
+ function makeHashTags() {
323
+ const possibleHashtags = [];
324
+ for (let i = 0; i < 20; i++) {
325
+ possibleHashtags.push('#' + u.makeName(2, ''));
326
+ }
327
+
328
+ const numHashtags = integer(integer(1, 5), integer(5, 10));
329
+ const hashtags = [];
330
+ for (let i = 0; i < numHashtags; i++) {
331
+ hashtags.push(chance.pickone(possibleHashtags));
332
+ }
333
+ return [hashtags];
334
+ };
335
+
336
+ function makeProducts(maxItems = 10) {
337
+
338
+ return function () {
339
+ const 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"];
340
+ const slugs = ['/sale/', '/featured/', '/home/', '/search/', '/wishlist/', '/'];
341
+ const assetExtension = ['.png', '.jpg', '.jpeg', '.heic', '.mp4', '.mov', '.avi'];
342
+ const data = [];
343
+ const numOfItems = integer(1, 12);
344
+
345
+ for (var i = 0; i < numOfItems; i++) {
346
+ const category = chance.pickone(categories);
347
+ const slug = chance.pickone(slugs);
348
+ const asset = chance.pickone(assetExtension);
349
+ const product_id = chance.guid();
350
+ const price = integer(1, 300);
351
+ const quantity = integer(1, 5);
352
+
353
+ const item = {
354
+ product_id: product_id,
355
+ sku: integer(11111, 99999),
356
+ amount: price,
357
+ quantity: quantity,
358
+ value: price * quantity,
359
+ featured: chance.pickone([true, false]),
360
+ category: category,
361
+ urlSlug: slug + category,
362
+ asset: `${category}-${integer(1, 20)}${asset}`
363
+ };
364
+
365
+ data.push(item);
366
+ }
367
+
368
+ return () => [data];
369
+ };
370
+ };
371
+
372
+
373
+ function designExperiment() {
374
+ return function () {
375
+ const variants = ["A", "B", "C", "Control"];
376
+ const variant = chance.pickone(variants);
377
+ const experiments = ["no password", "social sign in", "new tutorial", "new search"];
378
+ const experiment = chance.pickone(experiments);
379
+ const multi_variates = ["A/B", "A/B/C", "A/B/C/D", "Control"];
380
+ const multi_variate = chance.pickone(multi_variates);
381
+ const impression_id = chance.guid();
382
+
383
+
384
+
385
+ const chosen = {
386
+ variant,
387
+ experiment,
388
+ multi_variate,
389
+ impression_id
390
+ };
391
+
392
+ return [chosen];
393
+ };
394
+ }
395
+
396
+ function deviceAttributes(isMobile = false) {
397
+ return function () {
398
+ let devices = ["desktop", "laptop", "desktop", "laptop", "desktop", "laptop", "other"];
399
+ if (isMobile) devices = [...devices, "mobile", "mobile", "mobile", "tablet"];
400
+ const device = chance.pickone(devices);
401
+ let oses = ["Windows", "macOS", "Windows", "macOS", "macOS", "Linux", "Windows", "macOS", "Windows", "macOS", "macOS", "TempleOS"];
402
+ if (isMobile) oses = [...oses, "iOS", "Android", "iOS", "Android"];
403
+ const os = chance.pickone(oses);
404
+ const browser = chance.pickone(["Chrome", "Firefox", "Safari", "Edge", "Opera", "IE", "Brave", "Vivaldi"]);
405
+ const version = chance.integer({ min: 1, max: 15 });
406
+ const resolution = chance.pickone(["1920x1080", "1280x720", "1024x768", "800x600", "640x480"]);
407
+ const language = chance.pickone(["en-US", "en-US", "en-US", "en-GB", "es", "es", "fr", "de", "it", "ja", "zh", "ru"]);
408
+
409
+ const chosen = {
410
+ platform: device,
411
+ os,
412
+ browser,
413
+ version,
414
+ resolution,
415
+ language
416
+ };
417
+
418
+ return chosen;
419
+
420
+ };
421
+ }
422
+
423
+
424
+
425
+
426
+
427
+
428
+ export default config;
package/entry.js CHANGED
@@ -59,7 +59,7 @@ import getCliParams from './lib/cli/cli.js';
59
59
  process.exit(0);
60
60
  } catch (error) {
61
61
  console.error(`\nāŒ Job failed: ${error.message}`);
62
- if (process.env.NODE_ENV === 'development') {
62
+ if (process.env.NODE_ENV !== 'production') {
63
63
  console.error(error.stack);
64
64
  }
65
65
  process.exit(1);
package/index.js CHANGED
@@ -34,6 +34,8 @@ import dayjs from "dayjs";
34
34
  import utc from "dayjs/plugin/utc.js";
35
35
  import { timer } from 'ak-tools';
36
36
  import { existsSync } from 'fs';
37
+ import path from 'path';
38
+ import { pathToFileURL } from 'url';
37
39
  import { dataLogger as logger } from './lib/utils/logger.js';
38
40
 
39
41
  // Initialize dayjs and time constants
@@ -123,8 +125,11 @@ async function main(config) {
123
125
  console.log(`\nšŸ” Loading dungeon config from: ${firstArg}`);
124
126
  }
125
127
  try {
126
- const dungeonConfig = await import(firstArg);
127
- config = dungeonConfig.default || dungeonConfig;
128
+ const absolutePath = path.resolve(firstArg);
129
+ const dungeonConfig = await import(pathToFileURL(absolutePath).href);
130
+ const loaded = dungeonConfig.default || dungeonConfig;
131
+ // Merge: dungeon config provides defaults; CLI flags override
132
+ config = { ...loaded, ...config };
128
133
  } catch (error) {
129
134
  console.error(`\nāŒ Error loading dungeon config from ${firstArg}: ${error.message}`);
130
135
  throw error;
package/lib/cli/cli.js CHANGED
@@ -93,21 +93,18 @@ DATA MODEL: https://github.com/ak--47/make-mp-data/blob/main/default.js
93
93
  })
94
94
  .option("region", {
95
95
  demandOption: false,
96
- default: 'US',
97
96
  alias: 'r',
98
- describe: 'either US or EU or IN',
97
+ describe: 'either US or EU or IN (defaults to dungeon config or US)',
99
98
  type: 'string'
100
99
  })
101
100
  .option('concurrency', {
102
101
  alias: 'conn',
103
- default: 10,
104
102
  demandOption: false,
105
- describe: 'concurrency level for data generation',
103
+ describe: 'concurrency level for data generation (defaults to dungeon config)',
106
104
  type: 'number'
107
105
  })
108
106
  .options("complex", {
109
107
  demandOption: false,
110
- default: false,
111
108
  describe: 'use complex data model (model all entities)',
112
109
  alias: 'c',
113
110
  type: 'boolean',
@@ -115,7 +112,6 @@ DATA MODEL: https://github.com/ak--47/make-mp-data/blob/main/default.js
115
112
  })
116
113
  .options("simple", {
117
114
  demandOption: false,
118
- default: false,
119
115
  describe: 'use simple data model (basic events and users)',
120
116
  alias: 'simp',
121
117
  type: 'boolean',
@@ -123,7 +119,6 @@ DATA MODEL: https://github.com/ak--47/make-mp-data/blob/main/default.js
123
119
  })
124
120
  .options("sanity", {
125
121
  demandOption: false,
126
- default: false,
127
122
  describe: 'run sanity checks on the generated data',
128
123
  alias: 'san',
129
124
  type: 'boolean',
@@ -131,15 +126,13 @@ DATA MODEL: https://github.com/ak--47/make-mp-data/blob/main/default.js
131
126
  })
132
127
  .option("writeToDisk", {
133
128
  demandOption: false,
134
- default: true,
135
- describe: 'write data to disk',
129
+ describe: 'write data to disk (CLI defaults to true)',
136
130
  alias: 'w',
137
131
  type: 'boolean',
138
132
  coerce: boolCoerce
139
133
  })
140
134
  .option("hasSessionIds", {
141
135
  demandOption: false,
142
- default: false,
143
136
  describe: 'create session ids in the data',
144
137
  alias: 'sid',
145
138
  type: 'boolean',
@@ -147,7 +140,6 @@ DATA MODEL: https://github.com/ak--47/make-mp-data/blob/main/default.js
147
140
  })
148
141
  .option("hasAnonIds", {
149
142
  demandOption: false,
150
- default: false,
151
143
  describe: 'create anonymous ids in the data',
152
144
  alias: 'aid',
153
145
  type: 'boolean',
@@ -341,18 +341,15 @@ function transformSCDPropsWithoutCredentials(config) {
341
341
 
342
342
  // Missing credentials - handle based on job type
343
343
  if (!isUIJob) {
344
- // For programmatic/CLI usage, throw an error if trying to send SCDs to Mixpanel without credentials
345
- if (token) {
346
- throw new Error(
347
- 'Configuration error: SCD properties are configured but service credentials are missing.\n' +
348
- 'To import SCD data to Mixpanel, you must provide:\n' +
349
- ' - serviceAccount: Your Mixpanel service account username\n' +
350
- ' - serviceSecret: Your Mixpanel service account secret\n' +
351
- ' - projectId: Your Mixpanel project ID\n' +
352
- 'Without these credentials, SCD data cannot be imported to Mixpanel.'
344
+ // For programmatic/CLI usage: warn but continue. SCD files still generate;
345
+ // mixpanel-sender already gates SCD import on credential presence.
346
+ if (token && config.verbose !== false) {
347
+ console.warn(
348
+ 'āš ļø SCD properties configured but service credentials missing. ' +
349
+ 'SCD files will be generated but NOT imported to Mixpanel. ' +
350
+ 'Provide serviceAccount + serviceSecret + projectId to enable SCD import.'
353
351
  );
354
352
  }
355
- // If not sending to Mixpanel (no token), allow generation for testing
356
353
  return;
357
354
  }
358
355