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.
- package/.vscode/launch.json +25 -13
- package/{core → components}/cli.js +10 -3
- package/components/project.js +155 -0
- package/{core → components}/utils.js +81 -163
- package/index.js +1427 -0
- package/package.json +16 -10
- package/schemas/adspend.js +96 -0
- package/schemas/anon.js +3 -3
- package/schemas/big.js +160 -0
- package/schemas/business.js +327 -0
- package/schemas/complex.js +13 -6
- package/schemas/foobar.js +21 -6
- package/schemas/funnels.js +3 -3
- package/schemas/mirror.js +129 -0
- package/schemas/sanity.js +118 -0
- package/schemas/session-replay.js +136 -0
- package/schemas/simple.js +4 -4
- package/scratch.mjs +34 -9
- package/scripts/jsdoctest.js +1 -1
- package/scripts/{new.sh → new-dungeon.sh} +52 -13
- package/scripts/new-project.mjs +14 -0
- package/scripts/update-deps.sh +4 -0
- package/tests/benchmark/concurrency.mjs +52 -0
- package/tests/e2e.test.js +245 -40
- package/tests/int.test.js +619 -0
- package/tests/jest.config.js +8 -0
- package/tests/testCases.mjs +1 -1
- package/tests/testSoup.mjs +5 -4
- package/tests/unit.test.js +56 -51
- package/tsconfig.json +1 -1
- package/types.d.ts +83 -39
- package/core/index.js +0 -1013
- package/data/.gitkeep +0 -0
- package/scripts/deps.sh +0 -3
- /package/{core → components}/chart.js +0 -0
- /package/{core → components}/defaults.js +0 -0
- /package/scripts/{go.sh → run-index.sh} +0 -0
package/.vscode/launch.json
CHANGED
|
@@ -23,18 +23,30 @@
|
|
|
23
23
|
"type": "node-terminal"
|
|
24
24
|
},
|
|
25
25
|
{
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
"
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
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("
|
|
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("
|
|
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').
|
|
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 ||
|
|
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.
|
|
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
|
|
154
|
+
* @param {any} end
|
|
154
155
|
*/
|
|
155
|
-
function day(start, end
|
|
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
|
|
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
|
|
678
|
-
if (!earliestTime) earliestTime = global.
|
|
679
|
-
if (!latestTime) latestTime = global.
|
|
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,
|
|
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
|
-
...
|
|
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.
|
|
907
|
-
if (!latestTime) latestTime = global.
|
|
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 =
|
|
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
|
|
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 (
|
|
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 (
|
|
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
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
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
|
-
|
|
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
|
};
|