make-mp-data 1.4.5 → 1.5.1

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.
@@ -23,18 +23,30 @@
23
23
  "type": "node-terminal"
24
24
  },
25
25
  {
26
- "type": "node",
27
- "request": "launch",
28
- "name": "go",
29
- "runtimeExecutable": "nodemon",
30
- "program": "${file}",
31
- "restart": true,
32
- "console": "integratedTerminal",
33
- "internalConsoleOptions": "neverOpen",
34
- "args": [
35
- "--ignore",
36
- "./data/"
37
- ]
38
- }
26
+ "type": "node",
27
+ "request": "launch",
28
+ "name": "go",
29
+ "runtimeExecutable": "nodemon",
30
+ "runtimeArgs": ["--inspect"],
31
+ "program": "${workspaceFolder}/index.js",
32
+ "args": ["--ignore", "./data/*", "${file}"],
33
+ "restart": true,
34
+ "console": "integratedTerminal",
35
+ "internalConsoleOptions": "neverOpen",
36
+ "skipFiles": ["<node_internals>/**"],
37
+ },
38
+ {
39
+ "type": "node",
40
+ "request": "launch",
41
+ "name": "sanity",
42
+ "runtimeExecutable": "nodemon",
43
+ "runtimeArgs": ["--inspect"],
44
+ "program": "${workspaceFolder}/index.js",
45
+ "args": ["--ignore", "./data/*", "./schemas/sanity.js"],
46
+ "restart": true,
47
+ "console": "integratedTerminal",
48
+ "internalConsoleOptions": "neverOpen",
49
+ "skipFiles": ["<node_internals>/**"],
50
+ }
39
51
  ]
40
52
  }
@@ -15,7 +15,6 @@ by ak@mixpanel.com
15
15
 
