make-mp-data 1.5.0 → 1.5.2

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.
Files changed (46) hide show
  1. package/.gcloudignore +17 -0
  2. package/.vscode/launch.json +54 -19
  3. package/.vscode/settings.json +2 -0
  4. package/.vscode/tasks.json +12 -0
  5. package/components/ai.js +93 -0
  6. package/{src → components}/chart.js +14 -0
  7. package/{src → components}/cli.js +7 -1
  8. package/components/project.js +166 -0
  9. package/components/prompt.txt +98 -0
  10. package/{src → components}/utils.js +142 -41
  11. package/{schemas → dungeons}/adspend.js +2 -2
  12. package/{schemas → dungeons}/anon.js +2 -2
  13. package/{schemas → dungeons}/big.js +2 -2
  14. package/dungeons/business.js +327 -0
  15. package/{schemas → dungeons}/complex.js +10 -10
  16. package/dungeons/foobar.js +241 -0
  17. package/{schemas → dungeons}/funnels.js +3 -4
  18. package/dungeons/gaming.js +314 -0
  19. package/{schemas → dungeons}/mirror.js +2 -2
  20. package/{schemas/foobar.js → dungeons/sanity.js} +20 -27
  21. package/dungeons/scd.js +205 -0
  22. package/dungeons/session-replay.js +175 -0
  23. package/{schemas → dungeons}/simple.js +3 -3
  24. package/dungeons/userAgent.js +190 -0
  25. package/env.yaml +1 -0
  26. package/index.js +482 -167
  27. package/package.json +13 -6
  28. package/scripts/deploy.sh +11 -0
  29. package/scripts/jsdoctest.js +1 -1
  30. package/scripts/{new.sh → new-dungeon.sh} +39 -10
  31. package/scripts/new-project.mjs +14 -0
  32. package/scripts/update-deps.sh +4 -0
  33. package/tests/benchmark/concurrency.mjs +2 -2
  34. package/tests/cli.test.js +121 -0
  35. package/tests/e2e.test.js +134 -186
  36. package/tests/int.test.js +14 -12
  37. package/tests/jest.config.js +8 -0
  38. package/tests/testCases.mjs +1 -1
  39. package/tests/testSoup.mjs +4 -3
  40. package/tests/unit.test.js +16 -15
  41. package/tsconfig.json +1 -1
  42. package/types.d.ts +40 -8
  43. package/scripts/deps.sh +0 -3
  44. /package/{src → components}/defaults.js +0 -0
  45. /package/dungeons/{.gitkeep → customers/.gitkeep} +0 -0
  46. /package/scripts/{go.sh → run-index.sh} +0 -0
package/index.js CHANGED
@@ -10,35 +10,44 @@ ak@mixpanel.com
10
10
  //todo: regular interval events (like 'card charged')
11
11
  //todo: SCDs send to mixpanel
12
12
  //todo: decent 'new dungeon' workflow
13
+ //todo: validation that funnel events exist
14
+ //todo: ability to catch events not in funnels and make them random...
13
15
 
14
16
 
15
17
  //TIME
16
18
  const dayjs = require("dayjs");
17
19
  const utc = require("dayjs/plugin/utc");
18
20
  dayjs.extend(utc);
19
- const NOW = dayjs('2024-02-02').unix();
20
- global.NOW = NOW;
21
+ const FIXED_NOW = dayjs('2024-02-02').unix();
22
+ global.FIXED_NOW = FIXED_NOW;
21
23
  // ^ this creates a FIXED POINT in time; we will shift it later
24
+ let FIXED_BEGIN = dayjs.unix(FIXED_NOW).subtract(90, 'd').unix();
25
+ global.FIXED_BEGIN = FIXED_BEGIN;
22
26
  const actualNow = dayjs();
23
- const fixedNow = dayjs.unix(global.NOW);
24
- const timeShift = actualNow.diff(fixedNow, "second");
27
+ const timeShift = actualNow.diff(dayjs.unix(FIXED_NOW), "seconds");
28
+ const daysShift = actualNow.diff(dayjs.unix(FIXED_NOW), "days");
25
29
 
26
30
  // UTILS
27
31
  const { existsSync } = require("fs");
28
32
  const pLimit = require('p-limit');
29
33
  const os = require("os");
30
34
  const path = require("path");
31
- const { comma, bytesHuman, makeName, md5, clone, tracker, uid, timer, ls, rm } = require("ak-tools");
35
+ const { comma, bytesHuman, makeName, md5, clone, tracker, uid, timer, ls, rm, touch, load, sLog } = require("ak-tools");
32
36
  const jobTimer = timer('job');
33
- const { generateLineChart } = require('./src/chart.js');
37
+ const { generateLineChart } = require('./components/chart.js');
34
38
  const { version } = require('./package.json');
35
39
  const mp = require("mixpanel-import");
36
- const u = require("./src/utils.js");
37
- const getCliParams = require("./src/cli.js");
40
+ const u = require("./components/utils.js");
41
+ const getCliParams = require("./components/cli.js");
38
42
  const metrics = tracker("make-mp-data", "db99eb8f67ae50949a13c27cacf57d41", os.userInfo().username);
43
+ const t = require('ak-tools');
44
+
45
+
46
+ //CLOUD
47
+ const functions = require('@google-cloud/functions-framework');
39
48
 
40
49
  // DEFAULTS
41
- const { campaigns, devices, locations } = require('./src/defaults.js');
50
+ const { campaigns, devices, locations } = require('./components/defaults.js');
42
51
  let CAMPAIGNS;
43
52
  let DEFAULTS;
44
53
  /** @type {Storage} */
@@ -47,13 +56,17 @@ let STORAGE;
47
56
  let CONFIG;
48
57
  require('dotenv').config();
49
58
 
59
+ const { NODE_ENV = "unknown" } = process.env;
60
+
61
+
62
+
50
63
 
51
64
  // RUN STATE
52
65
  let VERBOSE = false;
53
66
  let isCLI = false;
54
67
  // if we are running in batch mode, we MUST write to disk before we can send to mixpanel
55
- let isBATCH_MODE = false;
56
- let BATCH_SIZE = 500_000;
68
+ let isBATCH_MODE = false;
69
+ let BATCH_SIZE = 1_000_000;
57
70
 
58
71
  //todo: these should be moved into the hookedArrays
59
72
  let operations = 0;
