make-mp-data 1.4.4 → 1.5.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/core/index.js DELETED
@@ -1,1008 +0,0 @@
1
- #! /usr/bin/env node
2
-
3
- /*
4
- make fake mixpanel data easily!
5
- by AK
6
- ak@mixpanel.com
7
- */
8
-
9
- //todo: churn ... is churnFunnel, possible to return, etc
10
- //todo: fixedTimeFunnel? if set this funnel will occur for all users at the same time ['cards charged', 'charge complete']
11
- //todo: send SCD data to mixpanel
12
- //todo: send and map lookup tables to mixpanel
13
-
14
- /** @typedef {import('../types').Config} Config */
15
- /** @typedef {import('../types').EventConfig} EventConfig */
16
- /** @typedef {import('../types').Funnel} Funnel */
17
- /** @typedef {import('../types').Person} Person */
18
- /** @typedef {import('../types').SCDTableRow} SCDTableRow */
19
- /** @typedef {import('../types').UserProfile} UserProfile */
20
- /** @typedef {import('../types').EventSpec} EventSpec */
21
-
22
- const dayjs = require("dayjs");
23
- const utc = require("dayjs/plugin/utc");
24
- dayjs.extend(utc);
25
- const NOW = dayjs('2024-02-02').unix(); //this is a FIXED POINT and we will shift it later
26
- global.NOW = NOW;
27
-
28
- const os = require("os");
29
- const path = require("path");
30
- const { comma, bytesHuman, makeName, md5, clone, tracker, uid } = require("ak-tools");
31
- const { generateLineChart } = require('./chart.js');
32
- const { version } = require('../package.json');
33
- const mp = require("mixpanel-import");
34
- const metrics = tracker("make-mp-data", "db99eb8f67ae50949a13c27cacf57d41", os.userInfo().username);
35
-
36
-
37
- const u = require("./utils.js");
38
- const getCliParams = require("./cli.js");
39
- const { campaigns, devices, locations } = require('./defaults.js');
40
-
41
- let VERBOSE = false;
42
- let isCLI = false;
43
- /** @type {Config} */
44
- let CONFIG;
45
- let CAMPAIGNS;
46
- let DEFAULTS;
47
- require('dotenv').config();
48
-
49
-
50
- function track(name, props, ...rest) {
51
- if (process.env.NODE_ENV === 'test') return;
52
- metrics(name, props, ...rest);
53
- }
54
-
55
-
56
-
57
- /**
58
- * generates fake mixpanel data
59
- * @param {Config} config
60
- */
61
- async function main(config) {
62
-
63
- //seed the random number generator
64
- // ^ this is critical; same seed = same data; seed can be passed in as an env var or in the config
65
- const seedWord = process.env.SEED || config.seed || "hello friend!";
66
- config.seed = seedWord;
67
- u.initChance(seedWord);
68
- const chance = u.getChance(); // ! this is the only safe way to get the chance instance
69
- let {
70
- seed,
71
- numEvents = 100000,
72
- numUsers = 1000,
73
- numDays = 30,
74
- epochStart = 0,
75
- epochEnd = dayjs().unix(),
76
- events = [{ event: "foo" }, { event: "bar" }, { event: "baz" }],
77
- superProps = { luckyNumber: [2, 2, 4, 4, 42, 42, 42, 2, 2, 4, 4, 42, 42, 42, 420] },
78
- funnels = [],
79
- userProps = {
80
- spiritAnimal: chance.animal.bind(chance),
81
- },
82
- scdProps = {},
83
- mirrorProps = {},
84
- groupKeys = [],
85
- groupProps = {},
86
- lookupTables = [],
87
- anonIds = false,
88
- sessionIds = false,
89
- format = "csv",
90
- token = null,
91
- region = "US",
92
- writeToDisk = false,
93
- verbose = false,
94
- makeChart = false,
95
- soup = {},
96
- hook = (record) => record,
97
- hasAdSpend = false,
98
- hasCampaigns = false,
99
- hasLocation = false,
100
- isAnonymous = false,
101
- hasBrowser = false,
102
- hasAndroidDevices = false,
103
- hasDesktopDevices = false,
104
- hasIOSDevices = false
105
- } = config;
106
-
107
- if (!config.superProps) config.superProps = superProps;
108
- if (!config.userProps || Object.keys(config?.userProps)) config.userProps = userProps;
109
-
110
-
111
- config.simulationName = makeName();
112
- const { simulationName } = config;
113
- if (epochStart && !numDays) numDays = dayjs.unix(epochEnd).diff(dayjs.unix(epochStart), "day");
114
- if (!epochStart && numDays) epochStart = dayjs.unix(epochEnd).subtract(numDays, "day").unix();
115
- if (epochStart && numDays) { } //noop
116
- if (!epochStart && !numDays) debugger; //never happens
117
- config.seed = seed;
118
- config.numEvents = numEvents;
119
- config.numUsers = numUsers;
120
- config.numDays = numDays;
121
- config.epochStart = epochStart;
122
- config.epochEnd = epochEnd;
123
- config.events = events;
124
- config.superProps = superProps;
125
- config.funnels = funnels;
126
- config.userProps = userProps;
127
- config.scdProps = scdProps;
128
- config.mirrorProps = mirrorProps;
129
- config.groupKeys = groupKeys;
130
- config.groupProps = groupProps;
131
- config.lookupTables = lookupTables;
132
- config.anonIds = anonIds;
133
- config.sessionIds = sessionIds;
134
- config.format = format;
135
- config.token = token;
136
- config.region = region;
137
- config.writeToDisk = writeToDisk;
138
- config.verbose = verbose;
139
- config.makeChart = makeChart;
140
- config.soup = soup;
141
- config.hook = hook;
142
- config.hasAdSpend = hasAdSpend;
143
- config.hasCampaigns = hasCampaigns;
144
- config.hasLocation = hasLocation;
145
- config.isAnonymous = isAnonymous;
146
- config.hasBrowser = hasBrowser;
147
- config.hasAndroidDevices = hasAndroidDevices;
148
- config.hasDesktopDevices = hasDesktopDevices;
149
- config.hasIOSDevices = hasIOSDevices;
150
-
151
- //event validation
152
- const validatedEvents = u.validateEventConfig(events);
153
- events = validatedEvents;
154
- config.events = validatedEvents;
155
-
156
- //globals
157
- global.MP_SIMULATION_CONFIG = config;
158
- CONFIG = config;
159
- VERBOSE = verbose;
160
- CAMPAIGNS = campaigns;
161
- DEFAULTS = {
162
- locations: u.pickAWinner(locations, 0),
163
- iOSDevices: u.pickAWinner(devices.iosDevices, 0),
164
- androidDevices: u.pickAWinner(devices.androidDevices, 0),
165
- desktopDevices: u.pickAWinner(devices.desktopDevices, 0),
166
- browsers: u.pickAWinner(devices.browsers, 0),
167
- campaigns: u.pickAWinner(campaigns, 0),
168
- };
169
-
170
- const runId = uid(42);
171
- let trackingParams = { runId, seed, numEvents, numUsers, numDays, anonIds, sessionIds, format, targetToken: token, region, writeToDisk, isCLI, version };
172
- track('start simulation', trackingParams);
173
-
174
- log(`------------------SETUP------------------`);
175
- log(`\nyour data simulation will heretofore be known as: \n\n\t${simulationName.toUpperCase()}...\n`);
176
- log(`and your configuration is:\n\n`, JSON.stringify({ seed, numEvents, numUsers, numDays, format, token, region, writeToDisk, anonIds, sessionIds }, null, 2));
177
- log(`------------------SETUP------------------`, "\n");
178
-
179
- //setup all the data structures we will push into
180
- const eventData = u.enrichArray([], { hook, type: "event", config });
181
- const userProfilesData = u.enrichArray([], { hook, type: "user", config });
182
- const adSpendData = u.enrichArray([], { hook, type: "ad-spend", config });
183
- const scdTableKeys = Object.keys(scdProps);
184
- const scdTableData = [];
185
- for (const [index, key] of scdTableKeys.entries()) {
186
- scdTableData[index] = u.enrichArray([], { hook, type: "scd", config, scdKey: key });
187
- }
188
- const groupProfilesData = u.enrichArray([], { hook, type: "group", config });
189
- const lookupTableData = u.enrichArray([], { hook, type: "lookup", config });
190
- const avgEvPerUser = Math.ceil(numEvents / numUsers);
191
-
192
- // if no funnels, make some out of events...
193
- if (!funnels || !funnels.length) {
194
- funnels = u.inferFunnels(events);
195
- config.funnels = funnels;
196
- CONFIG = config;
197
- }
198
-
199
- //user loop
200
- log(`---------------SIMULATION----------------`, "\n\n");
201
- loopUsers: for (let i = 1; i < numUsers + 1; i++) {
202
- u.progress([["users", i], ["events", eventData.length]]);
203
- const userId = chance.guid();
204
- const user = u.person(userId, numDays, isAnonymous);
205
- const { distinct_id, created, anonymousIds, sessionIds } = user;
206
- let numEventsPreformed = 0;
207
-
208
- if (hasLocation) {
209
- const location = u.choose(clone(DEFAULTS.locations()).map(l => { delete l.country; return l; }));
210
- for (const key in location) {
211
- user[key] = location[key];
212
- }
213
- }
214
-
215
-
216
-
217
- // profile creation
218
- const profile = makeProfile(userProps, user);
219
- userProfilesData.hookPush(profile);
220
-
221
- //scd creation
222
- /** @type {Record<string, SCDTableRow[]>} */
223
- // @ts-ignore
224
- const userSCD = {};
225
- for (const [index, key] of scdTableKeys.entries()) {
226
- const mutations = chance.integer({ min: 1, max: 10 });
227
- const changes = makeSCD(scdProps[key], key, distinct_id, mutations, created);
228
- // @ts-ignore
229
- userSCD[key] = changes;
230
- scdTableData[index].hookPush(changes);
231
- }
232
-
233
- let numEventsThisUserWillPreform = Math.floor(chance.normal({
234
- mean: avgEvPerUser,
235
- dev: avgEvPerUser / u.integer(u.integer(2, 5), u.integer(2, 7))
236
- }) * 0.714159265359);
237
-
238
- // power users do 5x more events
239
- chance.bool({ likelihood: 20 }) ? numEventsThisUserWillPreform *= 5 : null;
240
-
241
- // shitty users do 1/3 as many events
242
- chance.bool({ likelihood: 15 }) ? numEventsThisUserWillPreform *= 0.333 : null;
243
-
244
- numEventsThisUserWillPreform = Math.round(numEventsThisUserWillPreform);
245
-
246
- let userFirstEventTime;
247
-
248
- //first funnel
249
- const firstFunnels = funnels.filter((f) => f.isFirstFunnel).reduce(u.weighFunnels, []);
250
- const usageFunnels = funnels.filter((f) => !f.isFirstFunnel).reduce(u.weighFunnels, []);
251
- const userIsBornInDataset = chance.bool({ likelihood: 30 });
252
- if (firstFunnels.length && userIsBornInDataset) {
253
- /** @type {Funnel} */
254
- const firstFunnel = chance.pickone(firstFunnels, user);
255
-
256
- const [data, userConverted] = makeFunnel(firstFunnel, user, profile, userSCD, null, config);
257
- userFirstEventTime = dayjs(data[0].time).unix();
258
- numEventsPreformed += data.length;
259
- eventData.hookPush(data);
260
- if (!userConverted) continue loopUsers;
261
- }
262
-
263
- while (numEventsPreformed < numEventsThisUserWillPreform) {
264
- if (usageFunnels.length) {
265
- /** @type {Funnel} */
266
- const currentFunnel = chance.pickone(usageFunnels);
267
- const [data, userConverted] = makeFunnel(currentFunnel, user, profile, userSCD, userFirstEventTime, config);
268
- numEventsPreformed += data.length;
269
- eventData.hookPush(data);
270
- }
271
- }
272
- // end individual user loop
273
- }
274
-
275
- if (hasAdSpend) {
276
- const days = u.datesBetween(epochStart, epochEnd);
277
- for (const day of days) {
278
- const dailySpendData = makeAdSpend(day);
279
- for (const spendEvent of dailySpendData) {
280
- adSpendData.hookPush(spendEvent);
281
- }
282
- }
283
-
284
- }
285
-
286
- //flatten SCD tables
287
- scdTableData.forEach((table, index) => scdTableData[index] = table.flat());
288
-
289
- log("\n");
290
-
291
- // make group profiles
292
- for (const groupPair of groupKeys) {
293
- const groupKey = groupPair[0];
294
- const groupCardinality = groupPair[1];
295
- const groupProfiles = [];
296
- for (let i = 1; i < groupCardinality + 1; i++) {
297
- u.progress([["groups", i]]);
298
- const group = {
299
- [groupKey]: i,
300
- ...makeProfile(groupProps[groupKey])
301
- };
302
- group["distinct_id"] = i;
303
- groupProfiles.push(group);
304
- }
305
- groupProfilesData.hookPush({ key: groupKey, data: groupProfiles });
306
- }
307
- log("\n");
308
-
309
- // make lookup tables
310
- for (const lookupTable of lookupTables) {
311
- const { key, entries, attributes } = lookupTable;
312
- const data = [];
313
- for (let i = 1; i < entries + 1; i++) {
314
- u.progress([["lookups", i]]);
315
- const item = {
316
- [key]: i,
317
- ...makeProfile(attributes),
318
- };
319
- data.push(item);
320
- }
321
- lookupTableData.hookPush({ key, data });
322
- }
323
-
324
- // SHIFT TIME
325
- const actualNow = dayjs();
326
- const fixedNow = dayjs.unix(global.NOW);
327
- const timeShift = actualNow.diff(fixedNow, "second");
328
-
329
- eventData.forEach((event) => {
330
- try {
331
- const newTime = dayjs(event.time).add(timeShift, "second");
332
- event.time = newTime.toISOString();
333
- if (epochStart && newTime.unix() < epochStart) event = {};
334
- if (epochEnd && newTime.unix() > epochEnd) event = {};
335
- }
336
- catch (e) {
337
- //noop
338
- }
339
- });
340
-
341
- // const dayShift = actualNow.diff(global.NOW, "day");
342
- // userProfilesData.forEach((profile) => {
343
- // const newTime = dayjs(profile.created).add(dayShift, "day");
344
- // profile.created = newTime.toISOString();
345
- // });
346
-
347
-
348
- // draw charts
349
- if (makeChart) {
350
- const bornEvents = config.events?.filter((e) => e.isFirstEvent)?.map(e => e.event) || [];
351
- const bornFunnels = config.funnels?.filter((f) => f.isFirstFunnel)?.map(f => f.sequence[0]) || [];
352
- const bornBehaviors = [...bornEvents, ...bornFunnels];
353
- const chart = await generateLineChart(eventData, bornBehaviors, makeChart);
354
- }
355
-
356
- // create mirrorProps
357
- let mirrorEventData = [];
358
- const mirrorPropKeys = Object.keys(mirrorProps);
359
- if (mirrorPropKeys.length) {
360
- mirrorEventData = clone(eventData);
361
- for (const row of mirrorEventData) {
362
- for (const key of mirrorPropKeys) {
363
- if (mirrorProps[key]?.events?.includes(row?.event)) row[key] = hook(u.choose(mirrorProps[key]?.values), "mirror", { config, row, key });
364
- if (mirrorProps[key]?.events === "*") row[key] = hook(u.choose(mirrorProps[key]?.values), "mirror", { config, row, key });
365
- }
366
- }
367
- }
368
-
369
- const { eventFiles, userFiles, scdFiles, groupFiles, lookupFiles, mirrorFiles, folder, adSpendFiles } =
370
- u.buildFileNames(config);
371
- const pairs = [
372
- [eventFiles, [eventData]],
373
- [userFiles, [userProfilesData]],
374
- [adSpendFiles, [adSpendData]],
375
- [scdFiles, scdTableData],
376
- [groupFiles, groupProfilesData],
377
- [lookupFiles, lookupTableData],
378
- [mirrorFiles, [mirrorEventData]],
379
- ];
380
- log("\n");
381
- log(`---------------SIMULATION----------------`, "\n");
382
-
383
- if (!writeToDisk && !token) {
384
- track('end simulation', trackingParams);
385
- return {
386
- eventData,
387
- userProfilesData,
388
- scdTableData,
389
- groupProfilesData,
390
- lookupTableData,
391
- mirrorEventData,
392
- importResults: {},
393
- files: []
394
- };
395
- }
396
- log(`-----------------WRITES------------------`, `\n\n`);
397
-
398
- let writeFilePromises = [];
399
- if (writeToDisk) {
400
- if (verbose) log(`writing files... for ${simulationName}`);
401
- loopFiles: for (const ENTITY of pairs) {
402
- const [paths, data] = ENTITY;
403
- if (!data.length) continue loopFiles;
404
- for (const [index, path] of paths.entries()) {
405
- let TABLE;
406
- //group + lookup tables are structured differently
407
- if (data?.[index]?.["key"]) {
408
- TABLE = data[index].data;
409
- }
410
- else {
411
- TABLE = data[index];
412
- }
413
-
414
- log(`\twriting ${path}`);
415
- //if it's a lookup table, it's always a CSV
416
- if (format === "csv" || path.includes("-LOOKUP.csv")) {
417
- writeFilePromises.push(u.streamCSV(path, TABLE));
418
- }
419
- else {
420
- writeFilePromises.push(u.streamJSON(path, TABLE));
421
- }
422
-
423
- }
424
- }
425
- }
426
- const fileWriteResults = await Promise.all(writeFilePromises);
427
-
428
- const importResults = { events: {}, users: {}, groups: [] };
429
-
430
- //send to mixpanel
431
- if (token) {
432
- /** @type {import('mixpanel-import').Creds} */
433
- const creds = { token };
434
- /** @type {import('mixpanel-import').Options} */
435
- const commonOpts = {
436
- region,
437
- fixData: true,
438
- verbose: false,
439
- forceStream: true,
440
- strict: false, //! sometimes we get events in the future... it happens
441
- dryRun: false,
442
- abridged: false,
443
- fixJson: true,
444
- showProgress: true
445
- };
446
-
447
- if (eventData) {
448
- log(`importing events to mixpanel...\n`);
449
- const imported = await mp(creds, clone(eventData), {
450
- recordType: "event",
451
- ...commonOpts,
452
- });
453
- log(`\tsent ${comma(imported.success)} events\n`);
454
- importResults.events = imported;
455
- }
456
- if (userProfilesData) {
457
- log(`importing user profiles to mixpanel...\n`);
458
- const imported = await mp(creds, clone(userProfilesData), {
459
- recordType: "user",
460
- ...commonOpts,
461
- });
462
- log(`\tsent ${comma(imported.success)} user profiles\n`);
463
- importResults.users = imported;
464
- }
465
- if (adSpendData) {
466
- log(`importing ad spend data to mixpanel...\n`);
467
- const imported = await mp(creds, clone(adSpendData), {
468
- recordType: "event",
469
- ...commonOpts,
470
- });
471
- log(`\tsent ${comma(imported.success)} ad spend events\n`);
472
- importResults.adSpend = imported;
473
- }
474
- if (groupProfilesData) {
475
- for (const groupProfiles of groupProfilesData) {
476
- const groupKey = groupProfiles.key;
477
- const data = groupProfiles.data;
478
- log(`importing ${groupKey} profiles to mixpanel...\n`);
479
- const imported = await mp({ token, groupKey }, clone(data), {
480
- recordType: "group",
481
- ...commonOpts,
482
-
483
- });
484
- log(`\tsent ${comma(imported.success)} ${groupKey} profiles\n`);
485
-
486
- importResults.groups.push(imported);
487
- }
488
- }
489
- }
490
- log(`\n-----------------WRITES------------------`, "\n");
491
- track('end simulation', trackingParams);
492
-
493
- return {
494
- importResults,
495
- files: [eventFiles, userFiles, scdFiles, groupFiles, lookupFiles, mirrorFiles, folder],
496
- eventData,
497
- userProfilesData,
498
- scdTableData,
499
- groupProfilesData,
500
- lookupTableData,
501
- mirrorEventData,
502
- adSpendData
503
- };
504
- }
505
-
506
-
507
-
508
-
509
-
510
-
511
- /**
512
- * creates a random event
513
- * @param {string} distinct_id
514
- * @param {string[]} anonymousIds
515
- * @param {string[]} sessionIds
516
- * @param {number} earliestTime
517
- * @param {EventConfig} chosenEvent
518
- * @param {Object} superProps
519
- * @param {Object} groupKeys
520
- * @param {Boolean} isFirstEvent=false
521
- */
522
- function makeEvent(distinct_id, anonymousIds, sessionIds, earliestTime, chosenEvent, superProps, groupKeys, isFirstEvent = false) {
523
- const chance = u.getChance();
524
- const { mean = 0, deviation = 2, peaks = 5 } = CONFIG.soup;
525
- const { hasAndroidDevices, hasBrowser, hasCampaigns, hasDesktopDevices, hasIOSDevices, hasLocation } = CONFIG;
526
- //event model
527
- const eventTemplate = {
528
- event: chosenEvent.event,
529
- source: "dm4",
530
- };
531
-
532
- let defaultProps = {};
533
- let devicePool = [];
534
- if (hasLocation) defaultProps.location = clone(DEFAULTS.locations()).map(l => { delete l.country_code; return l; });
535
- if (hasBrowser) defaultProps.browser = DEFAULTS.browsers();
536
- if (hasAndroidDevices) devicePool.push(DEFAULTS.androidDevices());
537
- if (hasIOSDevices) devicePool.push(DEFAULTS.iOSDevices());
538
- if (hasDesktopDevices) devicePool.push(DEFAULTS.desktopDevices());
539
- // we don't always have campaigns, because of attribution
540
- if (hasCampaigns && chance.bool({ likelihood: 25 })) defaultProps.campaigns = DEFAULTS.campaigns();
541
- const devices = devicePool.flat();
542
- if (devices.length) defaultProps.device = devices;
543
-
544
-
545
- //event time
546
- if (earliestTime > NOW) {
547
- earliestTime = dayjs.unix(NOW).subtract(2, 'd').unix();
548
- };
549
-
550
- if (isFirstEvent) eventTemplate.time = dayjs.unix(earliestTime).toISOString();
551
- if (!isFirstEvent) eventTemplate.time = u.TimeSoup(earliestTime, NOW, peaks, deviation, mean);
552
-
553
- // anonymous and session ids
554
- if (CONFIG?.anonIds) eventTemplate.device_id = chance.pickone(anonymousIds);
555
- if (CONFIG?.sessionIds) eventTemplate.session_id = chance.pickone(sessionIds);
556
-
557
- //sometimes have a user_id
558
- if (!isFirstEvent && chance.bool({ likelihood: 42 })) eventTemplate.user_id = distinct_id;
559
-
560
- // ensure that there is a user_id or device_id
561
- if (!eventTemplate.user_id && !eventTemplate.device_id) eventTemplate.user_id = distinct_id;
562
-
563
- const props = { ...chosenEvent.properties, ...superProps };
564
-
565
- //iterate through custom properties
566
- for (const key in props) {
567
- try {
568
- eventTemplate[key] = u.choose(props[key]);
569
- } catch (e) {
570
- console.error(`error with ${key} in ${chosenEvent.event} event`, e);
571
- debugger;
572
- }
573
- }
574
-
575
- //iterate through default properties
576
- for (const key in defaultProps) {
577
- if (Array.isArray(defaultProps[key])) {
578
- const choice = u.choose(defaultProps[key]);
579
- if (typeof choice === "string") {
580
- if (!eventTemplate[key]) eventTemplate[key] = choice;
581
- }
582
-
583
- else if (Array.isArray(choice)) {
584
- for (const subChoice of choice) {
585
- if (!eventTemplate[key]) eventTemplate[key] = subChoice;
586
- }
587
- }
588
-
589
- else if (typeof choice === "object") {
590
- for (const subKey in choice) {
591
- if (typeof choice[subKey] === "string") {
592
- if (!eventTemplate[subKey]) eventTemplate[subKey] = choice[subKey];
593
- }
594
- else if (Array.isArray(choice[subKey])) {
595
- const subChoice = u.choose(choice[subKey]);
596
- if (!eventTemplate[subKey]) eventTemplate[subKey] = subChoice;
597
- }
598
-
599
- else if (typeof choice[subKey] === "object") {
600
- for (const subSubKey in choice[subKey]) {
601
- if (!eventTemplate[subSubKey]) eventTemplate[subSubKey] = choice[subKey][subSubKey];
602
- }
603
- }
604
-
605
- }
606
- }
607
-
608
-
609
- }
610
- }
611
-
612
- //iterate through groups
613
- for (const groupPair of groupKeys) {
614
- const groupKey = groupPair[0];
615
- const groupCardinality = groupPair[1];
616
- const groupEvents = groupPair[2] || [];
617
-
618
- // empty array for group events means all events
619
- if (!groupEvents.length) eventTemplate[groupKey] = u.pick(u.weightedRange(1, groupCardinality));
620
- if (groupEvents.includes(eventTemplate.event)) eventTemplate[groupKey] = u.pick(u.weightedRange(1, groupCardinality));
621
- }
622
-
623
- //make $insert_id
624
- eventTemplate.insert_id = md5(JSON.stringify(eventTemplate));
625
-
626
- return eventTemplate;
627
- }
628
-
629
- /**
630
- * from a funnel spec to a funnel that a user completes/doesn't complete
631
- * this is called MANY times per user
632
- * @param {Funnel} funnel
633
- * @param {Person} user
634
- * @param {UserProfile} profile
635
- * @param {Record<string, SCDTableRow[]>} scd
636
- * @param {number} firstEventTime
637
- * @param {Config} config
638
- * @return {[EventSpec[], Boolean]}
639
- */
640
- function makeFunnel(funnel, user, profile, scd, firstEventTime, config) {
641
- const chance = u.getChance();
642
- const { hook } = config;
643
- hook(funnel, "funnel-pre", { user, profile, scd, funnel, config });
644
- let {
645
- sequence,
646
- conversionRate = 50,
647
- order = 'sequential',
648
- timeToConvert = 1,
649
- props,
650
- requireRepeats = false,
651
- } = funnel;
652
- const { distinct_id, created, anonymousIds, sessionIds } = user;
653
- const { superProps, groupKeys } = config;
654
- const { name, email } = profile;
655
-
656
- //choose the properties for this funnel
657
- const chosenFunnelProps = { ...props, ...superProps };
658
- for (const key in props) {
659
- try {
660
- chosenFunnelProps[key] = u.choose(chosenFunnelProps[key]);
661
- } catch (e) {
662
- console.error(`error with ${key} in ${funnel.sequence.join(" > ")} funnel`, e);
663
- debugger;
664
- }
665
- }
666
-
667
- const funnelPossibleEvents = sequence
668
- .map((eventName) => {
669
- const foundEvent = config.events.find((e) => e.event === eventName);
670
- /** @type {EventConfig} */
671
- const eventSpec = foundEvent || { event: eventName, properties: {} };
672
- for (const key in eventSpec.properties) {
673
- try {
674
- eventSpec.properties[key] = u.choose(eventSpec.properties[key]);
675
- } catch (e) {
676
- console.error(`error with ${key} in ${eventSpec.event} event`, e);
677
- debugger;
678
- }
679
- }
680
- delete eventSpec.isFirstEvent;
681
- delete eventSpec.weight;
682
- eventSpec.properties = { ...eventSpec.properties, ...chosenFunnelProps };
683
- return eventSpec;
684
- })
685
- .reduce((acc, step) => {
686
- if (!requireRepeats) {
687
- if (acc.find(e => e.event === step.event)) {
688
- if (chance.bool({ likelihood: 50 })) {
689
- conversionRate = Math.floor(conversionRate * 1.25); //increase conversion rate
690
- acc.push(step);
691
- }
692
- //A SKIPPED STEP!
693
- else {
694
- conversionRate = Math.floor(conversionRate * .75); //reduce conversion rate
695
- return acc; //early return to skip the step
696
- }
697
- }
698
- else {
699
- acc.push(step);
700
- }
701
- }
702
- else {
703
- acc.push(step);
704
- }
705
- return acc;
706
- }, []);
707
-
708
- let doesUserConvert = chance.bool({ likelihood: conversionRate });
709
- let numStepsUserWillTake = sequence.length;
710
- if (!doesUserConvert) numStepsUserWillTake = u.integer(1, sequence.length - 1);
711
- const funnelTotalRelativeTimeInHours = timeToConvert / numStepsUserWillTake;
712
- const msInHour = 60000 * 60;
713
- const funnelStepsUserWillTake = funnelPossibleEvents.slice(0, numStepsUserWillTake);
714
-
715
- let funnelActualOrder = [];
716
-
717
- switch (order) {
718
- case "sequential":
719
- funnelActualOrder = funnelStepsUserWillTake;
720
- break;
721
- case "random":
722
- funnelActualOrder = u.shuffleArray(funnelStepsUserWillTake);
723
- break;
724
- case "first-fixed":
725
- funnelActualOrder = u.shuffleExceptFirst(funnelStepsUserWillTake);
726
- break;
727
- case "last-fixed":
728
- funnelActualOrder = u.shuffleExceptLast(funnelStepsUserWillTake);
729
- break;
730
- case "first-and-last-fixed":
731
- funnelActualOrder = u.fixFirstAndLast(funnelStepsUserWillTake);
732
- break;
733
- case "middle-fixed":
734
- funnelActualOrder = u.shuffleOutside(funnelStepsUserWillTake);
735
- break;
736
- case "interrupted":
737
- const potentialSubstitutes = config?.events
738
- ?.filter(e => !e.isFirstEvent)
739
- ?.filter(e => !sequence.includes(e.event)) || [];
740
- funnelActualOrder = u.interruptArray(funnelStepsUserWillTake, potentialSubstitutes);
741
- break;
742
- default:
743
- funnelActualOrder = funnelStepsUserWillTake;
744
- break;
745
- }
746
-
747
-
748
-
749
- let lastTimeJump = 0;
750
- const funnelActualEventsWithOffset = funnelActualOrder
751
- .map((event, index) => {
752
- if (index === 0) {
753
- event.relativeTimeMs = 0;
754
- return event;
755
- }
756
-
757
- // Calculate base increment for each step
758
- const baseIncrement = (timeToConvert * msInHour) / numStepsUserWillTake;
759
-
760
- // Introduce a random fluctuation factor
761
- const fluctuation = u.integer(-baseIncrement / u.integer(3, 5), baseIncrement / u.integer(3, 5));
762
-
763
- // Ensure the time increments are increasing and add randomness
764
- const previousTime = lastTimeJump;
765
- const currentTime = previousTime + baseIncrement + fluctuation;
766
-
767
- // Assign the calculated time to the event
768
- const chosenTime = Math.max(currentTime, previousTime + 1); // Ensure non-decreasing time
769
- lastTimeJump = chosenTime;
770
- event.relativeTimeMs = chosenTime;
771
- return event;
772
- });
773
-
774
-
775
- const earliestTime = firstEventTime || dayjs(created).unix();
776
- let funnelStartTime;
777
- let finalEvents = funnelActualEventsWithOffset
778
- .map((event, index) => {
779
- const newEvent = makeEvent(distinct_id, anonymousIds, sessionIds, earliestTime, event, {}, groupKeys);
780
- if (index === 0) {
781
- funnelStartTime = dayjs(newEvent.time);
782
- delete newEvent.relativeTimeMs;
783
- return newEvent;
784
- }
785
- try {
786
- newEvent.time = dayjs(funnelStartTime).add(event.relativeTimeMs, "milliseconds").toISOString();
787
- delete newEvent.relativeTimeMs;
788
- return newEvent;
789
- }
790
- catch (e) {
791
-
792
- debugger;
793
- }
794
- });
795
-
796
-
797
- hook(finalEvents, "funnel-post", { user, profile, scd, funnel, config });
798
- return [finalEvents, doesUserConvert];
799
- }
800
-
801
-
802
- function makeProfile(props, defaults) {
803
- //build the spec
804
- const profile = {
805
- ...defaults,
806
- };
807
-
808
- // anonymous and session ids
809
- if (!CONFIG?.anonIds) delete profile.anonymousIds;
810
- if (!CONFIG?.sessionIds) delete profile.sessionIds;
811
-
812
- for (const key in props) {
813
- try {
814
- profile[key] = u.choose(props[key]);
815
- } catch (e) {
816
- // debugger;
817
- }
818
- }
819
-
820
- return profile;
821
- }
822
-
823
- /**
824
- * @param {import('../types').ValueValid} prop
825
- * @param {string} scdKey
826
- * @param {string} distinct_id
827
- * @param {number} mutations
828
- * @param {string} created
829
- */
830
- function makeSCD(prop, scdKey, distinct_id, mutations, created) {
831
- if (JSON.stringify(prop) === "{}") return {};
832
- if (JSON.stringify(prop) === "[]") return [];
833
- const scdEntries = [];
834
- let lastInserted = dayjs(created);
835
- const deltaDays = dayjs().diff(lastInserted, "day");
836
-
837
- for (let i = 0; i < mutations; i++) {
838
- if (lastInserted.isAfter(dayjs())) break;
839
- const scd = makeProfile({ [scdKey]: prop }, { distinct_id });
840
- scd.startTime = lastInserted.toISOString();
841
- lastInserted = lastInserted.add(u.integer(1, 1000), "seconds");
842
- scd.insertTime = lastInserted.toISOString();
843
- scdEntries.push({ ...scd });
844
- lastInserted = lastInserted
845
- .add(u.integer(0, deltaDays), "day")
846
- .subtract(u.integer(1, 1000), "seconds");
847
- }
848
-
849
- return scdEntries;
850
- }
851
-
852
- //todo
853
- function makeAdSpend(day) {
854
- const chance = u.getChance();
855
- const adSpendEvents = [];
856
- for (const network of CAMPAIGNS) {
857
- const campaigns = network.utm_campaign;
858
- loopCampaigns: for (const campaign of campaigns) {
859
- if (campaign === "$organic") continue loopCampaigns;
860
-
861
- const CAC = u.integer(42, 420); //todo: get the # of users created in this day from eventData
862
- // Randomly generating cost
863
- const cost = chance.floating({ min: 10, max: 250, fixed: 2 });
864
-
865
- // Ensuring realistic CPC and CTR
866
- const avgCPC = chance.floating({ min: 0.33, max: 2.00, fixed: 4 });
867
- const avgCTR = chance.floating({ min: 0.05, max: 0.25, fixed: 4 });
868
-
869
- // Deriving impressions from cost and avg CPC
870
- const clicks = Math.floor(cost / avgCPC);
871
- const impressions = Math.floor(clicks / avgCTR);
872
- const views = Math.floor(impressions * avgCTR);
873
-
874
- //tags
875
- const utm_medium = u.choose(u.pickAWinner(network.utm_medium)());
876
- const utm_content = u.choose(u.pickAWinner(network.utm_content)());
877
- const utm_term = u.choose(u.pickAWinner(network.utm_term)());
878
- //each of these is a campaign
879
- const adSpendEvent = {
880
- event: "Ad Data",
881
- time: day,
882
- source: 'dm4',
883
- utm_campaign: campaign,
884
- campaign_id: md5(network.utm_source[0] + '-' + campaign),
885
- network: network.utm_source[0].toUpperCase(),
886
- distinct_id: network.utm_source[0].toUpperCase(),
887
- utm_source: network.utm_source[0],
888
- utm_medium,
889
- utm_content,
890
- utm_term,
891
-
892
- clicks,
893
- views,
894
- impressions,
895
- cost,
896
- date: dayjs(day).format("YYYY-MM-DD"),
897
- };
898
- adSpendEvents.push(adSpendEvent);
899
- }
900
-
901
-
902
- }
903
- return adSpendEvents;
904
- }
905
-
906
-
907
-
908
-
909
-
910
-
911
-
912
- // this is for CLI
913
- if (require.main === module) {
914
- isCLI = true;
915
- const args = getCliParams();
916
- // @ts-ignore
917
- let { token, seed, format, numDays, numUsers, numEvents, region, writeToDisk, complex = false, sessionIds, anonIds } = args;
918
- // @ts-ignore
919
- const suppliedConfig = args._[0];
920
-
921
- //if the user specifies an separate config file
922
- let config = null;
923
- if (suppliedConfig) {
924
- console.log(`using ${suppliedConfig} for data\n`);
925
- config = require(path.resolve(suppliedConfig));
926
- }
927
- else {
928
- if (complex) {
929
- console.log(`... using default COMPLEX configuration [everything] ...\n`);
930
- console.log(`... for more simple data, don't use the --complex flag ...\n`);
931
- console.log(`... or specify your own js config file (see docs or --help) ...\n`);
932
- config = require(path.resolve(__dirname, "../schemas/complex.js"));
933
- }
934
- else {
935
- console.log(`... using default SIMPLE configuration [events + users] ...\n`);
936
- console.log(`... for more complex data, use the --complex flag ...\n`);
937
- config = require(path.resolve(__dirname, "../schemas/simple.js"));
938
- }
939
- }
940
-
941
- //override config with cli params
942
- if (token) config.token = token;
943
- if (seed) config.seed = seed;
944
- if (format === "csv" && config.format === "json") format = "json";
945
- if (format) config.format = format;
946
- if (numDays) config.numDays = numDays;
947
- if (numUsers) config.numUsers = numUsers;
948
- if (numEvents) config.numEvents = numEvents;
949
- if (region) config.region = region;
950
- if (writeToDisk) config.writeToDisk = writeToDisk;
951
- if (writeToDisk === 'false') config.writeToDisk = false;
952
- if (sessionIds) config.sessionIds = sessionIds;
953
- if (anonIds) config.anonIds = anonIds;
954
- config.verbose = true;
955
-
956
- main(config)
957
- .then((data) => {
958
- log(`-----------------SUMMARY-----------------`);
959
- const d = { success: 0, bytes: 0 };
960
- const darr = [d];
961
- const { events = d, groups = darr, users = d } = data.importResults;
962
- const files = data.files;
963
- const folder = files?.pop();
964
- const groupBytes = groups.reduce((acc, group) => {
965
- return acc + group.bytes;
966
- }, 0);
967
- const groupSuccess = groups.reduce((acc, group) => {
968
- return acc + group.success;
969
- }, 0);
970
- const bytes = events.bytes + groupBytes + users.bytes;
971
- const stats = {
972
- events: comma(events.success || 0),
973
- users: comma(users.success || 0),
974
- groups: comma(groupSuccess || 0),
975
- bytes: bytesHuman(bytes || 0),
976
- };
977
- if (bytes > 0) console.table(stats);
978
- log(`\nfiles written to ${folder || "no where; we didn't write anything"} ...`);
979
- log(" " + files?.flat().join("\n "));
980
- log(`\n----------------SUMMARY-----------------\n\n\n`);
981
- })
982
- .catch((e) => {
983
- log(`------------------ERROR------------------`);
984
- console.error(e);
985
- log(`------------------ERROR------------------`);
986
- debugger;
987
- })
988
- .finally(() => {
989
- log("have a wonderful day :)");
990
- u.openFinder(path.resolve("./data"));
991
- });
992
- } else {
993
- main.utils = { ...u };
994
- module.exports = main;
995
- }
996
-
997
-
998
- function log(...args) {
999
- const cwd = process.cwd(); // Get the current working directory
1000
-
1001
- for (let i = 0; i < args.length; i++) {
1002
- // Replace occurrences of the current working directory with "./" in string arguments
1003
- if (typeof args[i] === 'string') {
1004
- args[i] = args[i].replace(new RegExp(cwd, 'g'), ".");
1005
- }
1006
- }
1007
- if (VERBOSE) console.log(...args);
1008
- }