16
16
  function cliParams() {
17
17
  console.log(hero);
18
- // @ts-ignore
19
18
  const args = yargs(process.argv.splice(2))
20
19
  .scriptName("make-mp-data")
21
20
  .usage(`\nusage:\nnpx $0 [dataModel.js] [options]
@@ -85,6 +84,13 @@ DATA MODEL: https://github.com/ak--47/make-mp-data/blob/main/default.js
85
84
  describe: 'either US or EU',
86
85
  type: 'string'
87
86
  })
87
+ .option('concurrency', {
88
+ alias: 'conn',
89
+ default: 500,
90
+ demandOption: false,
91
+ describe: 'concurrency level for data generation',
92
+ type: 'number'
93
+ })
88
94
  .options("complex", {
89
95
  demandOption: false,
90
96
  default: false,
@@ -101,7 +107,7 @@ DATA MODEL: https://github.com/ak--47/make-mp-data/blob/main/default.js
101
107
  type: 'boolean',
102
108
  coerce: boolCoerce
103
109
  })
104
- .option("sessionIds", {
110
+ .option("hasSessionIds", {
105
111
  demandOption: false,
106
112
  default: false,
107
113
  describe: 'create session ids in the data',
@@ -109,7 +115,7 @@ DATA MODEL: https://github.com/ak--47/make-mp-data/blob/main/default.js
109
115
  type: 'boolean',
110
116
  coerce: boolCoerce
111
117
  })
112
- .option("anonIds", {
118
+ .option("hasAnonIds", {
113
119
  demandOption: false,
114
120
  default: false,
115
121
  describe: 'create anonymous ids in the data',
@@ -187,6 +193,7 @@ DATA MODEL: https://github.com/ak--47/make-mp-data/blob/main/default.js
187
193
  type: 'boolean',
188
194
  coerce: boolCoerce
189
195
  })
196
+
190
197
  .help()
191
198
  .wrap(null)
192
199
  .argv;
@@ -0,0 +1,155 @@
1
+ require('dotenv').config();
2
+ const akTools = require('ak-tools');
3
+ const { rand, makeName } = akTools;
4
+ let { OAUTH_TOKEN = "" } = process.env;
5
+
6
+ /**
7
+ * Main function to create a project and add group keys to it.
8
+ *
9
+ * @param {Object} params - Parameters for the function.
10
+ * @param {string} [params.oauth=""] - OAuth token for authentication.
11
+ * @param {string} [params.orgId=""] - Organization ID.
12
+ * @param {Array<Object>} [params.groups=[]] - List of groups to add to the project.
13
+ * @param {string} [params.name=""] - Name of the user.
14
+ * @param {string} [params.email=""] - Email of the user.
15
+ * @param {string} [params.projectName=""] - Name of the project.
16
+ * @returns {Promise<Object>} The created project with additional group keys.
17
+ * @throws Will throw an error if OAUTH_TOKEN is not set.
18
+ * @throws Will throw an error if orgId is not found.
19
+ */
20
+ async function main(params = {}) {
21
+ let { oauth = "", orgId = "", groups = [], name = "", email = "", projectName } = params;
22
+ if (oauth) OAUTH_TOKEN = oauth;
23
+ if (!OAUTH_TOKEN) throw new Error('No OAUTH_TOKEN in .env');
24
+ if (!orgId) {
25
+ ({ orgId, name, email } = await getUser());
26
+ }
27
+ if (!orgId) throw new Error('No orgId found');
28
+ if (!projectName) projectName = makeName();
29
+ const project = await makeProject(orgId);
30
+ project.user = name;
31
+ project.email = email;
32
+ project.groups = groups;
33
+ project.orgId = orgId;
34
+ const groupKeys = [
35
+ // { display_name: 'Account', property_name: 'account_id' },
36
+ ];
37
+ groupKeys.push(...groups);
38
+ const addedGroupKeys = await addGroupKeys(groupKeys, project.id);
39
+ project.groupsAdded = addedGroupKeys;
40
+
41
+ return project;
42
+ }
43
+
44
+
45
+ async function makeProject(orgId, oauthToken = OAUTH_TOKEN) {
46
+ const excludedOrgs = [
47
+ 1, // Mixpanel
48
+ 328203, // Mixpanel Demo
49
+ 1673847, // SE Demo
50
+ 1866253 // Demo Projects
51
+ ];
52
+ if (!orgId || !oauthToken) throw new Error('Missing orgId or oauthToken');
53
+ const url = `https://mixpanel.com/api/app/organizations/${orgId}/create-project`;
54
+ const projectPayload = {
55
+ "cluster_id": 1,
56
+ "project_name": `GTM Metrics: Test Env ${rand(1000, 9999)}`,
57
+ "timezone_id": 404
58
+ };
59
+
60
+ const payload = {
61
+ method: 'POST',
62
+
63
+ headers: {
64
+ Authorization: `Bearer ${oauthToken}`,
65
+ },
66
+ body: JSON.stringify(projectPayload)
67
+
68
+ };
69
+
70
+ const projectsReq = await fetch(url, payload);
71
+ const projectsRes = await projectsReq.json();
72
+ const { api_secret, id, name, token } = projectsRes.results;
73
+
74
+ const data = {
75
+ api_secret,
76
+ id,
77
+ name,
78
+ token,
79
+ url: `https://mixpanel.com/project/${id}/app/settings#project/${id}`
80
+
81
+ };
82
+
83
+ return data;
84
+ }
85
+
86
+ async function getUser(oauthToken = OAUTH_TOKEN) {
87
+ const user = {};
88
+ try {
89
+ if (oauthToken) {
90
+ const info = await fetch(`https://mixpanel.com/api/app/me/?include_workspace_users=false`, { headers: { Authorization: `Bearer ${oauthToken}` } });
91
+ const data = await info.json();
92
+ if (data?.results) {
93
+ const { user_name = "", user_email = "" } = data.results;
94
+ if (user_name) user.name = user_name;
95
+ if (user_email) user.email = user_email;
96
+ const foundOrg = Object.values(data.results.organizations).filter(o => o.name.includes(user_name))?.pop();
97
+ if (foundOrg) {
98
+ user.orgId = foundOrg.id?.toString();
99
+ user.orgName = foundOrg.name;
100
+ }
101
+ if (!foundOrg) {
102
+ // the name is not in the orgs, so we need to find the org in which the user is the owner
103
+ const ignoreProjects = [1673847, 1866253, 328203];
104
+ const possibleOrg = Object.values(data.results.organizations)
105
+ .filter(o => o.role === 'owner')
106
+ .filter(o => !ignoreProjects.includes(o.id))?.pop();
107
+ if (possibleOrg) {
108
+ user.orgId = possibleOrg?.id?.toString();
109
+ user.orgName = possibleOrg.name;
110
+ }
111
+ }
112
+ }
113
+ }
114
+ }
115
+ catch (err) {
116
+ console.error('get user err', err);
117
+ }
118
+
119
+ return user;
120
+ }
121
+
122
+
123
+ async function addGroupKeys(groupKeyDfns = [], projectId, oauthToken = OAUTH_TOKEN) {
124
+ const url = `https://mixpanel.com/api/app/projects/${projectId}/data-groups/`;
125
+ const results = [];
126
+ loopKeys: for (const { display_name, property_name } of groupKeyDfns) {
127
+ const body = {
128
+ display_name,
129
+ property_name
130
+ };
131
+ const payload = {
132
+ method: 'POST',
133
+ headers: {
134
+ Authorization: `Bearer ${oauthToken}`,
135
+ 'Content-Type': 'application/json'
136
+ },
137
+ body: JSON.stringify(body)
138
+ };
139
+
140
+ try {
141
+ const res = await fetch(url, payload);
142
+ const data = await res.json();
143
+ results.push(data?.results);
144
+ }
145
+ catch (err) {
146
+ console.error('add group keys err', err);
147
+ continue loopKeys;
148
+ }
149
+
150
+ }
151
+ return results;
152
+ }
153
+
154
+
155
+ module.exports = main;
@@ -15,7 +15,7 @@ const { domainSuffix, domainPrefix } = require('./defaults');
15
15
  /** @typedef {import('../types').Config} Config */
16
16
  /** @typedef {import('../types').EventConfig} EventConfig */
17
17
  /** @typedef {import('../types').ValueValid} ValueValid */
18
- /** @typedef {import('../types').EnrichedArray} hookArray */
18
+ /** @typedef {import('../types').HookedArray} hookArray */
19
19
  /** @typedef {import('../types').hookArrayOptions} hookArrayOptions */
20
20
  /** @typedef {import('../types').Person} Person */
21
21
  /** @typedef {import('../types').Funnel} Funnel */
@@ -23,6 +23,7 @@ const { domainSuffix, domainPrefix } = require('./defaults');
23
23
  let globalChance;
24
24
  let chanceInitialized = false;
25
25
 
26
+ const ACTUAL_NOW = dayjs.utc();
26
27
 
27
28
 
28
29
  /*
@@ -40,7 +41,6 @@ function initChance(seed) {
40
41
  if (process.env.SEED) seed = process.env.SEED; // Override seed with environment variable if available
41
42
  if (!chanceInitialized) {
42
43
  globalChance = new Chance(seed);
43
- if (global.MP_SIMULATION_CONFIG) global.MP_SIMULATION_CONFIG.chance = globalChance;
44
44
  chanceInitialized = true;
45
45
  }
46
46
  return globalChance;
@@ -52,11 +52,11 @@ function initChance(seed) {
52
52
  */
53
53
  function getChance() {
54
54
  if (!chanceInitialized) {
55
- const seed = process.env.SEED || global.MP_SIMULATION_CONFIG?.seed;
55
+ const seed = process.env.SEED || "";
56
56
  if (!seed) {
57
- return new Chance();
57
+ return new Chance(); // this is a new RNG and therefore not deterministic
58
58
  }
59
- return initChance(seed);
59
+ return initChance(seed);
60
60
  }
61
61
  return globalChance;
62
62
  }
@@ -97,7 +97,8 @@ function pick(items) {
97
97
  */
98
98
  function date(inTheLast = 30, isPast = true, format = 'YYYY-MM-DD') {
99
99
  const chance = getChance();
100
- const now = global.NOW ? dayjs.unix(global.NOW) : dayjs();
100
+ // const now = global.FIXED_NOW ? dayjs.unix(global.FIXED_NOW) : dayjs();
101
+ const now = ACTUAL_NOW;
101
102
  if (Math.abs(inTheLast) > 365 * 10) inTheLast = chance.integer({ min: 1, max: 180 });
102
103
  return function () {
103
104
  const when = chance.integer({ min: 0, max: Math.abs(inTheLast) });
@@ -150,14 +151,17 @@ function datesBetween(start, end) {
150
151
  /**
151
152
  * returns a random date
152
153
  * @param {any} start
153
- * @param {any} end=global.NOW
154
+ * @param {any} end
154
155
  */
155
- function day(start, end = global.NOW) {
156
+ function day(start, end) {
157
+ // if (!end) end = global.FIXED_NOW ? global.FIXED_NOW : dayjs().unix();
158
+ if (!start) start = ACTUAL_NOW.subtract(30, 'd').toISOString();
159
+ if (!end) end = ACTUAL_NOW.toISOString();
156
160
  const chance = getChance();
157
161
  const format = 'YYYY-MM-DD';
158
162
  return function (min, max) {
159
163
  start = dayjs(start);
160
- end = dayjs.unix(global.NOW);
164
+ end = dayjs(end);
161
165
  const diff = end.diff(start, 'day');
162
166
  const delta = chance.integer({ min: min, max: diff });
163
167
  const day = start.add(delta, 'day');
@@ -312,52 +316,6 @@ function range(a, b, step = 1) {
312
316
  };
313
317
 
314
318
 
315
- /**
316
- * create funnels out of random events
317
- * @param {EventConfig[]} events
318
- */
319
- function inferFunnels(events) {
320
- const createdFunnels = [];
321
- const firstEvents = events.filter((e) => e.isFirstEvent).map((e) => e.event);
322
- const usageEvents = events.filter((e) => !e.isFirstEvent).map((e) => e.event);
323
- const numFunnelsToCreate = Math.ceil(usageEvents.length);
324
- /** @type {Funnel} */
325
- const funnelTemplate = {
326
- sequence: [],
327
- conversionRate: 50,
328
- order: 'sequential',
329
- requireRepeats: false,
330
- props: {},
331
- timeToConvert: 1,
332
- isFirstFunnel: false,
333
- weight: 1
334
- };
335
- if (firstEvents.length) {
336
- for (const event of firstEvents) {
337
- createdFunnels.push({ ...clone(funnelTemplate), sequence: [event], isFirstFunnel: true, conversionRate: 100 });
338
- }
339
- }
340
-
341
- //at least one funnel with all usage events
342
- createdFunnels.push({ ...clone(funnelTemplate), sequence: usageEvents });
343
-
344
- //for the rest, make random funnels
345
- followUpFunnels: for (let i = 1; i < numFunnelsToCreate; i++) {
346
- /** @type {Funnel} */
347
- const funnel = { ...clone(funnelTemplate) };
348
- funnel.conversionRate = integer(25, 75);
349
- funnel.timeToConvert = integer(1, 10);
350
- funnel.weight = integer(1, 10);
351
- const sequence = shuffleArray(usageEvents).slice(0, integer(2, usageEvents.length));
352
- funnel.sequence = sequence;
353
- funnel.order = 'random';
354
- createdFunnels.push(funnel);
355
- }
356
-
357
- return createdFunnels;
358
-
359
- }
360
-
361
319
 
362
320
  /*
363
321
  ----
@@ -674,14 +632,14 @@ function validateEventConfig(events) {
674
632
  return cleanEventConfig;
675
633
  }
676
634
 
677
- function validateTime(chosenTime, earliestTime, latestTime) {
678
- if (!earliestTime) earliestTime = global.NOW - (60 * 60 * 24 * 30); // 30 days ago
679
- if (!latestTime) latestTime = global.NOW;
635
+ function validTime(chosenTime, earliestTime, latestTime) {
636
+ if (!earliestTime) earliestTime = global.FIXED_BEGIN ? global.FIXED_BEGIN : dayjs().subtract(30, 'd').unix(); // 30 days ago
637
+ if (!latestTime) latestTime = global.FIXED_NOW ? global.FIXED_NOW : dayjs().unix();
680
638
 
681
639
  if (typeof chosenTime === 'number') {
682
640
  if (chosenTime > 0) {
683
641
  if (chosenTime > earliestTime) {
684
- if (chosenTime < latestTime) {
642
+ if (chosenTime < (latestTime)) {
685
643
  return true;
686
644
  }
687
645
 
@@ -691,6 +649,17 @@ function validateTime(chosenTime, earliestTime, latestTime) {
691
649
  return false;
692
650
  }
693
651
 
652
+ function validEvent(row) {
653
+ if (!row) return false;
654
+ if (!row.event) return false;
655
+ if (!row.time) return false;
656
+ if (!row.device_id && !row.user_id) return false;
657
+ if (!row.insert_id) return false;
658
+ if (!row.source) return false;
659
+ if (typeof row.time !== 'string') return false;
660
+ return true;
661
+ }
662
+
694
663
 
695
664
  /*
696
665
  ----
@@ -698,68 +667,7 @@ META
698
667
  ----
699
668
  */
700
669
 
701
- /**
702
- * our meta programming function which lets you mutate items as they are pushed into an array
703
- * @param {any[]} arr
704
- * @param {hookArrayOptions} opts
705
- * @returns {hookArray}}
706
- */
707
- function hookArray(arr = [], opts = {}) {
708
- const { hook = a => a, type = "", ...rest } = opts;
709
-
710
- function transformThenPush(item) {
711
- if (item === null) return false;
712
- if (item === undefined) return false;
713
- if (typeof item === 'object') {
714
- if (Object.keys(item).length === 0) return false;
715
- }
716
-
717
- //hook is passed an array
718
- if (Array.isArray(item)) {
719
- for (const i of item) {
720
- try {
721
- const enriched = hook(i, type, rest);
722
- if (Array.isArray(enriched)) enriched.forEach(e => arr.push(e));
723
- else arr.push(enriched);
724
-
725
- }
726
- catch (e) {
727
- console.error(`\n\nyour hook had an error\n\n`, e);
728
- arr.push(i);
729
- return false;
730
- }
731
-
732
- }
733
- return true;
734
- }
735
-
736
- //hook is passed a single item
737
- else {
738
- try {
739
- const enriched = hook(item, type, rest);
740
- if (Array.isArray(enriched)) enriched.forEach(e => arr.push(e));
741
- else arr.push(enriched);
742
- return true;
743
- }
744
- catch (e) {
745
- console.error(`\n\nyour hook had an error\n\n`, e);
746
- arr.push(item);
747
- return false;
748
- }
749
- }
750
-
751
- }
752
670
 
753
- /** @type {hookArray} */
754
- // @ts-ignore
755
- const enrichedArray = arr;
756
-
757
-
758
- enrichedArray.hookPush = transformThenPush;
759
-
760
-
761
- return enrichedArray;
762
- };
763
671
 
764
672
  /**
765
673
  * @param {Config} config
@@ -770,7 +678,7 @@ function buildFileNames(config) {
770
678
  extension = format === "csv" ? "csv" : "json";
771
679
  // const current = dayjs.utc().format("MM-DD-HH");
772
680
  let simName = config.simulationName;
773
- let writeDir = "./";
681
+ let writeDir = typeof config.writeToDisk === 'string' ? config.writeToDisk : "./";
774
682
  if (config.writeToDisk) {
775
683
  const dataFolder = path.resolve("./data");
776
684
  if (existsSync(dataFolder)) writeDir = dataFolder;
@@ -835,7 +743,6 @@ function buildFileNames(config) {
835
743
  * @param {[string, number][]} arrayOfArrays
836
744
  */
837
745
  function progress(arrayOfArrays) {
838
- // @ts-ignore
839
746
  readline.cursorTo(process.stdout, 0);
840
747
  let message = "";
841
748
  for (const status of arrayOfArrays) {
@@ -873,8 +780,9 @@ CORE
873
780
  */
874
781
 
875
782
  //the function which generates $distinct_id + $anonymous_ids, $session_ids, and created, skewing towards the present
876
- function generateUser(user_id, numDays, amplitude = 1, frequency = 1, skew = 1) {
783
+ function generateUser(user_id, opts, amplitude = 1, frequency = 1, skew = 1) {
877
784
  const chance = getChance();
785
+ const { numDays, isAnonymous, hasAvatar, hasAnonIds, hasSessionIds } = opts;
878
786
  // Uniformly distributed `u`, then skew applied
879
787
  let u = Math.pow(chance.random(), skew);
880
788
 
@@ -886,16 +794,18 @@ function generateUser(user_id, numDays, amplitude = 1, frequency = 1, skew = 1)
886
794
 
887
795
  // Clamp values to ensure they are within the desired range
888
796
  daysAgoBorn = Math.min(daysAgoBorn, numDays);
797
+ const props = person(user_id, daysAgoBorn, isAnonymous, hasAvatar, hasAnonIds, hasSessionIds);
889
798
 
890
799
  const user = {
891
800
  distinct_id: user_id,
892
- ...person(numDays),
801
+ ...props,
893
802
  };
894
803
 
895
804
 
896
805
  return user;
897
806
  }
898
807
 
808
+ let soupHits = 0;
899
809
  /**
900
810
  * build sign waves basically
901
811
  * @param {number} [earliestTime]
@@ -903,8 +813,8 @@ function generateUser(user_id, numDays, amplitude = 1, frequency = 1, skew = 1)
903
813
  * @param {number} [peaks=5]
904
814
  */
905
815
  function TimeSoup(earliestTime, latestTime, peaks = 5, deviation = 2, mean = 0) {
906
- if (!earliestTime) earliestTime = global.NOW - (60 * 60 * 24 * 30); // 30 days ago
907
- if (!latestTime) latestTime = global.NOW;
816
+ if (!earliestTime) earliestTime = global.FIXED_BEGIN ? global.FIXED_BEGIN : dayjs().subtract(30, 'd').unix(); // 30 days ago
817
+ if (!latestTime) latestTime = global.FIXED_NOW ? global.FIXED_NOW : dayjs().unix();
908
818
  const chance = getChance();
909
819
  const totalRange = latestTime - earliestTime;
910
820
  const chunkSize = totalRange / peaks;
@@ -921,8 +831,9 @@ function TimeSoup(earliestTime, latestTime, peaks = 5, deviation = 2, mean = 0)
921
831
  let isValidTime = false;
922
832
  do {
923
833
  iterations++;
834
+ soupHits++;
924
835
  offset = chance.normal({ mean: mean, dev: chunkSize / deviation });
925
- isValidTime = validateTime(chunkMid + offset, earliestTime, latestTime);
836
+ isValidTime = validTime(chunkMid + offset, earliestTime, latestTime);
926
837
  if (iterations > 25000) {
927
838
  throw `${iterations} iterations... exceeded`;
928
839
  }
@@ -946,15 +857,17 @@ function TimeSoup(earliestTime, latestTime, peaks = 5, deviation = 2, mean = 0)
946
857
  * @param {string} userId
947
858
  * @param {number} bornDaysAgo=30
948
859
  * @param {boolean} isAnonymous
860
+ * @param {boolean} hasAvatar
861
+ * @param {boolean} hasAnonIds
862
+ * @param {boolean} hasSessionIds
949
863
  * @return {Person}
950
864
  */
951
- function person(userId, bornDaysAgo = 30, isAnonymous = false) {
865
+ function person(userId, bornDaysAgo = 30, isAnonymous = false, hasAvatar = false, hasAnonIds = false, hasSessionIds = false) {
952
866
  const chance = getChance();
953
867
  //names and photos
954
868
  const l = chance.letter.bind(chance);
955
869
  let gender = chance.pickone(['male', 'female']);
956
870
  if (!gender) gender = "female";
957
- // @ts-ignore
958
871
  let first = chance.first({ gender });
959
872
  let last = chance.last();
960
873
  let name = `${first} ${last}`;
@@ -963,7 +876,7 @@ function person(userId, bornDaysAgo = 30, isAnonymous = false) {
963
876
  let randomAvatarNumber = integer(1, 99);
964
877
  let avPath = gender === 'male' ? `/men/${randomAvatarNumber}.jpg` : `/women/${randomAvatarNumber}.jpg`;
965
878
  let avatar = avatarPrefix + avPath;
966
- let created = dayjs.unix(global.NOW).subtract(bornDaysAgo, 'day').format('YYYY-MM-DD');
879
+ let created = dayjs().subtract(bornDaysAgo, 'day').format('YYYY-MM-DD');
967
880
  // const created = date(bornDaysAgo, true)();
968
881
 
969
882
 
@@ -982,21 +895,23 @@ function person(userId, bornDaysAgo = 30, isAnonymous = false) {
982
895
  user.name = "Anonymous User";
983
896
  user.email = l() + l() + `*`.repeat(integer(3, 6)) + l() + `@` + l() + `*`.repeat(integer(3, 6)) + l() + `.` + choose(domainSuffix);
984
897
  delete user.avatar;
985
-
986
898
  }
987
899
 
900
+ if (!hasAvatar) delete user.avatar;
901
+
988
902
  //anon Ids
989
- if (global.MP_SIMULATION_CONFIG?.anonIds) {
903
+ if (hasAnonIds) {
990
904
  const clusterSize = integer(2, 10);
991
905
  for (let i = 0; i < clusterSize; i++) {
992
906
  const anonId = uid(42);
993
907
  user.anonymousIds.push(anonId);
994
908
  }
995
-
996
909
  }
997
910
 
911
+ if (!hasAnonIds) delete user.anonymousIds;
912
+
998
913
  //session Ids
999
- if (global.MP_SIMULATION_CONFIG?.sessionIds) {
914
+ if (hasSessionIds) {
1000
915
  const sessionSize = integer(5, 30);
1001
916
  for (let i = 0; i < sessionSize; i++) {
1002
917
  const sessionId = [uid(5), uid(5), uid(5), uid(5)].join("-");
@@ -1004,6 +919,8 @@ function person(userId, bornDaysAgo = 30, isAnonymous = false) {
1004
919
  }
1005
920
  }
1006
921
 
922
+ if (!hasSessionIds) delete user.sessionIds;
923
+
1007
924
  return user;
1008
925
  };
1009
926
 
@@ -1012,32 +929,32 @@ function person(userId, bornDaysAgo = 30, isAnonymous = false) {
1012
929
 
1013
930
  //UNUSED
1014
931
 
1015
- function fixFunkyTime(earliestTime, latestTime) {
1016
- if (!earliestTime) earliestTime = global.NOW - (60 * 60 * 24 * 30); // 30 days ago
1017
- // if (typeof earliestTime !== "number") {
1018
- // if (parseInt(earliestTime) > 0) earliestTime = parseInt(earliestTime);
1019
- // if (dayjs(earliestTime).isValid()) earliestTime = dayjs(earliestTime).unix();
1020
- // }
1021
- if (typeof earliestTime !== "number") earliestTime = dayjs.unix(earliestTime).unix();
1022
- if (typeof latestTime !== "number") latestTime = global.NOW;
1023
- if (typeof latestTime === "number" && latestTime > global.NOW) latestTime = global.NOW;
1024
- if (earliestTime > latestTime) {
1025
- const tempEarlyTime = earliestTime;
1026
- const tempLateTime = latestTime;
1027
- earliestTime = tempLateTime;
1028
- latestTime = tempEarlyTime;
1029
- }
1030
- if (earliestTime === latestTime) {
1031
- earliestTime = dayjs.unix(earliestTime)
1032
- .subtract(integer(1, 14), "day")
1033
- .subtract(integer(1, 23), "hour")
1034
- .subtract(integer(1, 59), "minute")
1035
- .subtract(integer(1, 59), "second")
1036
- .unix();
1037
- }
1038
- return [earliestTime, latestTime];
1039
-
1040
- }
932
+ // function fixFunkyTime(earliestTime, latestTime) {
933
+ // if (!earliestTime) earliestTime = global.NOW - (60 * 60 * 24 * 30); // 30 days ago
934
+ // // if (typeof earliestTime !== "number") {
935
+ // // if (parseInt(earliestTime) > 0) earliestTime = parseInt(earliestTime);
936
+ // // if (dayjs(earliestTime).isValid()) earliestTime = dayjs(earliestTime).unix();
937
+ // // }
938
+ // if (typeof earliestTime !== "number") earliestTime = dayjs.unix(earliestTime).unix();
939
+ // if (typeof latestTime !== "number") latestTime = global.NOW;
940
+ // if (typeof latestTime === "number" && latestTime > global.NOW) latestTime = global.NOW;
941
+ // if (earliestTime > latestTime) {
942
+ // const tempEarlyTime = earliestTime;
943
+ // const tempLateTime = latestTime;
944
+ // earliestTime = tempLateTime;
945
+ // latestTime = tempEarlyTime;
946
+ // }
947
+ // if (earliestTime === latestTime) {
948
+ // earliestTime = dayjs.unix(earliestTime)
949
+ // .subtract(integer(1, 14), "day")
950
+ // .subtract(integer(1, 23), "hour")
951
+ // .subtract(integer(1, 59), "minute")
952
+ // .subtract(integer(1, 59), "second")
953
+ // .unix();
954
+ // }
955
+ // return [earliestTime, latestTime];
956
+
957
+ // }
1041
958
 
1042
959
 
1043
960
 
@@ -1078,7 +995,10 @@ module.exports = {
1078
995
 
1079
996
  initChance,
1080
997
  getChance,
1081
- validateTime,
998
+
999
+ validTime,
1000
+ validEvent,
1001
+
1082
1002
  boxMullerRandom,
1083
1003
  applySkew,
1084
1004
  mapToRange,
@@ -1100,12 +1020,10 @@ module.exports = {
1100
1020
  shuffleOutside,
1101
1021
  interruptArray,
1102
1022
  generateUser,
1103
- hookArray,
1104
1023
  optimizedBoxMuller,
1105
1024
  buildFileNames,
1106
1025
  streamJSON,
1107
1026
  streamCSV,
1108
- inferFunnels,
1109
1027
  datesBetween,
1110
1028
  weighChoices
1111
1029
  };