make-mp-data 2.1.6 → 3.0.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.
Files changed (76) hide show
  1. package/README.md +31 -0
  2. package/dungeons/adspend.js +2 -2
  3. package/dungeons/ai-chat-analytics-ed.js +3 -2
  4. package/dungeons/anon.js +2 -2
  5. package/dungeons/array-of-object-loopup.js +181 -0
  6. package/dungeons/benchmark-heavy.js +241 -0
  7. package/dungeons/benchmark-light.js +141 -0
  8. package/dungeons/big.js +9 -8
  9. package/dungeons/business.js +2 -1
  10. package/dungeons/clinch-agi.js +632 -0
  11. package/dungeons/complex.js +3 -2
  12. package/dungeons/copilot.js +383 -0
  13. package/dungeons/ecommerce-store.js +0 -0
  14. package/dungeons/experiments.js +5 -4
  15. package/dungeons/foobar.js +101 -101
  16. package/dungeons/funnels.js +2 -2
  17. package/dungeons/gaming.js +3 -2
  18. package/dungeons/harness/harness-education.js +988 -0
  19. package/dungeons/harness/harness-fintech.js +976 -0
  20. package/dungeons/harness/harness-food.js +985 -0
  21. package/dungeons/harness/harness-gaming.js +1178 -0
  22. package/dungeons/harness/harness-media.js +961 -0
  23. package/dungeons/harness/harness-sass.js +923 -0
  24. package/dungeons/harness/harness-social.js +928 -0
  25. package/dungeons/kurby.js +211 -0
  26. package/dungeons/media.js +5 -4
  27. package/dungeons/mil.js +4 -3
  28. package/dungeons/mirror.js +2 -2
  29. package/dungeons/money2020-ed.js +8 -7
  30. package/dungeons/sanity.js +3 -2
  31. package/dungeons/scd.js +3 -2
  32. package/dungeons/simple.js +29 -14
  33. package/dungeons/strict-event-test.js +30 -0
  34. package/dungeons/student-teacher.js +3 -2
  35. package/dungeons/text-generation.js +84 -85
  36. package/dungeons/too-big-events.js +166 -0
  37. package/dungeons/uday-schema.json +220 -0
  38. package/dungeons/userAgent.js +4 -3
  39. package/index.js +41 -54
  40. package/lib/core/config-validator.js +122 -7
  41. package/lib/core/context.js +7 -14
  42. package/lib/core/storage.js +60 -30
  43. package/lib/generators/adspend.js +12 -27
  44. package/lib/generators/events.js +6 -7
  45. package/lib/generators/funnels.js +16 -5
  46. package/lib/generators/product-lookup.js +262 -0
  47. package/lib/generators/product-names.js +195 -0
  48. package/lib/generators/profiles.js +3 -3
  49. package/lib/generators/scd.js +13 -3
  50. package/lib/generators/text.js +17 -4
  51. package/lib/orchestrators/mixpanel-sender.js +251 -208
  52. package/lib/orchestrators/user-loop.js +57 -19
  53. package/lib/templates/funnels-instructions.txt +272 -0
  54. package/lib/templates/hook-examples.json +187 -0
  55. package/lib/templates/hooks-instructions.txt +295 -8
  56. package/lib/templates/phrases.js +473 -16
  57. package/lib/templates/refine-instructions.txt +485 -0
  58. package/lib/templates/schema-instructions.txt +239 -109
  59. package/lib/templates/schema.d.ts +173 -0
  60. package/lib/templates/verbose-schema.js +140 -206
  61. package/lib/utils/ai.js +853 -77
  62. package/lib/utils/chart.js +210 -0
  63. package/lib/utils/function-registry.js +285 -0
  64. package/lib/utils/json-evaluator.js +172 -0
  65. package/lib/utils/logger.js +38 -0
  66. package/lib/utils/mixpanel.js +101 -0
  67. package/lib/utils/project.js +3 -2
  68. package/lib/utils/utils.js +41 -4
  69. package/package.json +13 -19
  70. package/types.d.ts +15 -5
  71. package/lib/generators/text-bak-old.js +0 -1121
  72. package/lib/orchestrators/worker-manager.js +0 -203
  73. package/lib/templates/phrases-bak.js +0 -925
  74. package/lib/templates/prompt (old).txt +0 -98
  75. package/lib/templates/scratch-dungeon-template.js +0 -116
  76. package/lib/templates/textQuickTest.js +0 -172