@@ -75,6 +88,7 @@ async function main(config) {
75
88
  // ^ this is critical; same seed = same data;
76
89
  // ^ seed can be passed in as an env var or in the config
77
90
  validateDungeonConfig(config);
91
+ global.FIXED_BEGIN = dayjs.unix(FIXED_NOW).subtract(config.numDays, 'd').unix();
78
92
 
79
93
  //GLOBALS
80
94
  CONFIG = config;
@@ -93,21 +107,25 @@ async function main(config) {
93
107
 
94
108
  //TRACKING
95
109
  const runId = uid(42);
96
- const { events, superProps, userProps, scdProps, groupKeys, groupProps, lookupTables, soup, hook, mirrorProps, ...trackingParams } = config;
110
+ const { events, superProps, userProps, scdProps, groupKeys, groupProps, lookupTables, soup, hook, mirrorProps, token: source_proj_token, ...trackingParams } = config;
97
111
  let { funnels } = config;
98
112
  trackingParams.runId = runId;
99
113
  trackingParams.version = version;
114
+ delete trackingParams.funnels;
100
115
 
101
116
  //STORAGE
102
117
  const { simulationName, format } = config;
103
118
  const eventData = await makeHookArray([], { hook, type: "event", config, format, filepath: `${simulationName}-EVENTS` });
104
119
  const userProfilesData = await makeHookArray([], { hook, type: "user", config, format, filepath: `${simulationName}-USERS` });
105
120
  const adSpendData = await makeHookArray([], { hook, type: "ad-spend", config, format, filepath: `${simulationName}-AD-SPEND` });
121
+ const groupEventData = await makeHookArray([], { hook, type: "group-event", config, format, filepath: `${simulationName}-GROUP-EVENTS` });
106
122
 
107
123
  // SCDs, Groups, + Lookups may have multiple tables
108
124
  const scdTableKeys = Object.keys(scdProps);
109
125
  const scdTableData = await Promise.all(scdTableKeys.map(async (key) =>
110
- await makeHookArray([], { hook, type: "scd", config, format, scdKey: key, filepath: `${simulationName}-SCD-${key}` })
126
+ //todo don't assume everything is a string... lol
127
+ // @ts-ignore
128
+ await makeHookArray([], { hook, type: "scd", config, format, scdKey: key, entityType: config.scdProps[key].type, dataType: "string", filepath: `${simulationName}-${scdProps[key]?.type || "user"}-SCD-${key}` })
111
129
  ));
112
130
  const groupTableKeys = Object.keys(groupKeys);
113
131
  const groupProfilesData = await Promise.all(groupTableKeys.map(async (key, index) => {
@@ -123,7 +141,17 @@ async function main(config) {
123
141
 
124
142
  const mirrorEventData = await makeHookArray([], { hook, type: "mirror", config, format, filepath: `${simulationName}-MIRROR` });
125
143
 
126
- STORAGE = { eventData, userProfilesData, scdTableData, groupProfilesData, lookupTableData, mirrorEventData, adSpendData };
144
+ STORAGE = {
145
+ eventData,
146
+ userProfilesData,
147
+ scdTableData,
148
+ groupProfilesData,
149
+ lookupTableData,
150
+ mirrorEventData,
151
+ adSpendData,
152
+ groupEventData
153
+
154
+ };
127
155
 
128
156
 
129
157
  track('start simulation', trackingParams);
@@ -156,22 +184,72 @@ async function main(config) {
156
184
  log("\n");
157
185
 
158
186
  //GROUP PROFILES
187
+ const groupSCDs = t.objFilter(scdProps, (scd) => scd.type !== 'user');
159
188
  for (const [index, groupPair] of groupKeys.entries()) {
160
189
  const groupKey = groupPair[0];
161
190
  const groupCardinality = groupPair[1];
162
191
  for (let i = 1; i < groupCardinality + 1; i++) {
163
192
  if (VERBOSE) u.progress([["groups", i]]);
164
- const props = await makeProfile(groupProps[groupKey]);
193
+
194
+ const props = await makeProfile(groupProps[groupKey], { created: () => { return dayjs().subtract(u.integer(0, CONFIG.numDays || 30), 'd').toISOString(); } });
165
195
  const group = {
166
196
  [groupKey]: i,
167
197
  ...props,
168
198
  };
169
199
  group["distinct_id"] = i.toString();
170
200
  await groupProfilesData[index].hookPush(group);
201
+
202
+ //SCDs
203
+ const thisGroupSCD = t.objFilter(groupSCDs, (scd) => scd.type === groupKey);
204
+ const groupSCDKeys = Object.keys(thisGroupSCD);
205
+ const groupSCD = {};
206
+ for (const [index, key] of groupSCDKeys.entries()) {
207
+ const { max = 100 } = groupSCDs[key];
208
+ const mutations = chance.integer({ min: 2, max });
209
+ const changes = await makeSCD(scdProps[key], key, i.toString(), mutations, group.created);
210
+ groupSCD[key] = changes;
211
+ const scdTable = scdTableData
212
+ .filter(hookArr => hookArr.scdKey === key);
213
+
214
+ await config.hook(changes, 'scd-pre', { profile: group, type: groupKey, scd: { [key]: groupSCDs[key] }, config, allSCDs: groupSCD });
215
+ await scdTable[0].hookPush(changes, { profile: group, type: groupKey });
216
+ }
217
+
218
+
171
219
  }
172
220
  }
173
221
  log("\n");
174
222
 
223
+ //GROUP EVENTS
224
+ if (config.groupEvents) {
225
+ for (const groupEvent of config.groupEvents) {
226
+ const { frequency, group_key, attribute_to_user, group_size, ...normalEvent } = groupEvent;
227
+ for (const group_num of Array.from({ length: group_size }, (_, i) => i + 1)) {
228
+ const groupProfile = groupProfilesData.find(groups => groups.groupKey === group_key).find(group => group[group_key] === group_num);
229
+ const { created, distinct_id } = groupProfile;
230
+ normalEvent[group_key] = distinct_id;
231
+ const random_user_id = chance.pick(eventData.filter(a => a.user_id)).user_id;
232
+ if (!random_user_id) debugger;
233
+ const deltaDays = actualNow.diff(dayjs(created), "day");
234
+ const numIntervals = Math.floor(deltaDays / frequency);
235
+ const eventsForThisGroup = [];
236
+ for (let i = 0; i < numIntervals; i++) {
237
+ const event = await makeEvent(random_user_id, null, normalEvent, [], [], {}, [], false, true);
238
+ if (!attribute_to_user) delete event.user_id;
239
+ event[group_key] = distinct_id;
240
+ event.time = dayjs(created).add(i * frequency, "day").toISOString();
241
+ delete event.distinct_id;
242
+ //always skip the first event
243
+ if (i !== 0) {
244
+ eventsForThisGroup.push(event);
245
+ }
246
+ }
247
+ await groupEventData.hookPush(eventsForThisGroup, { profile: groupProfile });
248
+ }
249
+ }
250
+ }
251
+
252
+
175
253
  //LOOKUP TABLES
176
254
  for (const [index, lookupTable] of lookupTables.entries()) {
177
255
  const { key, entries, attributes } = lookupTable;
@@ -204,7 +282,7 @@ async function main(config) {
204
282
  const bornBehaviors = [...bornEvents, ...bornFunnels];
205
283
  const chart = await generateLineChart(eventData, bornBehaviors, makeChart);
206
284
  }
207
- const { writeToDisk, token } = config;
285
+ const { writeToDisk = true, token } = config;
208
286
  if (!writeToDisk && !token) {
209
287
  jobTimer.stop(false);
210
288
  const { start, end, delta, human } = jobTimer.report(false);
@@ -243,6 +321,7 @@ async function main(config) {
243
321
  jobTimer.stop(false);
244
322
  const { start, end, delta, human } = jobTimer.report(false);
245
323
 
324
+ if (process.env.NODE_ENV === 'dev') debugger;
246
325
  return {
247
326
  ...STORAGE,
248
327
  importResults,
@@ -253,6 +332,53 @@ async function main(config) {
253
332
 
254
333
 
255
334
 
335
+ functions.http('entry', async (req, res) => {
336
+ const reqTimer = timer('request');
337
+ reqTimer.start();
338
+ let response = {};
339
+ let script = req.body || "";
340
+ let writePath;
341
+ try {
342
+ sLog("DM4: start");
343
+ if (!script) throw new Error("no script");
344
+
345
+ // Replace require("../ with require("./
346
+ script = script.replace(/require\("\.\.\//g, 'require("./');
347
+ // ^ need to replace this because of the way the script is passed in... this is sketch
348
+
349
+ /** @type {Config} */
350
+ const config = eval(script);
351
+ sLog("DM4: eval ok");
352
+
353
+ const { token } = config;
354
+ if (!token) throw new Error("no token");
355
+
356
+ /** @type {Config} */
357
+ const optionsYouCantChange = {
358
+ verbose: false,
359
+ writeToDisk: false,
360
+
361
+ };
362
+ const result = await main({
363
+ ...config,
364
+ ...optionsYouCantChange,
365
+ });
366
+ await rm(writePath);
367
+ reqTimer.stop(false);
368
+ const { start, end, delta, human } = jobTimer.report(false);
369
+ sLog(`DM4: end (${human})`, { ms: delta });
370
+ }
371
+ catch (e) {
372
+ sLog("DM4: error", { error: e.message });
373
+ response = { error: e.message };
374
+ res.status(500);
375
+ await rm(writePath);
376
+ }
377
+ finally {
378
+ res.send(response);
379
+ }
380
+ });
381
+
256
382
 
257
383
  /*
258
384
  ------
@@ -272,7 +398,7 @@ MODELS
272
398
  * @param {Boolean} [isFirstEvent]
273
399
  * @return {Promise<EventSchema>}
274
400
  */
275
- async function makeEvent(distinct_id, earliestTime, chosenEvent, anonymousIds, sessionIds, superProps, groupKeys, isFirstEvent) {
401
+ async function makeEvent(distinct_id, earliestTime, chosenEvent, anonymousIds, sessionIds, superProps, groupKeys, isFirstEvent, skipDefaults = false) {
276
402
  operations++;
277
403
  eventCount++;
278
404
  if (!distinct_id) throw new Error("no distinct_id");
@@ -304,11 +430,13 @@ async function makeEvent(distinct_id, earliestTime, chosenEvent, anonymousIds, s
304
430
 
305
431
  let defaultProps = {};
306
432
  let devicePool = [];
433
+
307
434
  if (hasLocation) defaultProps.location = DEFAULTS.locationsEvents();
308
435
  if (hasBrowser) defaultProps.browser = DEFAULTS.browsers();
309
436
  if (hasAndroidDevices) devicePool.push(DEFAULTS.androidDevices());
310
437
  if (hasIOSDevices) devicePool.push(DEFAULTS.iOSDevices());
311
438
  if (hasDesktopDevices) devicePool.push(DEFAULTS.desktopDevices());
439
+
312
440
  // we don't always have campaigns, because of attribution
313
441
  if (hasCampaigns && chance.bool({ likelihood: 25 })) defaultProps.campaigns = DEFAULTS.campaigns();
314
442
  const devices = devicePool.flat();
@@ -316,12 +444,10 @@ async function makeEvent(distinct_id, earliestTime, chosenEvent, anonymousIds, s
316
444
 
317
445
 
318
446
  //event time
319
- if (earliestTime > NOW) {
320
- earliestTime = dayjs.unix(NOW).subtract(2, 'd').unix();
321
- };
322
-
323
- if (isFirstEvent) eventTemplate.time = dayjs.unix(earliestTime).toISOString();
324
- if (!isFirstEvent) eventTemplate.time = u.TimeSoup(earliestTime, NOW, peaks, deviation, mean);
447
+ if (earliestTime) {
448
+ if (isFirstEvent) eventTemplate.time = dayjs.unix(earliestTime).toISOString();
449
+ if (!isFirstEvent) eventTemplate.time = u.TimeSoup(earliestTime, FIXED_NOW, peaks, deviation, mean);
450
+ }
325
451
 
326
452
  // anonymous and session ids
327
453
  if (anonymousIds.length) eventTemplate.device_id = chance.pickone(anonymousIds);
@@ -346,39 +472,39 @@ async function makeEvent(distinct_id, earliestTime, chosenEvent, anonymousIds, s
346
472
  }
347
473
 
348
474
  //iterate through default properties
349
- for (const key in defaultProps) {
350
- if (Array.isArray(defaultProps[key])) {
351
- const choice = u.choose(defaultProps[key]);
352
- if (typeof choice === "string") {
353
- if (!eventTemplate[key]) eventTemplate[key] = choice;
354
- }
355
-
356
- else if (Array.isArray(choice)) {
357
- for (const subChoice of choice) {
358
- if (!eventTemplate[key]) eventTemplate[key] = subChoice;
475
+ if (!skipDefaults) {
476
+ for (const key in defaultProps) {
477
+ if (Array.isArray(defaultProps[key])) {
478
+ const choice = u.choose(defaultProps[key]);
479
+ if (typeof choice === "string") {
480
+ if (!eventTemplate[key]) eventTemplate[key] = choice;
359
481
  }
360
- }
361
482
 
362
- else if (typeof choice === "object") {
363
- for (const subKey in choice) {
364
- if (typeof choice[subKey] === "string") {
365
- if (!eventTemplate[subKey]) eventTemplate[subKey] = choice[subKey];
366
- }
367
- else if (Array.isArray(choice[subKey])) {
368
- const subChoice = u.choose(choice[subKey]);
369
- if (!eventTemplate[subKey]) eventTemplate[subKey] = subChoice;
483
+ else if (Array.isArray(choice)) {
484
+ for (const subChoice of choice) {
485
+ if (!eventTemplate[key]) eventTemplate[key] = subChoice;
370
486
  }
487
+ }
371
488
 
372
- else if (typeof choice[subKey] === "object") {
373
- for (const subSubKey in choice[subKey]) {
374
- if (!eventTemplate[subSubKey]) eventTemplate[subSubKey] = choice[subKey][subSubKey];
489
+ else if (typeof choice === "object") {
490
+ for (const subKey in choice) {
491
+ if (typeof choice[subKey] === "string") {
492
+ if (!eventTemplate[subKey]) eventTemplate[subKey] = choice[subKey];
493
+ }
494
+ else if (Array.isArray(choice[subKey])) {
495
+ const subChoice = u.choose(choice[subKey]);
496
+ if (!eventTemplate[subKey]) eventTemplate[subKey] = subChoice;
375
497
  }
376
- }
377
498
 
499
+ else if (typeof choice[subKey] === "object") {
500
+ for (const subSubKey in choice[subKey]) {
501
+ if (!eventTemplate[subSubKey]) eventTemplate[subSubKey] = choice[subKey][subSubKey];
502
+ }
503
+ }
504
+
505
+ }
378
506
  }
379
507
  }
380
-
381
-
382
508
  }
383
509
  }
384
510
 
@@ -396,10 +522,11 @@ async function makeEvent(distinct_id, earliestTime, chosenEvent, anonymousIds, s
396
522
  //make $insert_id
397
523
  eventTemplate.insert_id = md5(JSON.stringify(eventTemplate));
398
524
 
399
- //time shift to present
400
- const newTime = dayjs(eventTemplate.time).add(timeShift, "second");
401
- eventTemplate.time = newTime.toISOString();
402
-
525
+ // move time forward
526
+ if (earliestTime) {
527
+ const timeShifted = dayjs(eventTemplate.time).add(timeShift, "seconds").toISOString();
528
+ eventTemplate.time = timeShifted;
529
+ }
403
530
 
404
531
 
405
532
  return eventTemplate;
@@ -489,6 +616,8 @@ async function makeFunnel(funnel, user, firstEventTime, profile, scd, config) {
489
616
  return acc;
490
617
  }, []);
491
618
 
619
+ if (conversionRate > 100) conversionRate = 100;
620
+ if (conversionRate < 0) conversionRate = 0;
492
621
  let doesUserConvert = chance.bool({ likelihood: conversionRate });
493
622
  let numStepsUserWillTake = sequence.length;
494
623
  if (!doesUserConvert) numStepsUserWillTake = u.integer(1, sequence.length - 1);
@@ -601,12 +730,23 @@ async function makeProfile(props, defaults) {
601
730
  ...defaults,
602
731
  };
603
732
 
733
+ for (const key in profile) {
734
+ try {
735
+ profile[key] = u.choose(profile[key]);
736
+ }
737
+ catch (e) {
738
+ // never gets here
739
+ debugger;
740
+ }
741
+ }
742
+
743
+
604
744
  for (const key in props) {
605
745
  try {
606
746
  profile[key] = u.choose(props[key]);
607
747
  } catch (e) {
608
748
  // never gets here
609
- // debugger;
749
+ debugger;
610
750
  }
611
751
  }
612
752
 
@@ -614,42 +754,67 @@ async function makeProfile(props, defaults) {
614
754
  }
615
755
 
616
756
  /**
617
- * @param {ValueValid} prop
757
+ * @param {SCDProp} scdProp
618
758
  * @param {string} scdKey
619
759
  * @param {string} distinct_id
620
760
  * @param {number} mutations
621
761
  * @param {string} created
622
762
  * @return {Promise<SCDSchema[]>}
623
763
  */
624
- async function makeSCD(prop, scdKey, distinct_id, mutations, created) {
625
- if (JSON.stringify(prop) === "{}" || JSON.stringify(prop) === "[]") return [];
764
+ async function makeSCD(scdProp, scdKey, distinct_id, mutations, created) {
765
+ if (Array.isArray(scdProp)) scdProp = { values: scdProp, frequency: 'week', max: 10, timing: 'fuzzy', type: 'user' };
766
+ const { frequency, max, timing, values, type } = scdProp;
767
+ if (JSON.stringify(values) === "{}" || JSON.stringify(values) === "[]") return [];
626
768
  const scdEntries = [];
627
769
  let lastInserted = dayjs(created);
628
770
  const deltaDays = dayjs().diff(lastInserted, "day");
771
+ const uuidKeyName = type === 'user' ? 'distinct_id' : type;
629
772
 
630
773
  for (let i = 0; i < mutations; i++) {
631
774
  if (lastInserted.isAfter(dayjs())) break;
632
- let scd = await makeProfile({ [scdKey]: prop }, { distinct_id });
775
+ let scd = await makeProfile({ [scdKey]: values }, { [uuidKeyName]: distinct_id });
633
776
 
634
777
  // Explicitly constructing SCDSchema object with all required properties
635
778
  const scdEntry = {
636
779
  ...scd, // spread existing properties
637
- distinct_id: scd.distinct_id || distinct_id, // ensure distinct_id is set
638
- insertTime: lastInserted.add(u.integer(1, 1000), "seconds").toISOString(),
639
- startTime: lastInserted.toISOString()
780
+ [uuidKeyName]: scd.distinct_id || distinct_id, // ensure distinct_id is set
781
+ startTime: null,
782
+ insertTime: null
640
783
  };
641
784
 
785
+ if (timing === 'fixed') {
786
+ if (frequency === "day") scdEntry.startTime = lastInserted.add(1, "day").startOf('day').toISOString();
787
+ if (frequency === "week") scdEntry.startTime = lastInserted.add(1, "week").startOf('week').toISOString();
788
+ if (frequency === "month") scdEntry.startTime = lastInserted.add(1, "month").startOf('month').toISOString();
789
+ }
790
+
791
+ if (timing === 'fuzzy') {
792
+ scdEntry.startTime = lastInserted.toISOString();
793
+ }
794
+
795
+ const insertTime = lastInserted.add(u.integer(1, 9000), "seconds");
796
+ scdEntry.insertTime = insertTime.toISOString();
797
+
798
+
799
+
642
800
  // Ensure TypeScript sees all required properties are set
643
801
  if (scdEntry.hasOwnProperty('insertTime') && scdEntry.hasOwnProperty('startTime')) {
644
802
  scdEntries.push(scdEntry);
645
803
  }
646
804
 
805
+ //advance time for next entry
647
806
  lastInserted = lastInserted
648
807
  .add(u.integer(0, deltaDays), "day")
649
- .subtract(u.integer(1, 1000), "seconds");
808
+ .subtract(u.integer(1, 9000), "seconds");
650
809
  }
651
810
 
652
- return scdEntries;
811
+ //de-dupe on startTime
812
+ const deduped = scdEntries.filter((entry, index, self) =>
813
+ index === self.findIndex((t) => (
814
+ t.startTime === entry.startTime
815
+ ))
816
+ );
817
+ return deduped;
653
818
  }
654
819
 
655
820
 
@@ -803,18 +968,24 @@ async function userLoop(config, storage, concurrency = 1) {
803
968
  userProps,
804
969
  scdProps,
805
970
  numDays,
971
+ percentUsersBornInDataset = 5,
806
972
  } = config;
807
973
  const { eventData, userProfilesData, scdTableData } = storage;
808
974
  const avgEvPerUser = numEvents / numUsers;
975
+ const startTime = Date.now();
809
976
 
810
- for (let i = 1; i < numUsers; i++) {
977
+ for (let i = 0; i < numUsers; i++) {
811
978
  await USER_CONN(async () => {
812
979
  userCount++;
813
- if (verbose) u.progress([["users", userCount], ["events", eventCount]]);
980
+ const eps = Math.floor(eventCount / ((Date.now() - startTime) / 1000));
981
+ if (verbose) u.progress([["users", userCount], ["events", eventCount], ["eps", eps]]);
814
982
  const userId = chance.guid();
815
983
  const user = u.generateUser(userId, { numDays, isAnonymous, hasAvatar, hasAnonIds, hasSessionIds });
816
984
  const { distinct_id, created } = user;
985
+ const userIsBornInDataset = chance.bool({ likelihood: percentUsersBornInDataset });
817
986
  let numEventsPreformed = 0;
987
+ if (!userIsBornInDataset) delete user.created;
988
+ const adjustedCreated = userIsBornInDataset ? dayjs(created).subtract(daysShift, 'd') : dayjs.unix(global.FIXED_BEGIN);
818
989
 
819
990
  if (hasLocation) {
820
991
  const location = u.choose(DEFAULTS.locationsUsers);
@@ -822,19 +993,24 @@ async function userLoop(config, storage, concurrency = 1) {
822
993
  user[key] = location[key];
823
994
  }
824
995
  }
825
-
996
+
826
997
  // Profile creation
827
998
  const profile = await makeProfile(userProps, user);
828
- await userProfilesData.hookPush(profile);
999
+
829
1000
 
830
1001
  // SCD creation
831
- const scdTableKeys = Object.keys(scdProps);
1002
+ const scdUserTables = t.objFilter(scdProps, (scd) => scd.type === 'user');
1003
+ const scdTableKeys = Object.keys(scdUserTables);
1004
+
1005
+
832
1006
  const userSCD = {};
833
1007
  for (const [index, key] of scdTableKeys.entries()) {
834
- const mutations = chance.integer({ min: 1, max: 10 }); //todo: configurable mutations?
1008
+ // @ts-ignore
1009
+ const { max = 100 } = scdProps[key];
1010
+ const mutations = chance.integer({ min: 1, max });
835
1011
  const changes = await makeSCD(scdProps[key], key, distinct_id, mutations, created);
836
1012
  userSCD[key] = changes;
837
- await scdTableData[index].hookPush(changes);
1013
+ await config.hook(changes, "scd-pre", { profile, type: 'user', scd: { [key]: scdProps[key] }, config, allSCDs: userSCD });
838
1014
  }
839
1015
 
840
1016
  let numEventsThisUserWillPreform = Math.floor(chance.normal({
@@ -849,23 +1025,30 @@ async function userLoop(config, storage, concurrency = 1) {
849
1025
 
850
1026
  let userFirstEventTime;
851
1027
 
852
- // First funnel logic...
853
1028
  const firstFunnels = funnels.filter((f) => f.isFirstFunnel).reduce(u.weighFunnels, []);
854
1029
  const usageFunnels = funnels.filter((f) => !f.isFirstFunnel).reduce(u.weighFunnels, []);
855
- const userIsBornInDataset = chance.bool({ likelihood: 30 });
1030
+
1031
+ const secondsInDay = 86400;
1032
+ const noise = () => chance.integer({ min: 0, max: secondsInDay });
1033
+ let usersEvents = [];
856
1034
 
857
1035
  if (firstFunnels.length && userIsBornInDataset) {
858
1036
  const firstFunnel = chance.pickone(firstFunnels, user);
859
- const [data, userConverted] = await makeFunnel(firstFunnel, user, null, profile, userSCD, config);
860
- userFirstEventTime = dayjs(data[0].time).unix();
1037
+
1038
+ const firstTime = adjustedCreated.subtract(noise(), 'seconds').unix();
1039
+ const [data, userConverted] = await makeFunnel(firstFunnel, user, firstTime, profile, userSCD, config);
1040
+ userFirstEventTime = dayjs(data[0].time).subtract(timeShift, 'seconds').unix();
861
1041
  numEventsPreformed += data.length;
862
- await eventData.hookPush(data);
1042
+ // await eventData.hookPush(data, { profile });
1043
+ usersEvents.push(...data);
863
1044
  if (!userConverted) {
864
1045
  if (verbose) u.progress([["users", userCount], ["events", eventCount]]);
865
1046
  return;
866
1047
  }
867
1048
  } else {
868
- userFirstEventTime = dayjs(created).unix();
1049
+ // userFirstEventTime = dayjs(created).unix();
1050
+ // userFirstEventTime = global.FIXED_BEGIN;
1051
+ userFirstEventTime = adjustedCreated.subtract(noise(), 'seconds').unix();
869
1052
  }
870
1053
 
871
1054
  while (numEventsPreformed < numEventsThisUserWillPreform) {
@@ -873,14 +1056,35 @@ async function userLoop(config, storage, concurrency = 1) {
873
1056
  const currentFunnel = chance.pickone(usageFunnels);
874
1057
  const [data, userConverted] = await makeFunnel(currentFunnel, user, userFirstEventTime, profile, userSCD, config);
875
1058
  numEventsPreformed += data.length;
876
- await eventData.hookPush(data);
1059
+ usersEvents.push(...data);
1060
+ // await eventData.hookPush(data, { profile });
877
1061
  } else {
878
1062
  const data = await makeEvent(distinct_id, userFirstEventTime, u.choose(config.events), user.anonymousIds, user.sessionIds, {}, config.groupKeys, true);
879
1063
  numEventsPreformed++;
880
- await eventData.hookPush(data);
1064
+ usersEvents.push(data);
1065
+ // await eventData.hookPush(data);
881
1066
  }
882
1067
  }
883
1068
 
1069
+ // NOW ADD ALL OUR DATA FOR THIS USER
1070
+ if (config.hook) {
1071
+ const newEvents = await config.hook(usersEvents, "everything", { profile, scd: userSCD, config, userIsBornInDataset });
1072
+ if (Array.isArray(newEvents)) usersEvents = newEvents;
1073
+ }
1074
+
1075
+ await userProfilesData.hookPush(profile);
1076
+
1077
+ if (Object.keys(userSCD).length) {
1078
+ for (const [key, changesArray] of Object.entries(userSCD)) {
1079
+ for (const changes of changesArray) {
1080
+ const target = scdTableData.filter(arr => arr.scdKey === key).pop();
1081
+ await target.hookPush(changes, { profile, type: 'user' });
1082
+ }
1083
+ }
1084
+ }
1085
+ await eventData.hookPush(usersEvents, { profile });
1086
+
1087
+
884
1088
  if (verbose) u.progress([["users", userCount], ["events", eventCount]]);
885
1089
  });
886
1090
  }
@@ -895,8 +1099,18 @@ async function userLoop(config, storage, concurrency = 1) {
895
1099
  * @param {Storage} storage
896
1100
  */
897
1101
  async function sendToMixpanel(config, storage) {
898
- const { adSpendData, eventData, groupProfilesData, lookupTableData, mirrorEventData, scdTableData, userProfilesData } = storage;
899
- const { token, region, writeToDisk } = config;
1102
+ const {
1103
+ adSpendData,
1104
+ eventData,
1105
+ groupProfilesData,
1106
+ lookupTableData,
1107
+ mirrorEventData,
1108
+ scdTableData,
1109
+ userProfilesData,
1110
+ groupEventData
1111
+
1112
+ } = storage;
1113
+ const { token, region, writeToDisk = true } = config;
900
1114
  const importResults = { events: {}, users: {}, groups: [] };
901
1115
 
902
1116
  /** @type {import('mixpanel-import').Creds} */
@@ -913,7 +1127,7 @@ async function sendToMixpanel(config, storage) {
913
1127
  dryRun: false,
914
1128
  abridged: false,
915
1129
  fixJson: true,
916
- showProgress: true,
1130
+ showProgress: NODE_ENV === "dev" ? true : false,
917
1131
  streamFormat: mpImportFormat
918
1132
  };
919
1133
 
@@ -949,7 +1163,7 @@ async function sendToMixpanel(config, storage) {
949
1163
  log(`\tsent ${comma(imported.success)} user profiles\n`);
950
1164
  importResults.users = imported;
951
1165
  }
952
- if (adSpendData || isBATCH_MODE) {
1166
+ if (groupEventData || isBATCH_MODE) {
953
1167
  log(`importing ad spend data to mixpanel...\n`);
954
1168
  let adSpendDataToImport = clone(adSpendData);
955
1169
  if (isBATCH_MODE) {
@@ -985,11 +1199,65 @@ async function sendToMixpanel(config, storage) {
985
1199
  }
986
1200
  }
987
1201
 
1202
+ if (groupEventData || isBATCH_MODE) {
1203
+ log(`importing group events to mixpanel...\n`);
1204
+ let groupEventDataToImport = clone(groupEventData);
1205
+ if (isBATCH_MODE) {
1206
+ const writeDir = groupEventData.getWriteDir();
1207
+ const files = await ls(writeDir.split(path.basename(writeDir)).join(""));
1208
+ groupEventDataToImport = files.filter(f => f.includes('-GROUP-EVENTS-'));
1209
+ }
1210
+ const imported = await mp(creds, groupEventDataToImport, {
1211
+ recordType: "event",
1212
+ ...commonOpts,
1213
+ strict: false
1214
+ });
1215
+ log(`\tsent ${comma(imported.success)} group events\n`);
1216
+ importResults.groupEvents = imported;
1217
+ }
1218
+ const { serviceAccount, projectId, serviceSecret } = config;
1219
+ if (serviceAccount && projectId && serviceSecret) {
1220
+ if (scdTableData || isBATCH_MODE) {
1221
+ log(`importing SCD data to mixpanel...\n`);
1222
+ for (const scdEntity of scdTableData) {
1223
+ const scdKey = scdEntity?.scdKey;
1224
+ log(`importing ${scdKey} SCD data to mixpanel...\n`);
1225
+ let scdDataToImport = clone(scdEntity);
1226
+ if (isBATCH_MODE) {
1227
+ const writeDir = scdEntity.getWriteDir();
1228
+ const files = await ls(writeDir.split(path.basename(writeDir)).join(""));
1229
+ scdDataToImport = files.filter(f => f.includes(`-SCD-${scdKey}`));
1230
+ }
1231
+
1232
+ const options = {
1233
+ recordType: "scd",
1234
+ scdKey,
1235
+ scdType: scdEntity.dataType,
1236
+ scdLabel: `${scdKey}-scd`,
1237
+ ...commonOpts,
1238
+ };
1239
+ if (scdEntity.entityType !== "user") options.groupKey = scdEntity.entityType
1240
+ const imported = await mp(
1241
+ {
1242
+ token,
1243
+ acct: serviceAccount,
1244
+ pass: serviceSecret,
1245
+ project: projectId
1246
+ },
1247
+ scdDataToImport,
1248
+ // @ts-ignore
1249
+ options);
1250
+ log(`\tsent ${comma(imported.success)} ${scdKey} SCD data\n`);
1251
+ importResults[`${scdKey}_scd`] = imported;
1252
+ }
1253
+ }
1254
+ }
1255
+
988
1256
  //if we are in batch mode, we need to delete the files
989
1257
  if (!writeToDisk && isBATCH_MODE) {
990
1258
  const writeDir = eventData?.getWriteDir() || userProfilesData?.getWriteDir();
991
1259
  const listDir = await ls(writeDir.split(path.basename(writeDir)).join(""));
992
- const files = listDir.filter(f => f.includes('-EVENTS-') || f.includes('-USERS-') || f.includes('-AD-SPEND-') || f.includes('-GROUPS-'));
1260
+ const files = listDir.filter(f => f.includes('-EVENTS-') || f.includes('-USERS-') || f.includes('-AD-SPEND-') || f.includes('-GROUPS-') || f.includes('-GROUP-EVENTS-'));
993
1261
  for (const file of files) {
994
1262
  await rm(file);
995
1263
  }
@@ -1048,6 +1316,7 @@ function validateDungeonConfig(config) {
1048
1316
  hasAndroidDevices = false,
1049
1317
  hasDesktopDevices = false,
1050
1318
  hasIOSDevices = false,
1319
+ alsoInferFunnels = false,
1051
1320
  name = "",
1052
1321
  batchSize = 500_000,
1053
1322
  concurrency = 500
@@ -1076,6 +1345,48 @@ function validateDungeonConfig(config) {
1076
1345
  funnels = inferFunnels(events);
1077
1346
  }
1078
1347
 
1348
+ if (alsoInferFunnels) {
1349
+ const inferredFunnels = inferFunnels(events);
1350
+ funnels = [...funnels, ...inferredFunnels];
1351
+ }
1352
+
1353
+
1354
+ const eventContainedInFunnels = Array.from(funnels.reduce((acc, f) => {
1355
+ const events = f.sequence;
1356
+ events.forEach(event => acc.add(event));
1357
+ return acc;
1358
+ }, new Set()));
1359
+
1360
+ const eventsNotInFunnels = events
1361
+ .filter(e => !e.isFirstEvent)
1362
+ .filter(e => !eventContainedInFunnels.includes(e.event)).map(e => e.event);
1363
+ if (eventsNotInFunnels.length) {
1364
+ // const biggestWeight = funnels.reduce((acc, f) => {
1365
+ // if (f.weight > acc) return f.weight;
1366
+ // return acc;
1367
+ // }, 0);
1368
+ // const smallestWeight = funnels.reduce((acc, f) => {
1369
+ // if (f.weight < acc) return f.weight;
1370
+ // return acc;
1371
+ // }, 0);
1372
+ // const weight = u.integer(smallestWeight, biggestWeight) * 2;
1373
+
1374
+ const sequence = u.shuffleArray(eventsNotInFunnels.flatMap(event => {
1375
+ const evWeight = config.events.find(e => e.event === event)?.weight || 1;
1376
+ return Array(evWeight).fill(event);
1377
+ }));
1378
+
1379
+
1380
+
1381
+ funnels.push({
1382
+ sequence,
1383
+ conversionRate: 50,
1384
+ order: 'random',
1385
+ timeToConvert: 24 * 14,
1386
+ requireRepeats: false,
1387
+ });
1388
+ }
1389
+
1079
1390
  config.concurrency = concurrency;
1080
1391
  config.funnels = funnels;
1081
1392
  config.batchSize = batchSize;
@@ -1139,6 +1450,8 @@ async function makeHookArray(arr = [], opts = {}) {
1139
1450
  if (existsSync(dataFolder)) writeDir = dataFolder;
1140
1451
  else writeDir = path.resolve("./");
1141
1452
 
1453
+ if (NODE_ENV === "prod") writeDir = path.resolve(os.tmpdir());
1454
+
1142
1455
  function getWritePath() {
1143
1456
  if (isBATCH_MODE) {
1144
1457
  return path.join(writeDir, `${filepath}-part-${batch.toString()}.${format}`);
@@ -1152,14 +1465,14 @@ async function makeHookArray(arr = [], opts = {}) {
1152
1465
  return path.join(writeDir, `${filepath}.${format}`);
1153
1466
  }
1154
1467
 
1155
- async function transformThenPush(item) {
1468
+ async function transformThenPush(item, meta) {
1156
1469
  if (item === null || item === undefined) return false;
1157
1470
  if (typeof item === 'object' && Object.keys(item).length === 0) return false;
1158
-
1471
+ const allMetaData = { ...rest, ...meta };
1159
1472
  if (Array.isArray(item)) {
1160
1473
  for (const i of item) {
1161
1474
  try {
1162
- const enriched = await hook(i, type, rest);
1475
+ const enriched = await hook(i, type, allMetaData);
1163
1476
  if (Array.isArray(enriched)) enriched.forEach(e => arr.push(e));
1164
1477
  else arr.push(enriched);
1165
1478
  } catch (e) {
@@ -1169,7 +1482,7 @@ async function makeHookArray(arr = [], opts = {}) {
1169
1482
  }
1170
1483
  } else {
1171
1484
  try {
1172
- const enriched = await hook(item, type, rest);
1485
+ const enriched = await hook(item, type, allMetaData);
1173
1486
  if (Array.isArray(enriched)) enriched.forEach(e => arr.push(e));
1174
1487
  else arr.push(enriched);
1175
1488
  } catch (e) {
@@ -1284,92 +1597,93 @@ CLI
1284
1597
  ----
1285
1598
  */
1286
1599
 
1287
- if (require.main === module) {
1288
- isCLI = true;
1289
- const args = /** @type {Config} */ (getCliParams());
1290
- let { token, seed, format, numDays, numUsers, numEvents, region, writeToDisk, complex = false, hasSessionIds, hasAnonIds } = args;
1291
- const suppliedConfig = args._[0];
1292
-
1293
- //if the user specifies an separate config file
1294
- let config = null;
1295
- if (suppliedConfig) {
1296
- console.log(`using ${suppliedConfig} for data\n`);
1297
- config = require(path.resolve(suppliedConfig));
1298
- }
1299
- else {
1300
- if (complex) {
1301
- console.log(`... using default COMPLEX configuration [everything] ...\n`);
1302
- console.log(`... for more simple data, don't use the --complex flag ...\n`);
1303
- console.log(`... or specify your own js config file (see docs or --help) ...\n`);
1304
- config = require(path.resolve(__dirname, "./schemas/complex.js"));
1600
+ if (NODE_ENV !== "prod") {
1601
+ if (require.main === module) {
1602
+ isCLI = true;
1603
+ const args = /** @type {Config} */ (getCliParams());
1604
+ let { token, seed, format, numDays, numUsers, numEvents, region, writeToDisk, complex = false, hasSessionIds, hasAnonIds } = args;
1605
+ const suppliedConfig = args._[0];
1606
+
1607
+ //if the user specifies an separate config file
1608
+ let config = null;
1609
+ if (suppliedConfig) {
1610
+ console.log(`using ${suppliedConfig} for data\n`);
1611
+ config = require(path.resolve(suppliedConfig));
1305
1612
  }
1306
1613
  else {
1307
- console.log(`... using default SIMPLE configuration [events + users] ...\n`);
1308
- console.log(`... for more complex data, use the --complex flag ...\n`);
1309
- config = require(path.resolve(__dirname, "./schemas/simple.js"));
1614
+ if (complex) {
1615
+ console.log(`... using default COMPLEX configuration [everything] ...\n`);
1616
+ console.log(`... for more simple data, don't use the --complex flag ...\n`);
1617
+ console.log(`... or specify your own js config file (see docs or --help) ...\n`);
1618
+ config = require(path.resolve(__dirname, "./dungeons/complex.js"));
1619
+ }
1620
+ else {
1621
+ console.log(`... using default SIMPLE configuration [events + users] ...\n`);
1622
+ console.log(`... for more complex data, use the --complex flag ...\n`);
1623
+ config = require(path.resolve(__dirname, "./dungeons/simple.js"));
1624
+ }
1310
1625
  }
1311
- }
1312
1626
 
1313
- //override config with cli params
1314
- if (token) config.token = token;
1315
- if (seed) config.seed = seed;
1316
- if (format === "csv" && config.format === "json") format = "json";
1317
- if (format) config.format = format;
1318
- if (numDays) config.numDays = numDays;
1319
- if (numUsers) config.numUsers = numUsers;
1320
- if (numEvents) config.numEvents = numEvents;
1321
- if (region) config.region = region;
1322
- if (writeToDisk) config.writeToDisk = writeToDisk;
1323
- if (writeToDisk === 'false') config.writeToDisk = false;
1324
- if (hasSessionIds) config.hasSessionIds = hasSessionIds;
1325
- if (hasAnonIds) config.hasAnonIds = hasAnonIds;
1326
- config.verbose = true;
1327
-
1328
- main(config)
1329
- .then((data) => {
1330
- //todo: rethink summary
1331
- log(`-----------------SUMMARY-----------------`);
1332
- const d = { success: 0, bytes: 0 };
1333
- const darr = [d];
1334
- const { events = d, groups = darr, users = d } = data?.importResults || {};
1335
- const files = data.files;
1336
- const folder = files?.[0]?.split(path.basename(files?.[0]))?.shift();
1337
- const groupBytes = groups.reduce((acc, group) => {
1338
- return acc + group.bytes;
1339
- }, 0);
1340
- const groupSuccess = groups.reduce((acc, group) => {
1341
- return acc + group.success;
1342
- }, 0);
1343
- const bytes = events.bytes + groupBytes + users.bytes;
1344
- const stats = {
1345
- events: comma(events.success || 0),
1346
- users: comma(users.success || 0),
1347
- groups: comma(groupSuccess || 0),
1348
- bytes: bytesHuman(bytes || 0),
1349
- };
1350
- if (bytes > 0) console.table(stats);
1351
- log(`\nfiles written to ${folder || "no where; we didn't write anything"} ...`);
1352
- log(" " + files?.flat().join("\n "));
1353
- log(`\n----------------SUMMARY-----------------\n\n\n`);
1354
- })
1355
- .catch((e) => {
1356
- log(`------------------ERROR------------------`);
1357
- console.error(e);
1358
- log(`------------------ERROR------------------`);
1359
- debugger;
1360
- })
1361
- .finally(() => {
1362
- log("enjoy your data! :)");
1363
- u.openFinder(path.resolve("./data"));
1364
- });
1365
- } else {
1366
- main.generators = { makeEvent, makeFunnel, makeProfile, makeSCD, makeAdSpend, makeMirror };
1367
- main.orchestrators = { userLoop, validateDungeonConfig, sendToMixpanel };
1368
- main.meta = { inferFunnels, hookArray: makeHookArray };
1369
- module.exports = main;
1627
+ //override config with cli params
1628
+ if (token) config.token = token;
1629
+ if (seed) config.seed = seed;
1630
+ if (format === "csv" && config.format === "json") format = "json";
1631
+ if (format) config.format = format;
1632
+ if (numDays) config.numDays = numDays;
1633
+ if (numUsers) config.numUsers = numUsers;
1634
+ if (numEvents) config.numEvents = numEvents;
1635
+ if (region) config.region = region;
1636
+ if (writeToDisk) config.writeToDisk = writeToDisk;
1637
+ if (writeToDisk === 'false') config.writeToDisk = false;
1638
+ if (hasSessionIds) config.hasSessionIds = hasSessionIds;
1639
+ if (hasAnonIds) config.hasAnonIds = hasAnonIds;
1640
+ config.verbose = true;
1641
+
1642
+ main(config)
1643
+ .then((data) => {
1644
+ log(`-----------------SUMMARY-----------------`);
1645
+ const d = { success: 0, bytes: 0 };
1646
+ const darr = [d];
1647
+ const { events = d, groups = darr, users = d } = data?.importResults || {};
1648
+ const files = data.files;
1649
+ const folder = files?.[0]?.split(path.basename(files?.[0]))?.shift() || "./data";
1650
+ const groupBytes = groups.reduce((acc, group) => {
1651
+ return acc + group.bytes;
1652
+ }, 0);
1653
+ const groupSuccess = groups.reduce((acc, group) => {
1654
+ return acc + group.success;
1655
+ }, 0);
1656
+ const bytes = events.bytes + groupBytes + users.bytes;
1657
+ const stats = {
1658
+ events: comma(events.success || 0),
1659
+ users: comma(users.success || 0),
1660
+ groups: comma(groupSuccess || 0),
1661
+ bytes: bytesHuman(bytes || 0),
1662
+ };
1663
+ if (bytes > 0) console.table(stats);
1664
+ log(`\nfiles written to ${folder || "no where; we didn't write anything"} ...`);
1665
+ log(" " + files?.flat().join("\n "));
1666
+ log(`\n----------------SUMMARY-----------------\n\n\n`);
1667
+ })
1668
+ .catch((e) => {
1669
+ log(`------------------ERROR------------------`);
1670
+ console.error(e);
1671
+ log(`------------------ERROR------------------`);
1672
+ debugger;
1673
+ })
1674
+ .finally(() => {
1675
+ log("enjoy your data! :)");
1676
+ });
1677
+ } else {
1678
+ main.generators = { makeEvent, makeFunnel, makeProfile, makeSCD, makeAdSpend, makeMirror };
1679
+ main.orchestrators = { userLoop, validateDungeonConfig, sendToMixpanel };
1680
+ main.meta = { inferFunnels, hookArray: makeHookArray };
1681
+ module.exports = main;
1682
+ }
1370
1683
  }
1371
1684
 
1372
1685
 
1686
+
1373
1687
  /*
1374
1688
  ----
1375
1689
  HELPERS
@@ -1394,7 +1708,7 @@ function track(name, props, ...rest) {
1394
1708
  }
1395
1709
 
1396
1710
 
1397
- /** @typedef {import('./types.js').Config} Config */
1711
+ /** @typedef {import('./types.js').Dungeon} Config */
1398
1712
  /** @typedef {import('./types.js').AllData} AllData */
1399
1713
  /** @typedef {import('./types.js').EventConfig} EventConfig */
1400
1714
  /** @typedef {import('./types.js').Funnel} Funnel */
@@ -1407,4 +1721,5 @@ function track(name, props, ...rest) {
1407
1721
  /** @typedef {import('./types.js').ValueValid} ValueValid */
1408
1722
  /** @typedef {import('./types.js').HookedArray} hookArray */
1409
1723
  /** @typedef {import('./types.js').hookArrayOptions} hookArrayOptions */
1410
- /** @typedef {import('./types.js').GroupProfileSchema} GroupProfile */
1724
+ /** @typedef {import('./types.js').GroupProfileSchema} GroupProfile */
1725
+ /** @typedef {import('./types.js').SCDProp} SCDProp */