make-mp-data 1.4.1 → 1.4.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.
package/index.js CHANGED
@@ -6,32 +6,43 @@ by AK
6
6
  ak@mixpanel.com
7
7
  */
8
8
 
9
+ //todo: ads-data
10
+ //todo: cart analysis
9
11
  //todo: churn ... is churnFunnel, possible to return, etc
10
12
  //todo: fixedTimeFunnel? if set this funnel will occur for all users at the same time ['cart charged', 'charge complete']
11
- //todo defaults!!!
12
- //todo ads-data
13
-
14
-
13
+
14
+ /** @typedef {import('./types.d.ts').Config} Config */
15
+ /** @typedef {import('./types.d.ts').EventConfig} EventConfig */
16
+ /** @typedef {import('./types.d.ts').Funnel} Funnel */
17
+ /** @typedef {import('./types.d.ts').Person} Person */
18
+ /** @typedef {import('./types.d.ts').SCDTableRow} SCDTableRow */
19
+ /** @typedef {import('./types.d.ts').UserProfile} UserProfile */
20
+ /** @typedef {import('./types.d.ts').EventSpec} EventSpec */
21
+
15
22
  const dayjs = require("dayjs");
16
23
  const utc = require("dayjs/plugin/utc");
17
24
  dayjs.extend(utc);
18
25
  const NOW = dayjs('2024-02-02').unix(); //this is a FIXED POINT and we will shift it later
19
26
  global.NOW = NOW;
20
- const mp = require("mixpanel-import");
27
+
28
+ const os = require("os");
21
29
  const path = require("path");
22
30
  const { comma, bytesHuman, makeName, md5, clone, tracker, uid } = require("ak-tools");
23
31
  const { generateLineChart } = require('./chart.js');
24
32
  const { version } = require('./package.json');
25
- const os = require("os");
33
+ const mp = require("mixpanel-import");
26
34
  const metrics = tracker("make-mp-data", "db99eb8f67ae50949a13c27cacf57d41", os.userInfo().username);
27
35
 
28
-
29
36
  const u = require("./utils.js");
30
- const cliParams = require("./cli.js");
37
+ const getCliParams = require("./cli.js");
38
+ const { campaigns, devices, locations } = require('./defaults.js');
31
39
 
32
40
  let VERBOSE = false;
33
41
  let isCLI = false;
42
+ /** @type {Config} */
34
43
  let CONFIG;
44
+
45
+ let DEFAULTS;
35
46
  require('dotenv').config();
36
47
 
37
48
 
@@ -40,25 +51,20 @@ function track(name, props, ...rest) {
40
51
  metrics(name, props, ...rest);
41
52
  }
42
53
 
43
- /** @typedef {import('./types.d.ts').Config} Config */
44
- /** @typedef {import('./types.d.ts').EventConfig} EventConfig */
45
- /** @typedef {import('./types.d.ts').Funnel} Funnel */
46
- /** @typedef {import('./types.d.ts').Person} Person */
47
- /** @typedef {import('./types.d.ts').SCDTableRow} SCDTableRow */
48
- /** @typedef {import('./types.d.ts').UserProfile} UserProfile */
49
- /** @typedef {import('./types.d.ts').EventSpec} EventSpec */
54
+
50
55
 
51
56
  /**
52
57
  * generates fake mixpanel data
53
58
  * @param {Config} config
54
59
  */