@@ -17,214 +17,257 @@ import mp from "mixpanel-import";
17
17
  * @returns {Promise<Object>} Import results for all data types
18
18
  */
19
19
  export async function sendToMixpanel(context) {
20
- const { config, storage } = context;
21
- const {
22
- adSpendData,
23
- eventData,
24
- groupProfilesData,
25
- lookupTableData,
26
- mirrorEventData,
27
- scdTableData,
28
- userProfilesData,
29
- groupEventData
30
- } = storage;
31
-
32
- const {
33
- token,
34
- region,
35
- writeToDisk = true,
36
- format,
37
- serviceAccount,
38
- projectId,
39
- serviceSecret,
40
- verbose = false
41
- } = config;
42
-
43
- const importResults = { events: {}, users: {}, groups: [] };
44
- const isBATCH_MODE = context.isBatchMode();
45
- const isCLI = context.isCLI();
46
- const NODE_ENV = process.env.NODE_ENV || "unknown";
47
-
48
- // Create verbose-aware log function
49
- const log = (message) => {
50
- if (verbose) console.log(message);
51
- };
52
-
53
- /** @type {import('mixpanel-import').Creds} */
54
- const creds = { token };
55
- const mpImportFormat = format === "json" ? "jsonl" : "csv";
56
-
57
- /** @type {import('mixpanel-import').Options} */
58
- const commonOpts = {
59
- region,
60
- fixData: true,
61
- verbose: false,
62
- forceStream: true,
63
- strict: true,
64
- epochEnd: dayjs().unix(),
65
- dryRun: false,
66
- abridged: false,
67
- fixJson: false,
68
- showProgress: verbose,
69
- streamFormat: mpImportFormat,
70
- workers: 35
71
- };
72
-
73
- // Import events
74
- if (eventData || isBATCH_MODE) {
75
- log(`importing events to mixpanel...\n`);
76
- let eventDataToImport = u.deepClone(eventData);
77
- if (isBATCH_MODE) {
78
- const writeDir = eventData.getWriteDir();
79
- const files = await ls(writeDir.split(path.basename(writeDir)).join(""));
80
- // @ts-ignore
81
- eventDataToImport = files.filter(f => f.includes('-EVENTS-'));
82
- }
83
- const imported = await mp(creds, eventDataToImport, {
84
- recordType: "event",
85
- ...commonOpts,
86
- });
87
- log(`\tsent ${comma(imported.success)} events\n`);
88
- importResults.events = imported;
89
- }
90
-
91
- // Import user profiles
92
- if (userProfilesData || isBATCH_MODE) {
93
- log(`importing user profiles to mixpanel...\n`);
94
- let userProfilesToImport = u.deepClone(userProfilesData);
95
- if (isBATCH_MODE) {
96
- const writeDir = userProfilesData.getWriteDir();
97
- const files = await ls(writeDir.split(path.basename(writeDir)).join(""));
98
- // @ts-ignore
99
- userProfilesToImport = files.filter(f => f.includes('-USERS-'));
100
- }
101
- const imported = await mp(creds, userProfilesToImport, {
102
- recordType: "user",
103
- ...commonOpts,
104
- });
105
- log(`\tsent ${comma(imported.success)} user profiles\n`);
106
- importResults.users = imported;
107
- }
108
-
109
- // Import ad spend data
110
- if (adSpendData || isBATCH_MODE) {
111
- log(`importing ad spend data to mixpanel...\n`);
112
- let adSpendDataToImport = u.deepClone(adSpendData);
113
- if (isBATCH_MODE) {
114
- const writeDir = adSpendData.getWriteDir();
115
- const files = await ls(writeDir.split(path.basename(writeDir)).join(""));
116
- // @ts-ignore
117
- adSpendDataToImport = files.filter(f => f.includes('-AD-SPEND-'));
118
- }
119
- const imported = await mp(creds, adSpendDataToImport, {
120
- recordType: "event",
121
- ...commonOpts,
122
- });
123
- log(`\tsent ${comma(imported.success)} ad spend events\n`);
124
- importResults.adSpend = imported;
125
- }
126
-
127
- // Import group profiles
128
- if (groupProfilesData || isBATCH_MODE) {
129
- for (const groupEntity of groupProfilesData) {
130
- const groupKey = groupEntity?.groupKey;
131
- log(`importing ${groupKey} profiles to mixpanel...\n`);
132
- let groupProfilesToImport = u.deepClone(groupEntity);
133
- if (isBATCH_MODE) {
134
- const writeDir = groupEntity.getWriteDir();
135
- const files = await ls(writeDir.split(path.basename(writeDir)).join(""));
136
- // @ts-ignore
137
- groupProfilesToImport = files.filter(f => f.includes(`-GROUPS-${groupKey}`));
138
- }
139
- const imported = await mp({ token, groupKey }, groupProfilesToImport, {
140
- recordType: "group",
141
- ...commonOpts,
20
+ const { config, storage } = context;
21
+ const {
22
+ adSpendData,
23
+ eventData,
24
+ groupProfilesData,
25
+ lookupTableData,
26
+ mirrorEventData,
27
+ scdTableData,
28
+ userProfilesData,
29
+ groupEventData
30
+ } = storage;
31
+
32
+ const {
33
+ token,
34
+ region,
35
+ writeToDisk = true,
36
+ format,
37
+ serviceAccount,
38
+ projectId,
39
+ serviceSecret
40
+ } = config;
41
+
42
+ const importResults = { events: {}, users: {}, groups: [] };
43
+ const isBATCH_MODE = context.isBatchMode();
44
+ const NODE_ENV = process.env.NODE_ENV || "unknown";
45
+
46
+ /** @type {import('mixpanel-import').Creds} */
47
+ const creds = { token };
48
+ const mpImportFormat = format === "json" ? "jsonl" : "csv";
49
+
50
+ const isDev = NODE_ENV !== 'production';
51
+
52
+ /** @type {import('mixpanel-import').Options} */
53
+ const commonOpts = {
54
+ region,
55
+ fixData: true,
56
+ verbose: isDev,
57
+ forceStream: true,
58
+ strict: true,
59
+ epochEnd: dayjs().unix(),
60
+ dryRun: false,
61
+ abridged: false,
62
+ fixJson: false,
63
+ showProgress: isDev,
64
+ streamFormat: mpImportFormat,
65
+ workers: 35
66
+ };
67
+
68
+ // Import events
69
+ if (eventData?.length > 0 || isBATCH_MODE) {
70
+ log(`importing events to mixpanel...\n`);
71
+ let eventDataToImport = u.deepClone(eventData);
72
+ // Check if we need to read from disk files instead of memory
73
+ const shouldReadFromFiles = isBATCH_MODE || (writeToDisk && eventData && eventData.length === 0);
74
+ if (shouldReadFromFiles && eventData?.getWriteDir) {
75
+ const writeDir = eventData.getWriteDir();
76
+ const files = await ls(writeDir);
77
+ // @ts-ignore
78
+ eventDataToImport = files.filter(f => f.includes('-EVENTS'));
79
+ }
80
+ const imported = await mp(creds, eventDataToImport, {
81
+ recordType: "event",
82
+ ...commonOpts,
83
+ });
84
+ log(`\tsent ${comma(imported.success)} events\n`);
85
+ importResults.events = imported;
86
+ }
87
+
88
+ // Import user profiles
89
+ if (userProfilesData?.length > 0 || isBATCH_MODE) {
90
+ log(`importing user profiles to mixpanel...\n`);
91
+ let userProfilesToImport = u.deepClone(userProfilesData);
92
+ // Check if we need to read from disk files instead of memory
93
+ const shouldReadFromFiles = isBATCH_MODE || (writeToDisk && userProfilesData && userProfilesData.length === 0);
94
+ if (shouldReadFromFiles && userProfilesData?.getWriteDir) {
95
+ const writeDir = userProfilesData.getWriteDir();
96
+ const files = await ls(writeDir);
97
+ // @ts-ignore
98
+ userProfilesToImport = files.filter(f => f.includes('-USERS'));
99
+ }
100
+ const imported = await mp(creds, userProfilesToImport, {
101
+ recordType: "user",
102
+ ...commonOpts,
103
+ });
104
+ log(`\tsent ${comma(imported.success)} user profiles\n`);
105
+ importResults.users = imported;
106
+ }
107
+
108
+ // Import ad spend data
109
+ if (adSpendData?.length > 0 || isBATCH_MODE) {
110
+ log(`importing ad spend data to mixpanel...\n`);
111
+ let adSpendDataToImport = u.deepClone(adSpendData);
112
+ // Check if we need to read from disk files instead of memory
113
+ const shouldReadFromFiles = isBATCH_MODE || (writeToDisk && adSpendData && adSpendData.length === 0);
114
+ if (shouldReadFromFiles && adSpendData?.getWriteDir) {
115
+ const writeDir = adSpendData.getWriteDir();
116
+ const files = await ls(writeDir);
117
+ // @ts-ignore
118
+ adSpendDataToImport = files.filter(f => f.includes('-ADSPEND'));
119
+ }
120
+ const imported = await mp(creds, adSpendDataToImport, {
121
+ recordType: "event",
122
+ ...commonOpts,
123
+ });
124
+ log(`\tsent ${comma(imported.success)} ad spend events\n`);
125
+ importResults.adSpend = imported;
126
+ }
127
+
128
+ // Import group profiles
129
+ if (groupProfilesData && Array.isArray(groupProfilesData) && groupProfilesData.length > 0) {
130
+ for (const groupEntity of groupProfilesData) {
131
+ if (!groupEntity || groupEntity.length === 0) continue;
132
+ const groupKey = groupEntity?.groupKey;
133
+ log(`importing ${groupKey} profiles to mixpanel...\n`);
134
+ let groupProfilesToImport = u.deepClone(groupEntity);
135
+ // Check if we need to read from disk files instead of memory
136
+ const shouldReadFromFiles = isBATCH_MODE || (writeToDisk && groupEntity.length === 0);
137
+ if (shouldReadFromFiles && groupEntity?.getWriteDir) {
138
+ const writeDir = groupEntity.getWriteDir();
139
+ const files = await ls(writeDir);
140
+ // @ts-ignore
141
+ groupProfilesToImport = files.filter(f => f.includes(`-${groupKey}-GROUPS`));
142
+ }
143
+ const imported = await mp({ token, groupKey }, groupProfilesToImport, {
144
+ recordType: "group",
145
+ ...commonOpts,
142
146
  groupKey,
143
- //dryRun: true
144
- });
145
- log(`\tsent ${comma(imported.success)} ${groupKey} profiles\n`);
146
- importResults.groups.push(imported);
147
- }
148
- }
149
-
150
- // Import group events
151
- if (groupEventData || isBATCH_MODE) {
152
- log(`importing group events to mixpanel...\n`);
153
- let groupEventDataToImport = u.deepClone(groupEventData);
154
- if (isBATCH_MODE) {
155
- const writeDir = groupEventData.getWriteDir();
156
- const files = await ls(writeDir.split(path.basename(writeDir)).join(""));
157
- // @ts-ignore
158
- groupEventDataToImport = files.filter(f => f.includes('-GROUP-EVENTS-'));
159
- }
160
- const imported = await mp(creds, groupEventDataToImport, {
161
- recordType: "event",
162
- ...commonOpts,
163
- strict: false
164
- });
165
- log(`\tsent ${comma(imported.success)} group events\n`);
166
- importResults.groupEvents = imported;
167
- }
168
-
169
- // Import SCD data (requires service account)
170
- if (serviceAccount && projectId && serviceSecret) {
171
- if (scdTableData || isBATCH_MODE) {
172
- log(`importing SCD data to mixpanel...\n`);
173
- for (const scdEntity of scdTableData) {
174
- const scdKey = scdEntity?.scdKey;
175
- log(`importing ${scdKey} SCD data to mixpanel...\n`);
176
- let scdDataToImport = u.deepClone(scdEntity);
177
- if (isBATCH_MODE) {
178
- const writeDir = scdEntity.getWriteDir();
179
- const files = await ls(writeDir.split(path.basename(writeDir)).join(""));
180
- // @ts-ignore
181
- scdDataToImport = files.filter(f => f.includes(`-SCD-${scdKey}`));
182
- }
183
-
184
- /** @type {import('mixpanel-import').Options} */
185
- const options = {
186
- recordType: "scd",
187
- scdKey,
188
- scdType: scdEntity.dataType,
189
- scdLabel: `${scdKey}-scd`,
190
- ...commonOpts,
191
- };
192
-
193
- if (scdEntity.entityType !== "user") options.groupKey = scdEntity.entityType;
194
-
195
- const imported = await mp(
196
- {
197
- token,
198
- acct: serviceAccount,
199
- pass: serviceSecret,
200
- project: projectId
201
- },
202
- scdDataToImport,
203
- options
204
- );
205
- log(`\tsent ${comma(imported.success)} ${scdKey} SCD data\n`);
206
- importResults[`${scdKey}_scd`] = imported;
207
- }
208
- }
209
- }
210
-
211
- // Clean up batch files if needed
212
- if (!writeToDisk && isBATCH_MODE) {
213
- const writeDir = eventData?.getWriteDir() || userProfilesData?.getWriteDir();
214
- const listDir = await ls(writeDir.split(path.basename(writeDir)).join(""));
215
- // @ts-ignore
216
- const files = listDir.filter(f =>
217
- f.includes('-EVENTS-') ||
218
- f.includes('-USERS-') ||
219
- f.includes('-AD-SPEND-') ||
220
- f.includes('-GROUPS-') ||
221
- f.includes('-GROUP-EVENTS-')
222
- );
223
- for (const file of files) {
224
- await rm(file);
225
- }
226
- }
227
-
228
- return importResults;
147
+ });
148
+ log(`\tsent ${comma(imported.success)} ${groupKey} profiles\n`);
149
+ importResults.groups.push(imported);
150
+ }
151
+ }
152
+
153
+ // Import group events
154
+ if (groupEventData?.length > 0) {
155
+ log(`importing group events to mixpanel...\n`);
156
+ let groupEventDataToImport = u.deepClone(groupEventData);
157
+ // Check if we need to read from disk files instead of memory
158
+ const shouldReadFromFiles = isBATCH_MODE || (writeToDisk && groupEventData.length === 0);
159
+ if (shouldReadFromFiles && groupEventData?.getWriteDir) {
160
+ const writeDir = groupEventData.getWriteDir();
161
+ const files = await ls(writeDir);
162
+ // @ts-ignore
163
+ groupEventDataToImport = files.filter(f => f.includes('-GROUP-EVENTS'));
164
+ }
165
+ const imported = await mp(creds, groupEventDataToImport, {
166
+ recordType: "event",
167
+ ...commonOpts,
168
+ strict: false
169
+ });
170
+ log(`\tsent ${comma(imported.success)} group events\n`);
171
+ importResults.groupEvents = imported;
172
+ }
173
+
174
+ // Import SCD data (requires service account)
175
+ if (serviceAccount && projectId && serviceSecret) {
176
+ if (scdTableData && Array.isArray(scdTableData) && scdTableData.length > 0) {
177
+ log(`importing SCD data to mixpanel...\n`);
178
+ for (const scdEntity of scdTableData) {
179
+ const scdKey = scdEntity?.scdKey;
180
+ const entityType = scdEntity?.entityType || 'user';
181
+ log(`importing ${scdKey} SCD data to mixpanel...\n`);
182
+ let scdDataToImport = u.deepClone(scdEntity);
183
+ // Check if we need to read from disk files instead of memory
184
+ const shouldReadFromFiles = isBATCH_MODE || (writeToDisk && scdEntity && scdEntity.length === 0);
185
+ if (shouldReadFromFiles && scdEntity?.getWriteDir) {
186
+ const writeDir = scdEntity.getWriteDir();
187
+ const files = await ls(writeDir);
188
+ // @ts-ignore
189
+ scdDataToImport = files.filter(f => f.includes(`-${scdKey}-SCD`))?.pop();
190
+
191
+ }
192
+
193
+ // Derive the data type from the actual SCD data
194
+ // todo: we can do better type inference here we don't need to visit the file
195
+ /** @type {"string" | "number" | "boolean"} */
196
+ let scdType = 'string'; // default to string
197
+ const scdExamplesValues = context.config.scdProps[Object.keys(context.config.scdProps).find(k => k === scdKey)].values;
198
+ if (scdExamplesValues) {
199
+ if (typeof scdExamplesValues[0] === 'number') {
200
+ scdType = 'number';
201
+ } else if (typeof scdExamplesValues[0] === 'boolean') {
202
+ scdType = 'boolean';
203
+ }
204
+ }
205
+
206
+
207
+
208
+ /** @type {import('mixpanel-import').Options} */
209
+ const options = {
210
+ recordType: "scd",
211
+ scdKey,
212
+ scdType,
213
+ scdLabel: `${scdKey}`,
214
+ fixData: true,
215
+ ...commonOpts,
216
+ };
217
+
218
+ // For group SCDs, add the groupKey
219
+ if (entityType !== "user") {
220
+ options.groupKey = entityType;
221
+ }
222
+
223
+ // SCD data is sketch and it shouldn't fail the whole import
224
+ try {
225
+ const imported = await mp(
226
+ {
227
+ token,
228
+ acct: serviceAccount,
229
+ pass: serviceSecret,
230
+ project: projectId
231
+ },
232
+ scdDataToImport,
233
+ options
234
+ );
235
+ log(`\tsent ${comma(imported.success)} ${scdKey} SCD data\n`);
236
+ importResults[`${scdKey}_scd`] = imported;
237
+ } catch (err) {
238
+ log(`\tfailed to import ${scdKey} SCD data: ${err.message}\n`);
239
+ importResults[`${scdKey}_scd`] = { success: 0, failed: 0, error: err.message };
240
+ }
241
+ }
242
+ }
243
+ }
244
+
245
+ // Clean up batch files if needed
246
+ if (!writeToDisk && isBATCH_MODE) {
247
+ const writeDir = eventData?.getWriteDir?.() || userProfilesData?.getWriteDir?.();
248
+ if (writeDir) {
249
+ const listDir = await ls(writeDir);
250
+ // @ts-ignore
251
+ const files = listDir.filter(f =>
252
+ f.includes('-EVENTS') ||
253
+ f.includes('-USERS') ||
254
+ f.includes('-ADSPEND') ||
255
+ f.includes('-GROUPS') ||
256
+ f.includes('-GROUP-EVENTS')
257
+ );
258
+ for (const file of files) {
259
+ await rm(file);
260
+ }
261
+ }
262
+ }
263
+
264
+ return importResults;
229
265
  }
