@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.
Files changed (54) hide show
  1. package/dungeons/technical/ad-spend.js +15 -4
  2. package/dungeons/technical/anonymous-users.js +15 -5
  3. package/dungeons/technical/array-of-object-lookup.js +21 -5
  4. package/dungeons/technical/experiments.js +23 -5
  5. package/dungeons/technical/foobar.js +28 -12
  6. package/dungeons/technical/group-analytics.js +15 -5
  7. package/dungeons/technical/mirror-strategies.js +15 -5
  8. package/dungeons/technical/nested-objects.js +15 -4
  9. package/dungeons/technical/retention-cadence.js +13 -7
  10. package/dungeons/technical/sanity.js +25 -6
  11. package/dungeons/technical/scale-test.js +15 -4
  12. package/dungeons/technical/scd.js +24 -5
  13. package/dungeons/technical/simple.js +20 -5
  14. package/dungeons/technical/simplest.js +23 -6
  15. package/dungeons/technical/text-generation.js +22 -6
  16. package/dungeons/user/.gitkeep +0 -0
  17. package/dungeons/vertical/community-schema.json +1 -1
  18. package/dungeons/vertical/community.js +49 -28
  19. package/dungeons/vertical/devtools-schema.json +1 -1
  20. package/dungeons/vertical/devtools.js +50 -29
  21. package/dungeons/vertical/ecommerce.js +23 -6
  22. package/dungeons/vertical/education-schema.json +1 -1
  23. package/dungeons/vertical/education.js +29 -19
  24. package/dungeons/vertical/fintech-schema.json +1 -1
  25. package/dungeons/vertical/fintech.js +27 -8
  26. package/dungeons/vertical/fitness-schema.json +1 -1
  27. package/dungeons/vertical/fitness.js +48 -22
  28. package/dungeons/vertical/food-delivery-schema.json +1 -1
  29. package/dungeons/vertical/food-delivery.js +51 -12
  30. package/dungeons/vertical/gaming-schema.json +1 -1
  31. package/dungeons/vertical/gaming.js +41 -7
  32. package/dungeons/vertical/healthcare-schema.json +1 -1
  33. package/dungeons/vertical/healthcare.js +69 -40
  34. package/dungeons/vertical/insurance-application-schema.json +1 -1
  35. package/dungeons/vertical/insurance-application.js +26 -8
  36. package/dungeons/vertical/logistics-schema.json +1 -1
  37. package/dungeons/vertical/logistics.js +69 -41
  38. package/dungeons/vertical/marketplace-schema.json +1 -1
  39. package/dungeons/vertical/marketplace.js +44 -19
  40. package/dungeons/vertical/media.js +56 -15
  41. package/dungeons/vertical/rpg-schema.json +1 -1
  42. package/dungeons/vertical/rpg.js +38 -8
  43. package/dungeons/vertical/sass.js +24 -7
  44. package/dungeons/vertical/social.js +23 -7
  45. package/dungeons/vertical/travel-schema.json +1 -1
  46. package/dungeons/vertical/travel.js +50 -19
  47. package/index.js +3 -0
  48. package/lib/core/config-validator.js +13 -2
  49. package/lib/generators/events.js +3 -5
  50. package/lib/orchestrators/mixpanel-sender.js +64 -3
  51. package/lib/orchestrators/user-loop.js +20 -0
  52. package/lib/utils/utils.js +129 -9
  53. package/package.json +2 -2
  54. package/types.d.ts +5 -3
@@ -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
- //session Ids
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.0",
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.8",
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;