55
60
  async function main(config) {
56
- //PARAMS
61
+
62
+ //seed the random number generator
63
+ // ^ this is critical; same seed = same data; seed can be passed in as an env var or in the config
57
64
  const seedWord = process.env.SEED || config.seed || "hello friend!";
58
65
  config.seed = seedWord;
59
66
  u.initChance(seedWord);
60
- const chance = u.getChance();
61
- config.chance = chance;
67
+ const chance = u.getChance(); // ! this is the only safe way to get the chance instance
62
68
  let {
63
69
  seed,
64
70
  numEvents = 100000,
@@ -67,10 +73,9 @@ async function main(config) {
67
73
  epochStart = 0,
68
74
  epochEnd = dayjs().unix(),
69
75
  events = [{ event: "foo" }, { event: "bar" }, { event: "baz" }],
70
- superProps = { platform: ["web", "iOS", "Android"] },
76
+ superProps = { luckyNumber: [2, 2, 4, 4, 42, 42, 42, 2, 2, 4, 4, 42, 42, 42, 420] },
71
77
  funnels = [],
72
78
  userProps = {
73
- favoriteColor: ["red", "green", "blue", "yellow"],
74
79
  spiritAnimal: chance.animal.bind(chance),
75
80
  },
76
81
  scdProps = {},
@@ -88,10 +93,20 @@ async function main(config) {
88
93
  makeChart = false,
89
94
  soup = {},
90
95
  hook = (record) => record,
96
+ hasAdSpend = false,
97
+ hasCampaigns = false,
98
+ hasLocation = false,
99
+ isAnonymous = false,
100
+ hasBrowser = false,
101
+ hasAndroidDevices = false,
102
+ hasDesktopDevices = false,
103
+ hasIOSDevices = false
91
104
  } = config;
105
+
92
106
  if (!config.superProps) config.superProps = superProps;
93
107
  if (!config.userProps || Object.keys(config?.userProps)) config.userProps = userProps;
94
- VERBOSE = verbose;
108
+
109
+
95
110
  config.simulationName = makeName();
96
111
  const { simulationName } = config;
97
112
  if (epochStart && !numDays) numDays = dayjs.unix(epochEnd).diff(dayjs.unix(epochStart), "day");
@@ -123,13 +138,33 @@ async function main(config) {
123
138
  config.makeChart = makeChart;
124
139
  config.soup = soup;
125
140
  config.hook = hook;
141
+ config.hasAdSpend = hasAdSpend;
142
+ config.hasCampaigns = hasCampaigns;
143
+ config.hasLocation = hasLocation;
144
+ config.isAnonymous = isAnonymous;
145
+ config.hasBrowser = hasBrowser;
146
+ config.hasAndroidDevices = hasAndroidDevices;
147
+ config.hasDesktopDevices = hasDesktopDevices;
148
+ config.hasIOSDevices = hasIOSDevices;
126
149
 
127
150
  //event validation
128
151
  const validatedEvents = u.validateEventConfig(events);
129
152
  events = validatedEvents;
130
153
  config.events = validatedEvents;
154
+
155
+ //globals
131
156
  global.MP_SIMULATION_CONFIG = config;
132
157
  CONFIG = config;
158
+ VERBOSE = verbose;
159
+ DEFAULTS = {
160
+ locations: u.pickAWinner(locations, 0),
161
+ iOSDevices: u.pickAWinner(devices.iosDevices, 0),
162
+ androidDevices: u.pickAWinner(devices.androidDevices, 0),
163
+ desktopDevices: u.pickAWinner(devices.desktopDevices, 0),
164
+ browsers: u.pickAWinner(devices.browsers, 0),
165
+ campaigns: u.pickAWinner(campaigns, 0),
166
+ };
167
+
133
168
  const runId = uid(42);
134
169
  let trackingParams = { runId, seed, numEvents, numUsers, numDays, anonIds, sessionIds, format, targetToken: token, region, writeToDisk, isCLI, version };
135
170
  track('start simulation', trackingParams);
@@ -153,7 +188,7 @@ async function main(config) {
153
188
 
154
189
  // if no funnels, make some out of events...
155
190
  if (!funnels || !funnels.length) {
156
- funnels = inferFunnels(events);
191
+ funnels = u.inferFunnels(events);
157
192
  config.funnels = funnels;
158
193
  CONFIG = config;
159
194
  }
@@ -163,11 +198,19 @@ async function main(config) {
163
198
  loopUsers: for (let i = 1; i < numUsers + 1; i++) {
164
199
  u.progress("users", i);
165
200
  const userId = chance.guid();
166
- // const user = u.generateUser(userId, numDays, amp, freq, skew);
167
- const user = u.generateUser(userId, numDays);
201
+ const user = u.person(userId, numDays, isAnonymous);
168
202
  const { distinct_id, created, anonymousIds, sessionIds } = user;
169
203
  let numEventsPreformed = 0;
170
204
 
205
+ if (hasLocation) {
206
+ const location = u.choose(DEFAULTS.locations().map(l => { delete l.country; return l; }));
207
+ for (const key in location) {
208
+ user[key] = location[key];
209
+ }
210
+ }
211
+
212
+
213
+
171
214
  // profile creation
172
215
  const profile = makeProfile(userProps, user);
173
216
  userProfilesData.hookPush(profile);
@@ -379,7 +422,7 @@ async function main(config) {
379
422
  fixData: true,
380
423
  verbose: false,
381
424
  forceStream: true,
382
- strict: true,
425
+ strict: false, //! sometimes we get events in the future... it happens
383
426
  dryRun: false,
384
427
  abridged: false,
385
428
  fixJson: true,
@@ -452,27 +495,42 @@ async function main(config) {
452
495
  * @param {Boolean} isFirstEvent=false
453
496
  */
454
497
  function makeEvent(distinct_id, anonymousIds, sessionIds, earliestTime, chosenEvent, superProps, groupKeys, isFirstEvent = false) {
455
- const { mean = 0, dev = 2, peaks = 5 } = CONFIG.soup;
498
+ const chance = u.getChance();
499
+ const { mean = 0, deviation = 2, peaks = 5 } = CONFIG.soup;
500
+ const { hasAndroidDevices, hasBrowser, hasCampaigns, hasDesktopDevices, hasIOSDevices, hasLocation } = CONFIG;
456
501
  //event model
457
502
  const eventTemplate = {
458
503
  event: chosenEvent.event,
459
504
  source: "dm4",
460
505
  };
461
506
 
507
+ let defaultProps = {};
508
+ let devicePool = [];
509
+ if (hasLocation) defaultProps.location = DEFAULTS.locations().map(l => { delete l.country_code; return l; });
510
+ if (hasBrowser) defaultProps.browser = DEFAULTS.browsers();
511
+ if (hasAndroidDevices) devicePool.push(DEFAULTS.androidDevices());
512
+ if (hasIOSDevices) devicePool.push(DEFAULTS.iOSDevices());
513
+ if (hasDesktopDevices) devicePool.push(DEFAULTS.desktopDevices());
514
+ // we don't always have campaigns, because of attribution
515
+ if (hasCampaigns && chance.bool({ likelihood: 25 })) defaultProps.campaigns = DEFAULTS.campaigns();
516
+ const devices = devicePool.flat();
517
+ if (devices.length) defaultProps.device = devices;
518
+
519
+
462
520
  //event time
463
521
  if (earliestTime > NOW) {
464
522
  earliestTime = dayjs.unix(NOW).subtract(2, 'd').unix();
465
523
  };
466
524
 
467
525
  if (isFirstEvent) eventTemplate.time = dayjs.unix(earliestTime).toISOString();
468
- if (!isFirstEvent) eventTemplate.time = u.TimeSoup(earliestTime, NOW, peaks, dev, mean);
526
+ if (!isFirstEvent) eventTemplate.time = u.TimeSoup(earliestTime, NOW, peaks, deviation, mean);
469
527
 
470
528
  // anonymous and session ids
471
- if (CONFIG?.anonIds) eventTemplate.device_id = CONFIG.chance.pickone(anonymousIds);
472
- if (CONFIG?.sessionIds) eventTemplate.session_id = CONFIG.chance.pickone(sessionIds);
529
+ if (CONFIG?.anonIds) eventTemplate.device_id = chance.pickone(anonymousIds);
530
+ if (CONFIG?.sessionIds) eventTemplate.session_id = chance.pickone(sessionIds);
473
531
 
474
532
  //sometimes have a user_id
475
- if (!isFirstEvent && CONFIG.chance.bool({ likelihood: 42 })) eventTemplate.user_id = distinct_id;
533
+ if (!isFirstEvent && chance.bool({ likelihood: 42 })) eventTemplate.user_id = distinct_id;
476
534
 
477
535
  // ensure that there is a user_id or device_id
478
536
  if (!eventTemplate.user_id && !eventTemplate.device_id) eventTemplate.user_id = distinct_id;
@@ -489,6 +547,34 @@ function makeEvent(distinct_id, anonymousIds, sessionIds, earliestTime, chosenEv
489
547
  }
490
548
  }
491
549
 
550
+ //iterate through default properties
551
+ for (const key in defaultProps) {
552
+ if (Array.isArray(defaultProps[key])) {
553
+ const choice = u.choose(defaultProps[key]);
554
+ if (typeof choice === "string") {
555
+ if (!eventTemplate[key]) eventTemplate[key] = choice;
556
+ }
557
+
558
+ else if (Array.isArray(choice)) {
559
+ for (const subChoice of choice) {
560
+ if (!eventTemplate[key]) eventTemplate[key] = subChoice;
561
+ }
562
+ }
563
+
564
+ else if (typeof choice === "object") {
565
+ for (const subKey in choice) {
566
+ if (Array.isArray(choice[subKey])) {
567
+ const subChoice = u.choose(choice[subKey]);
568
+ if (!eventTemplate[subKey]) eventTemplate[subKey] = subChoice;
569
+ }
570
+ if (!eventTemplate[subKey]) eventTemplate[subKey] = choice[subKey];
571
+ }
572
+ }
573
+
574
+
575
+ }
576
+ }
577
+
492
578
  //iterate through groups
493
579
  for (const groupPair of groupKeys) {
494
580
  const groupKey = groupPair[0];
@@ -518,6 +604,7 @@ function makeEvent(distinct_id, anonymousIds, sessionIds, earliestTime, chosenEv
518
604
  * @return {[EventSpec[], Boolean]}
519
605
  */
520
606
  function makeFunnel(funnel, user, profile, scd, firstEventTime, config) {
607
+ const chance = u.getChance();
521
608
  const { hook } = config;
522
609
  hook(funnel, "funnel-pre", { user, profile, scd, funnel, config });
523
610
  let {
@@ -532,6 +619,7 @@ function makeFunnel(funnel, user, profile, scd, firstEventTime, config) {
532
619
  const { superProps, groupKeys } = config;
533
620
  const { name, email } = profile;
534
621
 
622
+ //choose the properties for this funnel
535
623
  const chosenFunnelProps = { ...props, ...superProps };
536
624
  for (const key in props) {
537
625
  try {
@@ -563,7 +651,7 @@ function makeFunnel(funnel, user, profile, scd, firstEventTime, config) {
563
651
  .reduce((acc, step) => {
564
652
  if (!requireRepeats) {
565
653
  if (acc.find(e => e.event === step.event)) {
566
- if (config.chance.bool({ likelihood: 50 })) {
654
+ if (chance.bool({ likelihood: 50 })) {
567
655
  conversionRate = Math.floor(conversionRate * 1.25); //increase conversion rate
568
656
  acc.push(step);
569
657
  }
@@ -583,7 +671,7 @@ function makeFunnel(funnel, user, profile, scd, firstEventTime, config) {
583
671
  return acc;
584
672
  }, []);
585
673
 
586
- let doesUserConvert = config.chance.bool({ likelihood: conversionRate });
674
+ let doesUserConvert = chance.bool({ likelihood: conversionRate });
587
675
  let numStepsUserWillTake = sequence.length;
588
676
  if (!doesUserConvert) numStepsUserWillTake = u.integer(1, sequence.length - 1);
589
677
  const funnelTotalRelativeTimeInHours = timeToConvert / numStepsUserWillTake;
@@ -677,49 +765,6 @@ function makeFunnel(funnel, user, profile, scd, firstEventTime, config) {
677
765
  }
678
766
 
679
767
 
680
- function inferFunnels(events) {
681
- const createdFunnels = [];
682
- const firstEvents = events.filter((e) => e.isFirstEvent).map((e) => e.event);
683
- const usageEvents = events.filter((e) => !e.isFirstEvent).map((e) => e.event);
684
- const numFunnelsToCreate = Math.ceil(usageEvents.length);
685
- /** @type {Funnel} */
686
- const funnelTemplate = {
687
- sequence: [],
688
- conversionRate: 50,
689
- order: 'sequential',
690
- requireRepeats: false,
691
- props: {},
692
- timeToConvert: 1,
693
- isFirstFunnel: false,
694
- weight: 1
695
- };
696
- if (firstEvents.length) {
697
- for (const event of firstEvents) {
698
- createdFunnels.push({ ...clone(funnelTemplate), sequence: [event], isFirstFunnel: true, conversionRate: 100 });
699
- }
700
- }
701
-
702
- //at least one funnel with all usage events
703
- createdFunnels.push({ ...clone(funnelTemplate), sequence: usageEvents });
704
-
705
- //for the rest, make random funnels
706
- followUpFunnels: for (let i = 1; i < numFunnelsToCreate; i++) {
707
- /** @type {Funnel} */
708
- const funnel = { ...clone(funnelTemplate) };
709
- funnel.conversionRate = u.integer(25, 75);
710
- funnel.timeToConvert = u.integer(1, 10);
711
- funnel.weight = u.integer(1, 10);
712
- const sequence = u.shuffleArray(usageEvents).slice(0, u.integer(2, usageEvents.length));
713
- funnel.sequence = sequence;
714
- funnel.order = 'random';
715
- createdFunnels.push(funnel);
716
- }
717
-
718
- return createdFunnels;
719
-
720
- }
721
-
722
-
723
768
  function makeProfile(props, defaults) {
724
769
  //build the spec
725
770
  const profile = {
@@ -770,8 +815,69 @@ function makeSCD(prop, scdKey, distinct_id, mutations, created) {
770
815
  return scdEntries;
771
816
  }
772
817
 
773
-
774
-
818
+ //todo
819
+ // function makeAdSpend(spec, usersCreated, specialDays = []) {
820
+ // const networks = [
821
+ // ...new Set(
822
+ // attribution
823
+ // .filter(camp => !["$organic", "$referral"].includes(camp.utm_source.join()))
824
+ // .map(network => network.utm_source)
825
+ // .flat()
826
+ // )
827
+ // ];
828
+
829
+ // const campaigns = attribution.slice().pop().utm_campaign;
830
+ // const campaignParams = attribution.slice().pop();
831
+ // const { startDate, endDate } = spec;
832
+ // const days = getDeltaDays(startDate, endDate);
833
+ // const numDays = days.length;
834
+ // const data = [];
835
+ // for (const network of networks) {
836
+ // for (const day of days) {
837
+ // //cost per acquisition ~ $10-50
838
+ // let CAC = chance.integer({ min: 10, max: 50 });
839
+ // // daily spend is total users / total days * CAC / num networks
840
+ // let cost = Math.floor(((usersCreated / numDays) * CAC) / networks.length);
841
+ // // CTR ~ 0.5-10%
842
+ // let clicks = Math.floor(cost / chance.floating({ min: 0.5, max: 10 }));
843
+ // //boost CTR on "special days" ~ 15-50%
844
+ // if (day in specialDays) clicks *= Math.floor(clicks * chance.floating({ min: 1.15, max: 1.5 }));
845
+ // //impressions ~100-500 * cost
846
+ // let impressions = Math.floor(cost * chance.integer({ min: 100, max: 500 }));
847
+ // // views ~ 25-80% of impressions
848
+ // let views = Math.floor(impressions * chance.floating({ min: 0.25, max: 0.8 }));
849
+
850
+ // let campaign_name = chance.pickone(campaigns);
851
+ // let campaign_id = md5(campaign_name);
852
+ // data.push({
853
+ // event: "ad data",
854
+ // properties: {
855
+ // cost,
856
+ // clicks,
857
+ // impressions,
858
+ // campaign_name,
859
+ // campaign_id,
860
+ // network,
861
+ // views,
862
+
863
+ // //addendum
864
+ // utm_campaign: campaign_name,
865
+ // utm_source: network,
866
+ // utm_medium: campaignParams.utm_medium,
867
+ // utm_content: campaignParams.utm_content,
868
+ // utm_term: campaignParams.utm_term,
869
+
870
+ // source: network,
871
+ // $insert_id: campaign_id,
872
+ // time: dayjs.utc(day).hour(12).unix(),
873
+ // $source: "DM3",
874
+ // $mp_lib: "DM3"
875
+ // }
876
+ // });
877
+ // }
878
+ // }
879
+ // return data;
880
+ // }
775
881
 
776
882
 
777
883
 
@@ -782,7 +888,7 @@ function makeSCD(prop, scdKey, distinct_id, mutations, created) {
782
888
  // this is for CLI
783
889
  if (require.main === module) {
784
890
  isCLI = true;
785
- const args = cliParams();
891
+ const args = getCliParams();
786
892
  // @ts-ignore
787
893
  let { token, seed, format, numDays, numUsers, numEvents, region, writeToDisk, complex = false, sessionIds, anonIds } = args;
788
894
  // @ts-ignore
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "make-mp-data",
3
- "version": "1.4.01",
3
+ "version": "1.4.02",
4
4
  "description": "builds all mixpanel primitives for a given project",
5
5
  "main": "index.js",
6
6
  "types": "types.d.ts",
@@ -47,7 +47,7 @@
47
47
  "chartjs-node-canvas": "^4.1.6",
48
48
  "dayjs": "^1.11.11",
49
49
  "dotenv": "^16.4.5",
50
- "mixpanel-import": "^2.5.54",
50
+ "mixpanel-import": "^2.5.55",
51
51
  "yargs": "^17.7.2"
52
52
  },
53
53
  "devDependencies": {
@@ -57,4 +57,4 @@
57
57
  "jest": {
58
58
  "preset": "./tests/jest.config.js"
59
59
  }
60
- }
60
+ }
package/schemas/simple.js CHANGED
@@ -31,6 +31,16 @@ const config = {
31
31
  region: "US",
32
32
  anonIds: false, //if true, anonymousIds are created for each user
33
33
  sessionIds: false, //if true, sessionIds are created for each user
34
+ hasAdSpend: false,
35
+
36
+ hasLocation: true,
37
+ hasAndroidDevices: true,
38
+ hasIOSDevices: true,
39
+ hasDesktopDevices: true,
40
+ hasBrowser: true,
41
+ hasCampaigns: true,
42
+ isAnonymous: false,
43
+
34
44
 
35
45
  events: [
36
46
  {
package/scratch.mjs CHANGED
@@ -10,10 +10,11 @@ import execSync from 'child_process';
10
10
 
11
11
  /** @type {main.Config} */
12
12
  const spec = {
13
- ...funnels,
13
+ ...simple,
14
14
  writeToDisk: false,
15
15
  verbose: true,
16
- makeChart: false,
16
+ makeChart: false,
17
+ token: "e98e6af94f6ddfb5e967fa265484539a"
17
18
  };
18
19
 
19
20
 
@@ -43,7 +43,8 @@ const { applySkew,
43
43
  validateEventConfig,
44
44
  validateTime,
45
45
  interruptArray,
46
- optimizedBoxMuller
46
+ optimizedBoxMuller,
47
+ inferFunnels
47
48
  } = require('../utils');
48
49
 
49
50
 
@@ -374,7 +375,7 @@ describe('utilities', () => {
374
375
 
375
376
 
376
377
  test('person: fields', () => {
377
- const generatedPerson = person();
378
+ const generatedPerson = person('myId');
378
379
  expect(generatedPerson).toHaveProperty('name');
379
380
  expect(generatedPerson).toHaveProperty('email');
380
381
  expect(generatedPerson).toHaveProperty('avatar');
@@ -628,8 +629,8 @@ describe('utilities', () => {
628
629
  const mean = values.reduce((sum, val) => sum + val, 0) / values.length;
629
630
  const variance = values.reduce((sum, val) => sum + Math.pow(val - mean, 2), 0) / values.length;
630
631
  const stdDev = Math.sqrt(variance);
631
- expect(mean).toBeCloseTo(0, 1);
632
- expect(stdDev).toBeCloseTo(1, 1);
632
+ expect(mean).toBeLessThan(1);
633
+ expect(stdDev).toBeLessThan(1);
633
634
  });
634
635
 
635
636
 
package/types.d.ts CHANGED
@@ -12,10 +12,21 @@ declare namespace main {
12
12
  epochStart?: number;
13
13
  epochEnd?: number;
14
14
  numEvents?: number;
15
- numUsers?: number;
15
+ numUsers?: number;
16
+
17
+ //switches
18
+ isAnonymous?: boolean;
19
+ hasLocation?: boolean;
20
+ hasCampaigns?: boolean;
21
+ hasAdSpend?: boolean;
22
+ hasIOSDevices?: boolean;
23
+ hasAndroidDevices?: boolean;
24
+ hasDesktopDevices?: boolean;
25
+ hasBrowser?: boolean;
26
+
27
+
16
28
  format?: "csv" | "json";
17
29
  region?: "US" | "EU";
18
- chance?: any;
19
30
  events?: EventConfig[]; //can also be a array of strings
20
31
  superProps?: Record<string, ValueValid>;
21
32
  funnels?: Funnel[];
@@ -50,6 +61,8 @@ declare namespace main {
50
61
  | "mirror"
51
62
  | "funnel-pre"
52
63
  | "funnel-post"
64
+ | "ad-spend"
65
+ | "churn"
53
66
  | "";
54
67
  export type Hook<T> = (record: any, type: hookTypes, meta: any) => T;
55
68