230
266
 
267
+ /**
268
+ * Simple logging function
269
+ * @param {string} message - Message to log
270
+ */
271
+ function log(message) {
272
+ console.log(message);
273
+ }
@@ -39,7 +39,9 @@ export async function userLoop(context) {
39
39
  userProps,
40
40
  scdProps,
41
41
  numDays,
42
- percentUsersBornInDataset = 5,
42
+ percentUsersBornInDataset = 15,
43
+ strictEventCount = false,
44
+ bornRecentBias = 0.3, // 0 = uniform distribution, 1 = heavily biased toward recent births
43
45
  } = config;
44
46
 
45
47
  const { eventData, userProfilesData, scdTableData } = storage;
@@ -50,16 +52,32 @@ export async function userLoop(context) {
50
52
  const batchSize = Math.max(1, Math.ceil(numUsers / concurrency));
51
53
  const userPromises = [];
52
54
 
55
+ // Track if we've already logged the strict event count message
56
+ let hasLoggedStrictCountReached = false;
57
+
53
58
  for (let i = 0; i < numUsers; i++) {
54
59
  const userPromise = USER_CONN(async () => {
60
+ // Bail out early if strictEventCount is enabled and we've hit numEvents
61
+ if (strictEventCount && context.getEventCount() >= numEvents) {
62
+ if (verbose && !hasLoggedStrictCountReached) {
63
+ console.log(`\n\u2713 Reached target of ${numEvents.toLocaleString()} events with strict event count enabled. Stopping user generation.`);
64
+ hasLoggedStrictCountReached = true;
65
+ }
66
+ return;
67
+ }
68
+
55
69
  context.incrementUserCount();
56
70
  const eps = Math.floor(context.getEventCount() / ((Date.now() - startTime) / 1000));
71
+ const memUsed = u.bytesHuman(process.memoryUsage().heapUsed);
72
+ const duration = u.formatDuration(Date.now() - startTime);
57
73
 
58
74
  if (verbose) {
59
75
  u.progress([
60
76
  ["users", context.getUserCount()],
61
77
  ["events", context.getEventCount()],
62
- ["eps", eps]
78
+ ["eps", eps],
79
+ ["mem", memUsed],
80
+ ["time", duration]
63
81
  ]);
64
82
  }
65
83
 
@@ -73,9 +91,36 @@ export async function userLoop(context) {
73
91
 
74
92
  // Calculate time adjustments
75
93
  const daysShift = context.getDaysShift();
76
- const adjustedCreated = userIsBornInDataset
77
- ? dayjs(created).subtract(daysShift, 'd')
78
- : dayjs.unix(global.FIXED_BEGIN);
94
+
95
+ // Apply recency bias to birth dates for users born in dataset
96
+ // bornRecentBias: 0 = uniform distribution, 1 = heavily biased toward recent
97
+ let adjustedCreated;
98
+ if (userIsBornInDataset) {
99
+ let biasedCreated = dayjs(created).subtract(daysShift, 'd');
100
+
101
+ if (bornRecentBias > 0) {
102
+ // Calculate how far into the dataset this user was born (0 = start, 1 = end/recent)
103
+ const datasetStart = dayjs.unix(global.FIXED_BEGIN);
104
+ const datasetEnd = dayjs.unix(context.FIXED_NOW);
105
+ const totalDuration = datasetEnd.diff(datasetStart);
106
+ // Clamp userPosition to [0, 1] to handle edge cases from rounding in time calculations
107
+ const userPosition = Math.max(0, Math.min(1, biasedCreated.diff(datasetStart) / totalDuration));
108
+
109
+ // Apply power function to bias toward recent (higher values)
110
+ // exponent < 1 shifts distribution toward 1 (recent)
111
+ const exponent = 1 - (bornRecentBias * 0.7); // 0.3 bias -> 0.79 exponent (gentle nudge)
112
+ const biasedPosition = Math.pow(userPosition, exponent);
113
+
114
+ // Convert back to timestamp
115
+ biasedCreated = datasetStart.add(biasedPosition * totalDuration, 'millisecond');
116
+ }
117
+
118
+ adjustedCreated = biasedCreated;
119
+ // Update user.created to match biased timestamp for profile consistency
120
+ user.created = adjustedCreated.toISOString();
121
+ } else {
122
+ adjustedCreated = dayjs.unix(global.FIXED_BEGIN);
123
+ }
79
124
 
80
125
  if (hasLocation) {
81
126
  const location = u.pickRandom(u.choose(defaults.locationsUsers));
@@ -89,10 +134,10 @@ export async function userLoop(context) {
89
134
 
90
135
  // Call user hook after profile creation
91
136
  if (config.hook) {
92
- await config.hook(profile, "user", {
93
- user,
137
+ await config.hook(profile, "user", {
138
+ user,
94
139
  config,
95
- userIsBornInDataset
140
+ userIsBornInDataset
96
141
  });
97
142
  }
98
143
 
@@ -149,12 +194,9 @@ export async function userLoop(context) {
149
194
  const timeShift = context.getTimeShift();
150
195
  userFirstEventTime = dayjs(data[0].time).subtract(timeShift, 'seconds').unix();
151
196
  numEventsPreformed += data.length;
152
- usersEvents.push(...data);
197
+ usersEvents = usersEvents.concat(data);
153
198
 
154
199
  if (!userConverted) {
155
- // if (verbose) {
156
- // u.progress([["users", context.getUserCount()], ["events", context.getEventCount()]]);
157
- // }
158
200
  return;
159
201
  }
160
202
  } else {
@@ -167,11 +209,11 @@ export async function userLoop(context) {
167
209
  const currentFunnel = chance.pickone(usageFunnels);
168
210
  const [data, userConverted] = await makeFunnel(context, currentFunnel, user, userFirstEventTime, profile, userSCD);
169
211
  numEventsPreformed += data.length;
170
- usersEvents.push(...data);
212
+ usersEvents = usersEvents.concat(data);
171
213
  } else {
172
214
  const data = await makeEvent(context, distinct_id, userFirstEventTime, u.pick(config.events), user.anonymousIds, user.sessionIds, {}, config.groupKeys, true);
173
215
  numEventsPreformed++;
174
- usersEvents.push(data);
216
+ usersEvents = usersEvents.concat(data);
175
217
  }
176
218
  }
177
219
 
@@ -206,10 +248,6 @@ export async function userLoop(context) {
206
248
  }
207
249
 
208
250
  await eventData.hookPush(usersEvents, { profile });
209
-
210
- if (verbose) {
211
- // u.progress([["users", context.getUserCount()], ["events", context.getEventCount()]]);
212
- }
213
251
  });
214
252
 
215
253
  userPromises.push(userPromise);
@@ -233,4 +271,4 @@ export function matchConditions(profile, conditions) {
233
271
  if (profile[key] !== value) return false;
234
272
  }
235
273
  return true;
236
- }
274
+ }