make-mp-data 3.0.2 → 3.0.4
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/dungeons/adspend.js +13 -26
- package/dungeons/anon.js +1 -1
- package/dungeons/array-of-object-lookup.js +1 -2
- package/dungeons/benchmark-heavy.js +5 -6
- package/dungeons/benchmark-light.js +13 -28
- package/dungeons/big.js +3 -3
- package/dungeons/business.js +11 -12
- package/dungeons/complex.js +1 -2
- package/dungeons/copilot.js +8 -6
- package/dungeons/education.js +21 -22
- package/dungeons/experiments.js +4 -5
- package/dungeons/fintech.js +25 -26
- package/dungeons/foobar.js +1 -1
- package/dungeons/food.js +24 -25
- package/dungeons/funnels.js +2 -2
- package/dungeons/gaming.js +39 -40
- package/dungeons/media.js +30 -31
- package/dungeons/mil.js +17 -18
- package/dungeons/mirror.js +2 -3
- package/dungeons/retention-cadence.js +1 -2
- package/dungeons/rpg.js +42 -43
- package/dungeons/sanity.js +1 -2
- package/dungeons/sass.js +32 -33
- package/dungeons/scd.js +3 -4
- package/dungeons/simple.js +13 -14
- package/dungeons/social.js +27 -28
- package/dungeons/soup-test.js +52 -0
- package/dungeons/streaming.js +17 -18
- package/dungeons/student-teacher.js +0 -1
- package/dungeons/text-generation.js +0 -1
- package/dungeons/user-agent.js +1 -2
- package/index.js +18 -6
- package/lib/core/config-validator.js +22 -33
- package/lib/core/context.js +6 -3
- package/lib/generators/events.js +13 -10
- package/lib/generators/funnels.js +7 -4
- package/lib/generators/scd.js +29 -17
- package/lib/generators/text.js +18 -12
- package/lib/orchestrators/mixpanel-sender.js +26 -38
- package/lib/orchestrators/user-loop.js +68 -15
- package/lib/templates/phrases.js +8 -5
- package/lib/utils/function-registry.js +17 -0
- package/lib/utils/utils.js +15 -84
- package/package.json +3 -1
- package/types.d.ts +86 -19
- package/lib/templates/verbose-schema.js +0 -272
- package/lib/utils/chart.js +0 -210
package/dungeons/streaming.js
CHANGED
|
@@ -44,10 +44,9 @@ const config = {
|
|
|
44
44
|
hasAdSpend: false,
|
|
45
45
|
|
|
46
46
|
hasAvatar: false,
|
|
47
|
-
makeChart: false,
|
|
48
47
|
|
|
49
48
|
batchSize: 2_500_000,
|
|
50
|
-
concurrency:
|
|
49
|
+
concurrency: 1,
|
|
51
50
|
writeToDisk: false,
|
|
52
51
|
|
|
53
52
|
funnels: [],
|
|
@@ -66,7 +65,7 @@ const config = {
|
|
|
66
65
|
]),
|
|
67
66
|
"watch time": u.weighNumRange(1, 65, .89, 100),
|
|
68
67
|
|
|
69
|
-
"category":
|
|
68
|
+
"category": [
|
|
70
69
|
"comedy",
|
|
71
70
|
"educational",
|
|
72
71
|
"music",
|
|
@@ -74,15 +73,15 @@ const config = {
|
|
|
74
73
|
"news",
|
|
75
74
|
"gaming",
|
|
76
75
|
"travel",
|
|
77
|
-
]
|
|
78
|
-
quality:
|
|
76
|
+
],
|
|
77
|
+
quality: [
|
|
79
78
|
"240p",
|
|
80
79
|
"360p",
|
|
81
80
|
"480p",
|
|
82
81
|
"720p",
|
|
83
82
|
"1080p",
|
|
84
83
|
"4k",
|
|
85
|
-
],
|
|
84
|
+
],
|
|
86
85
|
autoplay: [
|
|
87
86
|
true,
|
|
88
87
|
false,
|
|
@@ -121,13 +120,13 @@ const config = {
|
|
|
121
120
|
weight: 3,
|
|
122
121
|
properties: {
|
|
123
122
|
video_id: u.pickAWinner(videoIds),
|
|
124
|
-
"share network":
|
|
123
|
+
"share network": [
|
|
125
124
|
"facebook",
|
|
126
125
|
"twitter",
|
|
127
126
|
"reddit",
|
|
128
127
|
"email",
|
|
129
128
|
"whatsapp",
|
|
130
|
-
]
|
|
129
|
+
],
|
|
131
130
|
},
|
|
132
131
|
},
|
|
133
132
|
{
|
|
@@ -175,18 +174,18 @@ const config = {
|
|
|
175
174
|
event: "create playlist",
|
|
176
175
|
weight: 4,
|
|
177
176
|
properties: {
|
|
178
|
-
"play list name":
|
|
177
|
+
"play list name": [
|
|
179
178
|
"favorites",
|
|
180
179
|
"watch later",
|
|
181
180
|
"my music",
|
|
182
181
|
"funny videos",
|
|
183
182
|
"educational",
|
|
184
|
-
]
|
|
185
|
-
privacy:
|
|
183
|
+
],
|
|
184
|
+
privacy: [
|
|
186
185
|
"public",
|
|
187
186
|
"private",
|
|
188
187
|
"unlisted",
|
|
189
|
-
]
|
|
188
|
+
],
|
|
190
189
|
},
|
|
191
190
|
},
|
|
192
191
|
{
|
|
@@ -205,11 +204,11 @@ const config = {
|
|
|
205
204
|
event: "account login",
|
|
206
205
|
weight: 9,
|
|
207
206
|
properties: {
|
|
208
|
-
"log in method":
|
|
207
|
+
"log in method": [
|
|
209
208
|
"email",
|
|
210
209
|
"google",
|
|
211
210
|
"facebook",
|
|
212
|
-
]
|
|
211
|
+
],
|
|
213
212
|
success: [
|
|
214
213
|
true,
|
|
215
214
|
false,
|
|
@@ -233,11 +232,11 @@ const config = {
|
|
|
233
232
|
}
|
|
234
233
|
],
|
|
235
234
|
superProps: {
|
|
236
|
-
platform:
|
|
235
|
+
platform: [
|
|
237
236
|
"web",
|
|
238
237
|
"ios",
|
|
239
238
|
"android",
|
|
240
|
-
]
|
|
239
|
+
],
|
|
241
240
|
network_type: [
|
|
242
241
|
"wifi",
|
|
243
242
|
"cellular",
|
|
@@ -257,13 +256,13 @@ const config = {
|
|
|
257
256
|
"45-54",
|
|
258
257
|
"55+",
|
|
259
258
|
],
|
|
260
|
-
preferred_genre:
|
|
259
|
+
preferred_genre: [
|
|
261
260
|
"comedy",
|
|
262
261
|
"action",
|
|
263
262
|
"drama",
|
|
264
263
|
"sci-fi",
|
|
265
264
|
"horror",
|
|
266
|
-
]
|
|
265
|
+
],
|
|
267
266
|
upload_count: [
|
|
268
267
|
0,
|
|
269
268
|
1,
|
package/dungeons/user-agent.js
CHANGED
package/index.js
CHANGED
|
@@ -27,6 +27,7 @@ import { makeMirror } from './lib/generators/mirror.js';
|
|
|
27
27
|
import { makeGroupProfile, makeProfile } from './lib/generators/profiles.js';
|
|
28
28
|
|
|
29
29
|
// Utilities
|
|
30
|
+
import { initChance } from './lib/utils/utils.js';
|
|
30
31
|
|
|
31
32
|
// External dependencies
|
|
32
33
|
import dayjs from "dayjs";
|
|
@@ -138,6 +139,12 @@ async function main(config) {
|
|
|
138
139
|
// Step 1: Validate and enrich configuration
|
|
139
140
|
validatedConfig = validateDungeonConfig(config);
|
|
140
141
|
|
|
142
|
+
// Ensure seeded RNG is initialized (dungeons do this at module scope,
|
|
143
|
+
// but npm-module consumers pass seed via config object)
|
|
144
|
+
if (validatedConfig.seed) {
|
|
145
|
+
initChance(validatedConfig.seed);
|
|
146
|
+
}
|
|
147
|
+
|
|
141
148
|
// Update FIXED_BEGIN based on configured numDays
|
|
142
149
|
const configNumDays = validatedConfig.numDays || 30;
|
|
143
150
|
global.FIXED_BEGIN = dayjs.unix(FIXED_NOW).subtract(configNumDays, 'd').unix();
|
|
@@ -237,8 +244,11 @@ async function generateAdSpendData(context) {
|
|
|
237
244
|
const { config, storage } = context;
|
|
238
245
|
const { numDays } = config;
|
|
239
246
|
|
|
247
|
+
const timeShift = context.TIME_SHIFT_SECONDS;
|
|
240
248
|
for (let day = 0; day < numDays; day++) {
|
|
241
|
-
const
|
|
249
|
+
const fixedDay = dayjs.unix(global.FIXED_BEGIN).add(day, 'day').unix();
|
|
250
|
+
const shiftedDay = Math.min(fixedDay + timeShift, context.MAX_TIME);
|
|
251
|
+
const targetDay = dayjs.unix(shiftedDay).toISOString();
|
|
242
252
|
const adSpendEvents = await makeAdSpend(context, targetDay);
|
|
243
253
|
|
|
244
254
|
if (adSpendEvents.length > 0) {
|
|
@@ -266,7 +276,7 @@ async function generateGroupProfiles(context) {
|
|
|
266
276
|
const groupContainer = storage.groupProfilesData[i];
|
|
267
277
|
|
|
268
278
|
if (!groupContainer) {
|
|
269
|
-
console.warn(`Warning: No storage container found for group key: ${groupKey}`);
|
|
279
|
+
if (config.verbose) console.warn(`Warning: No storage container found for group key: ${groupKey}`);
|
|
270
280
|
continue;
|
|
271
281
|
}
|
|
272
282
|
|
|
@@ -309,7 +319,7 @@ async function generateLookupTables(context) {
|
|
|
309
319
|
const lookupContainer = storage.lookupTableData[i];
|
|
310
320
|
|
|
311
321
|
if (!lookupContainer) {
|
|
312
|
-
console.warn(`Warning: No storage container found for lookup table: ${key}`);
|
|
322
|
+
if (config.verbose) console.warn(`Warning: No storage container found for lookup table: ${key}`);
|
|
313
323
|
continue;
|
|
314
324
|
}
|
|
315
325
|
|
|
@@ -378,16 +388,19 @@ async function generateGroupSCDs(context) {
|
|
|
378
388
|
|
|
379
389
|
// Use a base time for the group entity (similar to user creation time)
|
|
380
390
|
const baseTime = context.FIXED_BEGIN || context.FIXED_NOW;
|
|
381
|
-
|
|
391
|
+
let changes = await makeSCD(context, scdConfig, scdKey, groupId, mutations, baseTime);
|
|
382
392
|
|
|
383
393
|
// Apply hook if configured
|
|
384
394
|
if (config.hook) {
|
|
385
|
-
await config.hook(changes, "scd-pre", {
|
|
395
|
+
const hookResult = await config.hook(changes, "scd-pre", {
|
|
386
396
|
type: 'group',
|
|
387
397
|
groupKey,
|
|
388
398
|
scd: { [scdKey]: scdConfig },
|
|
389
399
|
config
|
|
390
400
|
});
|
|
401
|
+
if (Array.isArray(hookResult)) {
|
|
402
|
+
changes = hookResult;
|
|
403
|
+
}
|
|
391
404
|
}
|
|
392
405
|
|
|
393
406
|
// Store SCDs in the appropriate SCD table
|
|
@@ -538,7 +551,6 @@ async function extractFileInfo(storage, config) {
|
|
|
538
551
|
}
|
|
539
552
|
} catch (error) {
|
|
540
553
|
// If scanning fails, just return empty array
|
|
541
|
-
console.warn('Warning: Could not scan data directory for files:', error.message);
|
|
542
554
|
}
|
|
543
555
|
}
|
|
544
556
|
|
|
@@ -106,8 +106,7 @@ export function validateDungeonConfig(config) {
|
|
|
106
106
|
token = null,
|
|
107
107
|
region = "US",
|
|
108
108
|
writeToDisk = false,
|
|
109
|
-
verbose =
|
|
110
|
-
makeChart = false,
|
|
109
|
+
verbose = false,
|
|
111
110
|
soup = {},
|
|
112
111
|
hook = (record) => record,
|
|
113
112
|
hasAdSpend = false,
|
|
@@ -167,15 +166,17 @@ export function validateDungeonConfig(config) {
|
|
|
167
166
|
throw new Error('Hook string did not evaluate to a function');
|
|
168
167
|
}
|
|
169
168
|
} catch (error) {
|
|
170
|
-
|
|
171
|
-
|
|
169
|
+
if (config.verbose !== false) {
|
|
170
|
+
console.warn(`\u26a0\ufe0f Failed to convert hook string to function: ${error.message}`);
|
|
171
|
+
console.warn('Using default pass-through hook');
|
|
172
|
+
}
|
|
172
173
|
hook = (record) => record;
|
|
173
174
|
}
|
|
174
175
|
}
|
|
175
176
|
|
|
176
177
|
// Ensure hook is a function
|
|
177
178
|
if (typeof hook !== 'function') {
|
|
178
|
-
console.warn('\u26a0\ufe0f Hook is not a function, using default pass-through hook');
|
|
179
|
+
if (config.verbose !== false) console.warn('\u26a0\ufe0f Hook is not a function, using default pass-through hook');
|
|
179
180
|
hook = (record) => record;
|
|
180
181
|
}
|
|
181
182
|
|
|
@@ -187,6 +188,17 @@ export function validateDungeonConfig(config) {
|
|
|
187
188
|
events = events.map(e => ({ event: /** @type {string} */ (e) }));
|
|
188
189
|
}
|
|
189
190
|
|
|
191
|
+
// Validate: if every user is born in dataset, we need either isFirstEvent or isFirstFunnel
|
|
192
|
+
const percentBorn = config.percentUsersBornInDataset ?? 15;
|
|
193
|
+
const hasFirstEvent = events.some(e => e.isFirstEvent);
|
|
194
|
+
const hasFirstFunnel = funnels.some(f => f.isFirstFunnel);
|
|
195
|
+
if (percentBorn >= 100 && !hasFirstEvent && !hasFirstFunnel) {
|
|
196
|
+
throw new Error(
|
|
197
|
+
"percentUsersBornInDataset is 100% but no event has isFirstEvent and no funnel has isFirstFunnel. " +
|
|
198
|
+
"Either add isFirstEvent to an event, add a first funnel, or lower percentUsersBornInDataset."
|
|
199
|
+
);
|
|
200
|
+
}
|
|
201
|
+
|
|
190
202
|
// Handle funnel inference
|
|
191
203
|
if (alsoInferFunnels) {
|
|
192
204
|
const inferredFunnels = inferFunnels(events);
|
|
@@ -202,6 +214,7 @@ export function validateDungeonConfig(config) {
|
|
|
202
214
|
|
|
203
215
|
const eventsNotInFunnels = events
|
|
204
216
|
.filter(e => !e.isFirstEvent)
|
|
217
|
+
.filter(e => !e.isStrictEvent)
|
|
205
218
|
.filter(e => !eventContainedInFunnels.includes(e.event))
|
|
206
219
|
.map(e => e.event);
|
|
207
220
|
|
|
@@ -283,36 +296,12 @@ export function validateDungeonConfig(config) {
|
|
|
283
296
|
hasDesktopDevices,
|
|
284
297
|
hasIOSDevices,
|
|
285
298
|
name,
|
|
286
|
-
makeChart,
|
|
287
299
|
strictEventCount
|
|
288
300
|
};
|
|
289
301
|
|
|
290
302
|
return validatedConfig;
|
|
291
303
|
}
|
|
292
304
|
|
|
293
|
-
/**
|
|
294
|
-
* Validates configuration for required fields
|
|
295
|
-
* @param {Object} config - Configuration to validate
|
|
296
|
-
* @throws {Error} If required fields are missing
|
|
297
|
-
*/
|
|
298
|
-
/**
|
|
299
|
-
* Validates required configuration parameters
|
|
300
|
-
* @param {Dungeon} config - Configuration object to validate
|
|
301
|
-
* @returns {boolean} True if validation passes
|
|
302
|
-
*/
|
|
303
|
-
export function validateRequiredConfig(config) {
|
|
304
|
-
if (!config) {
|
|
305
|
-
throw new Error("Configuration is required");
|
|
306
|
-
}
|
|
307
|
-
|
|
308
|
-
if (typeof config !== 'object') {
|
|
309
|
-
throw new Error("Configuration must be an object");
|
|
310
|
-
}
|
|
311
|
-
|
|
312
|
-
// Could add more specific validation here
|
|
313
|
-
return true;
|
|
314
|
-
}
|
|
315
|
-
|
|
316
305
|
/**
|
|
317
306
|
* Transforms SCD properties to regular user/group properties when service account credentials are missing
|
|
318
307
|
* ONLY applies to UI jobs - programmatic usage always generates SCD files
|
|
@@ -350,7 +339,7 @@ function transformSCDPropsWithoutCredentials(config) {
|
|
|
350
339
|
}
|
|
351
340
|
|
|
352
341
|
// UI job without credentials - convert SCD props to regular props
|
|
353
|
-
console.log('\u26a0\ufe0f Service account credentials missing - converting SCD properties to static properties');
|
|
342
|
+
if (config.verbose !== false) console.log('\u26a0\ufe0f Service account credentials missing - converting SCD properties to static properties');
|
|
354
343
|
|
|
355
344
|
// Ensure userProps and groupProps exist
|
|
356
345
|
if (!config.userProps) config.userProps = {};
|
|
@@ -369,20 +358,20 @@ function transformSCDPropsWithoutCredentials(config) {
|
|
|
369
358
|
if (type === "user") {
|
|
370
359
|
// Add to userProps
|
|
371
360
|
config.userProps[propKey] = values;
|
|
372
|
-
console.log(` \u2713 Converted user SCD property: ${propKey}`);
|
|
361
|
+
if (config.verbose !== false) console.log(` \u2713 Converted user SCD property: ${propKey}`);
|
|
373
362
|
} else {
|
|
374
363
|
// Add to groupProps for the specific group type
|
|
375
364
|
if (!config.groupProps[type]) {
|
|
376
365
|
config.groupProps[type] = {};
|
|
377
366
|
}
|
|
378
367
|
config.groupProps[type][propKey] = values;
|
|
379
|
-
console.log(` \u2713 Converted group SCD property: ${propKey} (${type})`);
|
|
368
|
+
if (config.verbose !== false) console.log(` \u2713 Converted group SCD property: ${propKey} (${type})`);
|
|
380
369
|
}
|
|
381
370
|
}
|
|
382
371
|
|
|
383
372
|
// Clear out scdProps since we've converted everything
|
|
384
373
|
config.scdProps = {};
|
|
385
|
-
console.log('\u2713 SCD properties converted to static properties\n');
|
|
374
|
+
if (config.verbose !== false) console.log('\u2713 SCD properties converted to static properties\n');
|
|
386
375
|
}
|
|
387
376
|
|
|
388
377
|
export { inferFunnels, transformSCDPropsWithoutCredentials };
|
package/lib/core/context.js
CHANGED
|
@@ -149,12 +149,12 @@ export function createContext(config, storage = null, _unusedCliMode = null) {
|
|
|
149
149
|
|
|
150
150
|
// Time helper methods
|
|
151
151
|
getTimeShift() {
|
|
152
|
-
const actualNow = dayjs().
|
|
152
|
+
const actualNow = dayjs().subtract(1, "hour");
|
|
153
153
|
return actualNow.diff(dayjs.unix(this.FIXED_NOW), "seconds");
|
|
154
154
|
},
|
|
155
155
|
|
|
156
156
|
getDaysShift() {
|
|
157
|
-
const actualNow = dayjs().
|
|
157
|
+
const actualNow = dayjs().subtract(1, "hour");
|
|
158
158
|
return actualNow.diff(dayjs.unix(this.FIXED_NOW), "days");
|
|
159
159
|
},
|
|
160
160
|
|
|
@@ -164,9 +164,12 @@ export function createContext(config, storage = null, _unusedCliMode = null) {
|
|
|
164
164
|
|
|
165
165
|
// PERFORMANCE: Pre-calculated time shift (instead of calculating per-event)
|
|
166
166
|
TIME_SHIFT_SECONDS: (() => {
|
|
167
|
-
const actualNow = dayjs().
|
|
167
|
+
const actualNow = dayjs().subtract(1, "hour");
|
|
168
168
|
return actualNow.diff(dayjs.unix(global.FIXED_NOW), "seconds");
|
|
169
169
|
})(),
|
|
170
|
+
|
|
171
|
+
// Max timestamp (unix seconds) — clamp here to prevent future events
|
|
172
|
+
MAX_TIME: dayjs().unix(),
|
|
170
173
|
};
|
|
171
174
|
|
|
172
175
|
return context;
|
package/lib/generators/events.js
CHANGED
|
@@ -52,7 +52,9 @@ export async function makeEvent(
|
|
|
52
52
|
const chance = u.getChance();
|
|
53
53
|
|
|
54
54
|
// Extract soup configuration for time distribution
|
|
55
|
-
|
|
55
|
+
// Dynamic peaks: one per week for long ranges, minimum 5
|
|
56
|
+
const defaultPeaks = Math.max(5, Math.ceil((config.numDays || 30) / 7));
|
|
57
|
+
const { mean = 0, deviation = 2, peaks = defaultPeaks } = config.soup || {};
|
|
56
58
|
|
|
57
59
|
// Extract feature flags from config
|
|
58
60
|
const {
|
|
@@ -95,18 +97,19 @@ export async function makeEvent(
|
|
|
95
97
|
|
|
96
98
|
// Set event time using TimeSoup for realistic distribution
|
|
97
99
|
if (earliestTime) {
|
|
100
|
+
let shiftedTimestamp;
|
|
98
101
|
if (isFirstEvent) {
|
|
99
|
-
|
|
100
|
-
const shiftedTimestamp = earliestTime + context.TIME_SHIFT_SECONDS;
|
|
101
|
-
eventTemplate.time = dayjs.unix(shiftedTimestamp).toISOString();
|
|
102
|
+
shiftedTimestamp = earliestTime + context.TIME_SHIFT_SECONDS;
|
|
102
103
|
} else {
|
|
103
|
-
//
|
|
104
|
-
const
|
|
105
|
-
|
|
106
|
-
const soupTimestamp = new Date(soupTime).getTime() / 1000; // Convert to unix seconds
|
|
107
|
-
const shiftedTimestamp = soupTimestamp + context.TIME_SHIFT_SECONDS;
|
|
108
|
-
eventTemplate.time = dayjs.unix(shiftedTimestamp).toISOString();
|
|
104
|
+
// TimeSoup returns unix seconds; shift and convert to ISO once
|
|
105
|
+
const soupTimestamp = u.TimeSoup(earliestTime, context.FIXED_NOW, peaks, deviation, mean);
|
|
106
|
+
shiftedTimestamp = soupTimestamp + context.TIME_SHIFT_SECONDS;
|
|
109
107
|
}
|
|
108
|
+
// Drop events that would land in the future (Mixpanel rewrites these to "now", causing pile-ups)
|
|
109
|
+
if (shiftedTimestamp > context.MAX_TIME) {
|
|
110
|
+
eventTemplate._drop = true;
|
|
111
|
+
}
|
|
112
|
+
eventTemplate.time = dayjs.unix(Math.min(shiftedTimestamp, context.MAX_TIME)).toISOString();
|
|
110
113
|
}
|
|
111
114
|
|
|
112
115
|
// Add anonymous and session identifiers
|
|
@@ -131,10 +131,9 @@ export async function makeFunnel(context, funnel, user, firstEventTime, profile
|
|
|
131
131
|
numStepsUserWillTake
|
|
132
132
|
);
|
|
133
133
|
|
|
134
|
-
// Add session start event if configured
|
|
134
|
+
// Add session start event if configured (clone to avoid mutating shared config)
|
|
135
135
|
if (sessionStartEvents.length) {
|
|
136
|
-
const sessionStartEvent = chance.pickone(sessionStartEvents);
|
|
137
|
-
sessionStartEvent.relativeTimeMs = -15000; // 15 seconds before funnel
|
|
136
|
+
const sessionStartEvent = { ...chance.pickone(sessionStartEvents), relativeTimeMs: -15000 };
|
|
138
137
|
funnelEventsWithTiming.push(sessionStartEvent);
|
|
139
138
|
}
|
|
140
139
|
|
|
@@ -386,7 +385,11 @@ async function generateFunnelEvents(
|
|
|
386
385
|
}
|
|
387
386
|
|
|
388
387
|
try {
|
|
389
|
-
|
|
388
|
+
let computedTime = dayjs(funnelStartTime).add(event.relativeTimeMs, "milliseconds");
|
|
389
|
+
// Drop events that would land in the future
|
|
390
|
+
if (context.MAX_TIME && computedTime.unix() > context.MAX_TIME) {
|
|
391
|
+
newEvent._drop = true;
|
|
392
|
+
}
|
|
390
393
|
if (computedTime.isValid()) {
|
|
391
394
|
newEvent.time = computedTime.toISOString();
|
|
392
395
|
}
|
package/lib/generators/scd.js
CHANGED
|
@@ -41,19 +41,22 @@ export async function makeSCD(context, scdProp, scdKey, distinct_id, mutations,
|
|
|
41
41
|
} = scdProp;
|
|
42
42
|
|
|
43
43
|
// Return empty array if no values provided
|
|
44
|
-
if (
|
|
44
|
+
if (!values || (Array.isArray(values) && values.length === 0) ||
|
|
45
|
+
(typeof values === 'object' && Object.keys(values).length === 0)) {
|
|
45
46
|
return [];
|
|
46
47
|
}
|
|
47
|
-
|
|
48
|
+
|
|
48
49
|
const scdEntries = [];
|
|
49
50
|
let lastInserted = dayjs(created);
|
|
50
|
-
const
|
|
51
|
+
const now = dayjs();
|
|
52
|
+
const deltaDays = now.diff(lastInserted, "day");
|
|
51
53
|
const uuidKeyName = type === 'user' ? 'distinct_id' : type;
|
|
54
|
+
let lastStartTime = null; // Track for monotonic ordering
|
|
52
55
|
|
|
53
56
|
for (let i = 0; i < mutations; i++) {
|
|
54
57
|
// Stop if we've reached the current time
|
|
55
|
-
if (lastInserted.isAfter(
|
|
56
|
-
|
|
58
|
+
if (lastInserted.isAfter(now)) break;
|
|
59
|
+
|
|
57
60
|
// Create profile with the SCD property
|
|
58
61
|
const scd = await makeProfile(context, { [scdKey]: values }, { [uuidKeyName]: distinct_id });
|
|
59
62
|
|
|
@@ -78,6 +81,9 @@ export async function makeSCD(context, scdProp, scdKey, distinct_id, mutations,
|
|
|
78
81
|
case "month":
|
|
79
82
|
scdEntry.startTime = lastInserted.add(1, "month").startOf('month').toISOString();
|
|
80
83
|
break;
|
|
84
|
+
case "year":
|
|
85
|
+
scdEntry.startTime = lastInserted.add(1, "year").startOf('year').toISOString();
|
|
86
|
+
break;
|
|
81
87
|
}
|
|
82
88
|
}
|
|
83
89
|
|
|
@@ -85,28 +91,34 @@ export async function makeSCD(context, scdProp, scdKey, distinct_id, mutations,
|
|
|
85
91
|
scdEntry.startTime = lastInserted.toISOString();
|
|
86
92
|
}
|
|
87
93
|
|
|
94
|
+
// Enforce monotonic ordering — startTime must advance
|
|
95
|
+
if (lastStartTime && scdEntry.startTime && scdEntry.startTime <= lastStartTime) {
|
|
96
|
+
scdEntry.startTime = dayjs(lastStartTime).add(1, "second").toISOString();
|
|
97
|
+
}
|
|
98
|
+
lastStartTime = scdEntry.startTime;
|
|
99
|
+
|
|
88
100
|
// Set insert time (slightly after start time)
|
|
89
|
-
const insertTime =
|
|
101
|
+
const insertTime = dayjs(scdEntry.startTime).add(u.integer(1, 9000), "seconds");
|
|
90
102
|
scdEntry.insertTime = insertTime.toISOString();
|
|
91
103
|
|
|
92
104
|
// Set the time field for Mixpanel SCD import (uses startTime)
|
|
93
105
|
scdEntry.time = scdEntry.startTime;
|
|
94
106
|
|
|
95
107
|
// Only add entry if all required properties are set
|
|
96
|
-
if (scdEntry.
|
|
108
|
+
if (scdEntry.startTime && scdEntry.insertTime && scdEntry.time) {
|
|
97
109
|
scdEntries.push(scdEntry);
|
|
98
110
|
}
|
|
99
111
|
|
|
100
|
-
// Advance time for next entry
|
|
101
|
-
|
|
102
|
-
.
|
|
103
|
-
|
|
112
|
+
// Advance time for next entry — scale step size to available range
|
|
113
|
+
if (deltaDays > 1) {
|
|
114
|
+
const advance = Math.max(1, u.integer(0, Math.ceil(deltaDays / mutations)));
|
|
115
|
+
lastInserted = dayjs(scdEntry.startTime).add(advance, "day");
|
|
116
|
+
} else {
|
|
117
|
+
// Short range: advance by hours instead of days
|
|
118
|
+
const advance = Math.max(1, u.integer(1, 12));
|
|
119
|
+
lastInserted = dayjs(scdEntry.startTime).add(advance, "hour");
|
|
120
|
+
}
|
|
104
121
|
}
|
|
105
122
|
|
|
106
|
-
|
|
107
|
-
const deduped = scdEntries.filter((entry, index, self) =>
|
|
108
|
-
index === self.findIndex((t) => t.startTime === entry.startTime)
|
|
109
|
-
);
|
|
110
|
-
|
|
111
|
-
return deduped;
|
|
123
|
+
return scdEntries;
|
|
112
124
|
}
|
package/lib/generators/text.js
CHANGED
|
@@ -27,23 +27,29 @@ import seedrandom from 'seedrandom';
|
|
|
27
27
|
import crypto from 'crypto';
|
|
28
28
|
import SentimentPkg from 'sentiment';
|
|
29
29
|
import { PHRASE_BANK, GENERATION_PATTERNS, ORGANIC_PATTERNS } from '../templates/phrases.js';
|
|
30
|
+
import { getChance } from '../utils/utils.js';
|
|
30
31
|
|
|
31
32
|
const Sentiment = typeof SentimentPkg === 'function' ? SentimentPkg : SentimentPkg.default;
|
|
32
33
|
const sentiment = new Sentiment();
|
|
33
34
|
|
|
34
35
|
// ============= Helper Functions =============
|
|
35
36
|
|
|
37
|
+
function seededRandom() {
|
|
38
|
+
const c = getChance();
|
|
39
|
+
return c.floating({ min: 0, max: 1 });
|
|
40
|
+
}
|
|
41
|
+
|
|
36
42
|
function chance(probability) {
|
|
37
|
-
return
|
|
43
|
+
return seededRandom() < probability;
|
|
38
44
|
}
|
|
39
45
|
|
|
40
46
|
function pick(array) {
|
|
41
|
-
return array[Math.floor(
|
|
47
|
+
return array[Math.floor(seededRandom() * array.length)];
|
|
42
48
|
}
|
|
43
49
|
|
|
44
50
|
function pickWeighted(items, weights) {
|
|
45
51
|
const total = weights.reduce((a, b) => a + b, 0);
|
|
46
|
-
let random =
|
|
52
|
+
let random = seededRandom() * total;
|
|
47
53
|
for (let i = 0; i < items.length; i++) {
|
|
48
54
|
random -= weights[i];
|
|
49
55
|
if (random <= 0) return items[i];
|
|
@@ -437,7 +443,7 @@ class NaturalTypoEngine {
|
|
|
437
443
|
|
|
438
444
|
// Fallback: transpose letters
|
|
439
445
|
if (word.length > 3) {
|
|
440
|
-
const pos = Math.floor(
|
|
446
|
+
const pos = Math.floor(seededRandom() * (word.length - 2)) + 1;
|
|
441
447
|
return word.slice(0, pos) + word[pos + 1] + word[pos] + word.slice(pos + 2);
|
|
442
448
|
}
|
|
443
449
|
|
|
@@ -705,7 +711,7 @@ class OrganicTextGenerator {
|
|
|
705
711
|
|
|
706
712
|
generateStreamOfConsciousness() {
|
|
707
713
|
const thoughts = [];
|
|
708
|
-
const numThoughts = 2 + Math.floor(
|
|
714
|
+
const numThoughts = 2 + Math.floor(seededRandom() * 4);
|
|
709
715
|
|
|
710
716
|
for (let i = 0; i < numThoughts; i++) {
|
|
711
717
|
const thought = this.thoughtStream.generateThought(
|
|
@@ -778,11 +784,11 @@ class OrganicTextGenerator {
|
|
|
778
784
|
generateFragmented() {
|
|
779
785
|
const fragments = [];
|
|
780
786
|
const numFragments = this.config.style === 'search' ?
|
|
781
|
-
1 + Math.floor(
|
|
782
|
-
2 + Math.floor(
|
|
787
|
+
1 + Math.floor(seededRandom() * 3) :
|
|
788
|
+
2 + Math.floor(seededRandom() * 4);
|
|
783
789
|
|
|
784
790
|
for (let i = 0; i < numFragments; i++) {
|
|
785
|
-
const type =
|
|
791
|
+
const type = seededRandom();
|
|
786
792
|
|
|
787
793
|
if (type < 0.3) {
|
|
788
794
|
fragments.push(pick(ORGANIC_PATTERNS.fragments.incomplete));
|
|
@@ -933,7 +939,7 @@ class OrganicTextGenerator {
|
|
|
933
939
|
|
|
934
940
|
// Insert markers naturally
|
|
935
941
|
for (const marker of markers) {
|
|
936
|
-
const insertPoint = Math.floor(
|
|
942
|
+
const insertPoint = Math.floor(seededRandom() * text.length);
|
|
937
943
|
text = text.slice(0, insertPoint) + ' ' + marker + ' ' + text.slice(insertPoint);
|
|
938
944
|
}
|
|
939
945
|
|
|
@@ -958,8 +964,8 @@ class OrganicTextGenerator {
|
|
|
958
964
|
}
|
|
959
965
|
|
|
960
966
|
addTimestamp(text) {
|
|
961
|
-
const hour = Math.floor(
|
|
962
|
-
const min = Math.floor(
|
|
967
|
+
const hour = Math.floor(seededRandom() * 24);
|
|
968
|
+
const min = Math.floor(seededRandom() * 60);
|
|
963
969
|
const timestamp = `[${hour}:${min.toString().padStart(2, '0')}]`;
|
|
964
970
|
|
|
965
971
|
return timestamp + ' ' + text;
|
|
@@ -978,7 +984,7 @@ class OrganicTextGenerator {
|
|
|
978
984
|
}
|
|
979
985
|
|
|
980
986
|
breakSentence(sentence) {
|
|
981
|
-
const breakPoint = Math.floor(sentence.length * (0.3 +
|
|
987
|
+
const breakPoint = Math.floor(sentence.length * (0.3 + seededRandom() * 0.4));
|
|
982
988
|
return sentence.slice(0, breakPoint) + '...';
|
|
983
989
|
}
|
|
984
990
|
|