@ak--47/dungeon-master 1.2.0 → 1.2.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/dungeons/technical/ad-spend.js +15 -4
- package/dungeons/technical/anonymous-users.js +15 -5
- package/dungeons/technical/array-of-object-lookup.js +21 -5
- package/dungeons/technical/experiments.js +23 -5
- package/dungeons/technical/foobar.js +28 -12
- package/dungeons/technical/group-analytics.js +15 -5
- package/dungeons/technical/mirror-strategies.js +15 -5
- package/dungeons/technical/nested-objects.js +15 -4
- package/dungeons/technical/retention-cadence.js +13 -7
- package/dungeons/technical/sanity.js +25 -6
- package/dungeons/technical/scale-test.js +15 -4
- package/dungeons/technical/scd.js +24 -5
- package/dungeons/technical/simple.js +20 -5
- package/dungeons/technical/simplest.js +23 -6
- package/dungeons/technical/text-generation.js +22 -6
- package/dungeons/user/.gitkeep +0 -0
- package/dungeons/vertical/community-schema.json +1 -1
- package/dungeons/vertical/community.js +49 -28
- package/dungeons/vertical/devtools-schema.json +1 -1
- package/dungeons/vertical/devtools.js +50 -29
- package/dungeons/vertical/ecommerce.js +23 -6
- package/dungeons/vertical/education-schema.json +1 -1
- package/dungeons/vertical/education.js +29 -19
- package/dungeons/vertical/fintech-schema.json +1 -1
- package/dungeons/vertical/fintech.js +27 -8
- package/dungeons/vertical/fitness-schema.json +1 -1
- package/dungeons/vertical/fitness.js +48 -22
- package/dungeons/vertical/food-delivery-schema.json +1 -1
- package/dungeons/vertical/food-delivery.js +51 -12
- package/dungeons/vertical/gaming-schema.json +1 -1
- package/dungeons/vertical/gaming.js +41 -7
- package/dungeons/vertical/healthcare-schema.json +1 -1
- package/dungeons/vertical/healthcare.js +69 -40
- package/dungeons/vertical/insurance-application-schema.json +1 -1
- package/dungeons/vertical/insurance-application.js +26 -8
- package/dungeons/vertical/logistics-schema.json +1 -1
- package/dungeons/vertical/logistics.js +69 -41
- package/dungeons/vertical/marketplace-schema.json +1 -1
- package/dungeons/vertical/marketplace.js +44 -19
- package/dungeons/vertical/media.js +56 -15
- package/dungeons/vertical/rpg-schema.json +1 -1
- package/dungeons/vertical/rpg.js +38 -8
- package/dungeons/vertical/sass.js +24 -7
- package/dungeons/vertical/social.js +23 -7
- package/dungeons/vertical/travel-schema.json +1 -1
- package/dungeons/vertical/travel.js +50 -19
- package/index.js +3 -0
- package/lib/core/config-validator.js +13 -2
- package/lib/generators/events.js +3 -5
- package/lib/orchestrators/mixpanel-sender.js +64 -3
- package/lib/orchestrators/user-loop.js +20 -0
- package/lib/utils/utils.js +129 -9
- package/package.json +2 -2
- package/types.d.ts +5 -3
package/lib/utils/utils.js
CHANGED
|
@@ -1314,15 +1314,7 @@ function person(userId, bornDaysAgo = 30, isAnonymous = false, hasAvatar = false
|
|
|
1314
1314
|
|
|
1315
1315
|
if (!hasAnonIds) delete user.anonymousIds;
|
|
1316
1316
|
|
|
1317
|
-
//
|
|
1318
|
-
if (hasSessionIds) {
|
|
1319
|
-
const sessionSize = integer(5, 30);
|
|
1320
|
-
for (let i = 0; i < sessionSize; i++) {
|
|
1321
|
-
const sessionId = [uid(5), uid(5), uid(5), uid(5)].join("-");
|
|
1322
|
-
user.sessionIds.push(sessionId);
|
|
1323
|
-
}
|
|
1324
|
-
}
|
|
1325
|
-
|
|
1317
|
+
// Session IDs are now assigned post-hoc in user-loop.js based on temporal gaps
|
|
1326
1318
|
if (!hasSessionIds) delete user.sessionIds;
|
|
1327
1319
|
|
|
1328
1320
|
return user;
|
|
@@ -1436,6 +1428,131 @@ function deepClone(thing, opts) {
|
|
|
1436
1428
|
};
|
|
1437
1429
|
|
|
1438
1430
|
|
|
1431
|
+
/**
|
|
1432
|
+
* Generates a session ID in the standard format
|
|
1433
|
+
* @returns {string} Session ID like "xxxxx-xxxxx-xxxxx-xxxxx"
|
|
1434
|
+
*/
|
|
1435
|
+
function generateSessionId() {
|
|
1436
|
+
return [uid(5), uid(5), uid(5), uid(5)].join("-");
|
|
1437
|
+
}
|
|
1438
|
+
|
|
1439
|
+
/**
|
|
1440
|
+
* Redistributes events into temporal clusters (sessions).
|
|
1441
|
+
*
|
|
1442
|
+
* Algorithm:
|
|
1443
|
+
* 1. Sort events by time
|
|
1444
|
+
* 2. Determine number of sessions (total events / avg events per session)
|
|
1445
|
+
* 3. Generate session anchor times using TimeSoup
|
|
1446
|
+
* 4. Assign events round-robin to sessions
|
|
1447
|
+
* 5. Within each session, retime events with tight spacing (5-300s apart)
|
|
1448
|
+
* 6. Regenerate insert_ids for retimed events
|
|
1449
|
+
* 7. Re-sort by time
|
|
1450
|
+
*
|
|
1451
|
+
* Mutates events in place. Does NOT assign session_id (call assignSessionIds after).
|
|
1452
|
+
*
|
|
1453
|
+
* @param {Object[]} events - Array of event objects with .time (ISO string)
|
|
1454
|
+
* @param {number} timeoutMinutes - Session timeout in minutes (used to determine intra-session spacing)
|
|
1455
|
+
* @param {Object} soupParams - Parameters for TimeSoup anchor generation
|
|
1456
|
+
*/
|
|
1457
|
+
function bunchIntoSessions(events, timeoutMinutes, soupParams) {
|
|
1458
|
+
if (events.length < 2) return;
|
|
1459
|
+
|
|
1460
|
+
const chance = getChance();
|
|
1461
|
+
const { earliestTime, latestTime, peaks, deviation, mean,
|
|
1462
|
+
dayOfWeekWeights, hourOfDayWeights, timeShiftSeconds, maxTime } = soupParams;
|
|
1463
|
+
|
|
1464
|
+
// Sort by time first
|
|
1465
|
+
events.sort((a, b) => a.time < b.time ? -1 : a.time > b.time ? 1 : 0);
|
|
1466
|
+
|
|
1467
|
+
// Determine number of sessions: target 3-8 events per session
|
|
1468
|
+
const eventsPerSession = chance.integer({ min: 3, max: 8 });
|
|
1469
|
+
const numSessions = Math.max(1, Math.ceil(events.length / eventsPerSession));
|
|
1470
|
+
|
|
1471
|
+
// Generate session anchor times using TimeSoup
|
|
1472
|
+
const anchors = [];
|
|
1473
|
+
for (let i = 0; i < numSessions; i++) {
|
|
1474
|
+
const soupTime = TimeSoup(earliestTime, latestTime, peaks, deviation, mean,
|
|
1475
|
+
dayOfWeekWeights, hourOfDayWeights, timeShiftSeconds);
|
|
1476
|
+
anchors.push(soupTime + timeShiftSeconds); // shifted to present time
|
|
1477
|
+
}
|
|
1478
|
+
anchors.sort((a, b) => a - b);
|
|
1479
|
+
|
|
1480
|
+
// Distribute events across sessions round-robin (preserving original order → temporal order)
|
|
1481
|
+
const sessionBuckets = anchors.map(() => []);
|
|
1482
|
+
for (let i = 0; i < events.length; i++) {
|
|
1483
|
+
const bucketIndex = Math.min(i % numSessions, numSessions - 1);
|
|
1484
|
+
sessionBuckets[bucketIndex].push(events[i]);
|
|
1485
|
+
}
|
|
1486
|
+
|
|
1487
|
+
// Retime events within each session
|
|
1488
|
+
let writeIndex = 0;
|
|
1489
|
+
for (let s = 0; s < numSessions; s++) {
|
|
1490
|
+
const bucket = sessionBuckets[s];
|
|
1491
|
+
if (bucket.length === 0) continue;
|
|
1492
|
+
|
|
1493
|
+
let currentTime = anchors[s];
|
|
1494
|
+
for (let e = 0; e < bucket.length; e++) {
|
|
1495
|
+
const ev = bucket[e];
|
|
1496
|
+
const clampedTime = Math.min(currentTime, maxTime);
|
|
1497
|
+
|
|
1498
|
+
ev.time = dayjs.unix(clampedTime).toISOString();
|
|
1499
|
+
// Regenerate insert_id to match new time
|
|
1500
|
+
const distinctId = ev.user_id || ev.device_id || ev.distinct_id || '';
|
|
1501
|
+
ev.insert_id = quickHash(`${ev.event}-${ev.time}-${distinctId}`);
|
|
1502
|
+
|
|
1503
|
+
if (currentTime > maxTime) {
|
|
1504
|
+
ev._drop = true;
|
|
1505
|
+
}
|
|
1506
|
+
|
|
1507
|
+
// Advance time within session: 5-300 seconds (5s to 5min)
|
|
1508
|
+
currentTime += chance.integer({ min: 5, max: 300 });
|
|
1509
|
+
}
|
|
1510
|
+
}
|
|
1511
|
+
|
|
1512
|
+
// Re-sort by time
|
|
1513
|
+
events.sort((a, b) => a.time < b.time ? -1 : a.time > b.time ? 1 : 0);
|
|
1514
|
+
}
|
|
1515
|
+
|
|
1516
|
+
/**
|
|
1517
|
+
* Assigns session IDs to a chronologically sorted array of events.
|
|
1518
|
+
* A new session starts when:
|
|
1519
|
+
* - Gap between consecutive events exceeds timeoutMinutes
|
|
1520
|
+
* - Session duration exceeds 24 hours
|
|
1521
|
+
*
|
|
1522
|
+
* Events MUST be sorted by time before calling this function.
|
|
1523
|
+
* Mutates events in place (adds session_id property).
|
|
1524
|
+
*
|
|
1525
|
+
* @param {Object[]} events - Sorted array of event objects with .time (ISO string)
|
|
1526
|
+
* @param {number} timeoutMinutes - Session timeout in minutes (default 30)
|
|
1527
|
+
* @returns {Object[]} Same array, with session_id added to each event
|
|
1528
|
+
*/
|
|
1529
|
+
function assignSessionIds(events, timeoutMinutes = 30) {
|
|
1530
|
+
if (!events.length) return events;
|
|
1531
|
+
|
|
1532
|
+
const timeoutMs = timeoutMinutes * 60 * 1000;
|
|
1533
|
+
const maxSessionMs = 24 * 60 * 60 * 1000;
|
|
1534
|
+
|
|
1535
|
+
let currentSessionId = generateSessionId();
|
|
1536
|
+
let sessionStartMs = new Date(events[0].time).getTime();
|
|
1537
|
+
let lastEventMs = sessionStartMs;
|
|
1538
|
+
|
|
1539
|
+
for (const event of events) {
|
|
1540
|
+
const eventMs = new Date(event.time).getTime();
|
|
1541
|
+
const gapFromLast = eventMs - lastEventMs;
|
|
1542
|
+
const sessionDuration = eventMs - sessionStartMs;
|
|
1543
|
+
|
|
1544
|
+
if (gapFromLast > timeoutMs || sessionDuration > maxSessionMs) {
|
|
1545
|
+
currentSessionId = generateSessionId();
|
|
1546
|
+
sessionStartMs = eventMs;
|
|
1547
|
+
}
|
|
1548
|
+
|
|
1549
|
+
event.session_id = currentSessionId;
|
|
1550
|
+
lastEventMs = eventMs;
|
|
1551
|
+
}
|
|
1552
|
+
|
|
1553
|
+
return events;
|
|
1554
|
+
}
|
|
1555
|
+
|
|
1439
1556
|
export {
|
|
1440
1557
|
pick,
|
|
1441
1558
|
date,
|
|
@@ -1486,4 +1603,7 @@ export {
|
|
|
1486
1603
|
wrapFunc,
|
|
1487
1604
|
bytesHuman,
|
|
1488
1605
|
formatDuration,
|
|
1606
|
+
generateSessionId,
|
|
1607
|
+
assignSessionIds,
|
|
1608
|
+
bunchIntoSessions,
|
|
1489
1609
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ak--47/dungeon-master",
|
|
3
|
-
"version": "1.2.
|
|
3
|
+
"version": "1.2.2",
|
|
4
4
|
"description": "generate fancy datasets",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "index.js",
|
|
@@ -66,7 +66,7 @@
|
|
|
66
66
|
"dotenv": "^16.4.5",
|
|
67
67
|
"hyparquet-writer": "^0.6.1",
|
|
68
68
|
"mixpanel": "^0.18.0",
|
|
69
|
-
"mixpanel-import": "^3.2.
|
|
69
|
+
"mixpanel-import": "^3.2.9",
|
|
70
70
|
"p-limit": "^3.1.0",
|
|
71
71
|
"pino": "^9.0.0",
|
|
72
72
|
"pino-pretty": "^11.0.0",
|
package/types.d.ts
CHANGED
|
@@ -72,8 +72,10 @@ export interface Dungeon {
|
|
|
72
72
|
verbose?: boolean;
|
|
73
73
|
/** If true, users get anonymous device IDs in addition to distinct_id. */
|
|
74
74
|
hasAnonIds?: boolean;
|
|
75
|
-
/** If true, users get session IDs attached to events. */
|
|
75
|
+
/** If true, users get session IDs attached to events based on temporal clustering. */
|
|
76
76
|
hasSessionIds?: boolean;
|
|
77
|
+
/** Session timeout in minutes. Events with gaps exceeding this start a new session. Default: 30. Only used when hasSessionIds is true. */
|
|
78
|
+
sessionTimeout?: number;
|
|
77
79
|
/** If true, auto-generates funnels from the events array in addition to any explicit funnels. */
|
|
78
80
|
alsoInferFunnels?: boolean;
|
|
79
81
|
/** Restrict all location data to a single country (e.g., "US", "GB"). */
|
|
@@ -231,8 +233,8 @@ export interface hookArrayOptions<T> {
|
|
|
231
233
|
* an enriched array is an array that has a hookPush method that can be used to transform-then-push items into the array
|
|
232
234
|
*/
|
|
233
235
|
export interface HookedArray<T> extends Array<T> {
|
|
234
|
-
hookPush: (item: T | T[], ...meta: any[]) => any
|
|
235
|
-
flush: () => void
|
|
236
|
+
hookPush: (item: T | T[], ...meta: any[]) => Promise<any>;
|
|
237
|
+
flush: () => Promise<void>;
|
|
236
238
|
getWriteDir: () => string;
|
|
237
239
|
getWritePath: () => string;
|
|
238
240
|
[key: string]: any;
|