@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/dungeons/vertical/rpg.js
CHANGED
|
@@ -1,14 +1,21 @@
|
|
|
1
|
+
// ── TWEAK THESE ──
|
|
2
|
+
const SEED = "harness-gaming";
|
|
3
|
+
const num_days = 100;
|
|
4
|
+
const num_users = 5_000;
|
|
5
|
+
const avg_events_per_user = 120;
|
|
6
|
+
let token = "your-mixpanel-token";
|
|
7
|
+
|
|
8
|
+
// ── env overrides ──
|
|
9
|
+
if (process.env.MP_TOKEN) token = process.env.MP_TOKEN;
|
|
10
|
+
|
|
1
11
|
import dayjs from "dayjs";
|
|
2
12
|
import utc from "dayjs/plugin/utc.js";
|
|
3
13
|
import "dotenv/config";
|
|
4
14
|
import * as u from "../../lib/utils/utils.js";
|
|
5
15
|
import * as v from "ak-tools";
|
|
6
16
|
|
|
7
|
-
const SEED = "harness-gaming";
|
|
8
17
|
dayjs.extend(utc);
|
|
9
18
|
const chance = u.initChance(SEED);
|
|
10
|
-
const num_users = 5_000;
|
|
11
|
-
const days = 100;
|
|
12
19
|
|
|
13
20
|
/** @typedef {import("../../types").Dungeon} Config */
|
|
14
21
|
|
|
@@ -164,10 +171,10 @@ const itemIds = v.range(1, 301).map(n => `item_${v.uid(7)}`);
|
|
|
164
171
|
|
|
165
172
|
/** @type {Config} */
|
|
166
173
|
const config = {
|
|
167
|
-
token
|
|
174
|
+
token,
|
|
168
175
|
seed: SEED,
|
|
169
|
-
numDays:
|
|
170
|
-
numEvents: num_users *
|
|
176
|
+
numDays: num_days,
|
|
177
|
+
numEvents: num_users * avg_events_per_user,
|
|
171
178
|
numUsers: num_users,
|
|
172
179
|
hasAnonIds: false,
|
|
173
180
|
hasSessionIds: true,
|
|
@@ -519,7 +526,7 @@ const config = {
|
|
|
519
526
|
],
|
|
520
527
|
|
|
521
528
|
superProps: {
|
|
522
|
-
|
|
529
|
+
Platform: [
|
|
523
530
|
"PC",
|
|
524
531
|
"Mac",
|
|
525
532
|
"PlayStation",
|
|
@@ -562,6 +569,20 @@ const config = {
|
|
|
562
569
|
"Ranger",
|
|
563
570
|
"Paladin"
|
|
564
571
|
],
|
|
572
|
+
Platform: [
|
|
573
|
+
"PC",
|
|
574
|
+
"Mac",
|
|
575
|
+
"PlayStation",
|
|
576
|
+
"Xbox",
|
|
577
|
+
"Switch"
|
|
578
|
+
],
|
|
579
|
+
graphics_quality: [
|
|
580
|
+
"Low",
|
|
581
|
+
"Medium",
|
|
582
|
+
"High",
|
|
583
|
+
"Ultra"
|
|
584
|
+
],
|
|
585
|
+
subscription_tier: ["Free", "Free", "Free", "Premium", "Elite"],
|
|
565
586
|
},
|
|
566
587
|
|
|
567
588
|
groupKeys: [
|
|
@@ -595,7 +616,7 @@ const config = {
|
|
|
595
616
|
*/
|
|
596
617
|
hook: function (record, type, meta) {
|
|
597
618
|
const NOW = dayjs();
|
|
598
|
-
const DATASET_START = NOW.subtract(
|
|
619
|
+
const DATASET_START = NOW.subtract(num_days, 'days');
|
|
599
620
|
const CURSED_WEEK_START = DATASET_START.add(40, 'days');
|
|
600
621
|
const CURSED_WEEK_END = DATASET_START.add(47, 'days');
|
|
601
622
|
const LEGENDARY_WEAPON_RELEASE = DATASET_START.add(45, 'days');
|
|
@@ -642,6 +663,15 @@ const config = {
|
|
|
642
663
|
// Hook #3, #4, #5, #6, #1: EVERYTHING - Complex behavioral patterns
|
|
643
664
|
if (type === "everything") {
|
|
644
665
|
const userEvents = record;
|
|
666
|
+
const profile = meta.profile;
|
|
667
|
+
|
|
668
|
+
// Stamp superProps from profile for consistency
|
|
669
|
+
userEvents.forEach(e => {
|
|
670
|
+
e.Platform = profile.Platform;
|
|
671
|
+
e.graphics_quality = profile.graphics_quality;
|
|
672
|
+
e.subscription_tier = profile.subscription_tier;
|
|
673
|
+
});
|
|
674
|
+
|
|
645
675
|
const firstEventTime = userEvents.length > 0 ? dayjs(userEvents[0].time) : null;
|
|
646
676
|
|
|
647
677
|
// Track user behaviors
|
|
@@ -1,14 +1,21 @@
|
|
|
1
|
+
// ── TWEAK THESE ──
|
|
2
|
+
const SEED = "harness-sass";
|
|
3
|
+
const num_days = 100;
|
|
4
|
+
const num_users = 5_000;
|
|
5
|
+
const avg_events_per_user = 120;
|
|
6
|
+
let token = "your-mixpanel-token";
|
|
7
|
+
|
|
8
|
+
// ── env overrides ──
|
|
9
|
+
if (process.env.MP_TOKEN) token = process.env.MP_TOKEN;
|
|
10
|
+
|
|
1
11
|
import dayjs from "dayjs";
|
|
2
12
|
import utc from "dayjs/plugin/utc.js";
|
|
3
13
|
import "dotenv/config";
|
|
4
14
|
import * as u from "../../lib/utils/utils.js";
|
|
5
15
|
import * as v from "ak-tools";
|
|
6
16
|
|
|
7
|
-
const SEED = "harness-sass";
|
|
8
17
|
dayjs.extend(utc);
|
|
9
18
|
const chance = u.initChance(SEED);
|
|
10
|
-
const num_users = 5_000;
|
|
11
|
-
const days = 100;
|
|
12
19
|
|
|
13
20
|
/** @typedef {import("../../types").Dungeon} Config */
|
|
14
21
|
|
|
@@ -248,10 +255,10 @@ const failedDeployUsers = new Map();
|
|
|
248
255
|
|
|
249
256
|
/** @type {Config} */
|
|
250
257
|
const config = {
|
|
251
|
-
token
|
|
258
|
+
token,
|
|
252
259
|
seed: SEED,
|
|
253
|
-
numDays:
|
|
254
|
-
numEvents: num_users *
|
|
260
|
+
numDays: num_days,
|
|
261
|
+
numEvents: num_users * avg_events_per_user,
|
|
255
262
|
numUsers: num_users,
|
|
256
263
|
hasAnonIds: false,
|
|
257
264
|
hasSessionIds: true,
|
|
@@ -547,6 +554,8 @@ const config = {
|
|
|
547
554
|
annual_contract_value: [0],
|
|
548
555
|
customer_success_manager: [false],
|
|
549
556
|
customer_health_score: u.weighNumRange(1, 100),
|
|
557
|
+
plan_tier: ["free", "free", "team", "team", "business", "enterprise"],
|
|
558
|
+
cloud_provider: ["aws", "gcp", "azure", "multi_cloud"],
|
|
550
559
|
},
|
|
551
560
|
|
|
552
561
|
groupKeys: [
|
|
@@ -580,7 +589,7 @@ const config = {
|
|
|
580
589
|
*/
|
|
581
590
|
hook: function (record, type, meta) {
|
|
582
591
|
const NOW = dayjs();
|
|
583
|
-
const DATASET_START = NOW.subtract(
|
|
592
|
+
const DATASET_START = NOW.subtract(num_days, "days");
|
|
584
593
|
|
|
585
594
|
// ─────────────────────────────────────────────────────────────
|
|
586
595
|
// Hook #1: END-OF-QUARTER SPIKE (event)
|
|
@@ -698,6 +707,14 @@ const config = {
|
|
|
698
707
|
// ─────────────────────────────────────────────────────────────
|
|
699
708
|
if (type === "everything") {
|
|
700
709
|
const userEvents = record;
|
|
710
|
+
const profile = meta.profile;
|
|
711
|
+
|
|
712
|
+
// Stamp superProps from profile for consistency
|
|
713
|
+
userEvents.forEach(e => {
|
|
714
|
+
e.plan_tier = profile.plan_tier;
|
|
715
|
+
e.cloud_provider = profile.cloud_provider;
|
|
716
|
+
});
|
|
717
|
+
|
|
701
718
|
if (userEvents && userEvents.length > 0) {
|
|
702
719
|
const firstEvent = userEvents[0];
|
|
703
720
|
const idHash = String(firstEvent.user_id || firstEvent.device_id).split("").reduce((acc, char) => acc + char.charCodeAt(0), 0);
|
|
@@ -1,14 +1,21 @@
|
|
|
1
|
+
// ── TWEAK THESE ──
|
|
2
|
+
const SEED = "harness-social";
|
|
3
|
+
const num_days = 100;
|
|
4
|
+
const num_users = 5_000;
|
|
5
|
+
const avg_events_per_user = 120;
|
|
6
|
+
let token = "your-mixpanel-token";
|
|
7
|
+
|
|
8
|
+
// ── env overrides ──
|
|
9
|
+
if (process.env.MP_TOKEN) token = process.env.MP_TOKEN;
|
|
10
|
+
|
|
1
11
|
import dayjs from "dayjs";
|
|
2
12
|
import utc from "dayjs/plugin/utc.js";
|
|
3
13
|
import "dotenv/config";
|
|
4
14
|
import * as u from "../../lib/utils/utils.js";
|
|
5
15
|
import * as v from "ak-tools";
|
|
6
16
|
|
|
7
|
-
const SEED = "harness-social";
|
|
8
17
|
dayjs.extend(utc);
|
|
9
18
|
const chance = u.initChance(SEED);
|
|
10
|
-
const num_users = 5_000;
|
|
11
|
-
const days = 100;
|
|
12
19
|
|
|
13
20
|
/** @typedef {import("../../types").Dungeon} Config */
|
|
14
21
|
|
|
@@ -162,10 +169,10 @@ const postIds = v.range(1, 1001).map(n => `post_${v.uid(8)}`);
|
|
|
162
169
|
|
|
163
170
|
/** @type {Config} */
|
|
164
171
|
const config = {
|
|
165
|
-
token
|
|
172
|
+
token,
|
|
166
173
|
seed: SEED,
|
|
167
|
-
numDays:
|
|
168
|
-
numEvents: num_users *
|
|
174
|
+
numDays: num_days,
|
|
175
|
+
numEvents: num_users * avg_events_per_user,
|
|
169
176
|
numUsers: num_users,
|
|
170
177
|
hasAnonIds: false,
|
|
171
178
|
hasSessionIds: true,
|
|
@@ -445,6 +452,8 @@ const config = {
|
|
|
445
452
|
},
|
|
446
453
|
|
|
447
454
|
userProps: {
|
|
455
|
+
app_version: ["4.0", "4.1", "4.2", "4.3", "5.0"],
|
|
456
|
+
account_type: ["personal", "creator", "business"],
|
|
448
457
|
"follower_count": u.weighNumRange(0, 10000, 0.2, 50),
|
|
449
458
|
"following_count": u.weighNumRange(0, 5000, 0.3, 100),
|
|
450
459
|
"bio_length": u.weighNumRange(0, 160),
|
|
@@ -469,7 +478,7 @@ const config = {
|
|
|
469
478
|
|
|
470
479
|
hook: function (record, type, meta) {
|
|
471
480
|
const NOW = dayjs();
|
|
472
|
-
const DATASET_START = NOW.subtract(
|
|
481
|
+
const DATASET_START = NOW.subtract(num_days, 'days');
|
|
473
482
|
const ALGORITHM_CHANGE_DAY = DATASET_START.add(45, 'days');
|
|
474
483
|
const REENGAGEMENT_START = DATASET_START.add(30, 'days');
|
|
475
484
|
|
|
@@ -529,6 +538,13 @@ const config = {
|
|
|
529
538
|
const userEvents = record;
|
|
530
539
|
if (!userEvents || userEvents.length === 0) return record;
|
|
531
540
|
|
|
541
|
+
// Stamp superProps from profile for consistency
|
|
542
|
+
const profile = meta.profile;
|
|
543
|
+
userEvents.forEach(e => {
|
|
544
|
+
e.app_version = profile.app_version;
|
|
545
|
+
e.account_type = profile.account_type;
|
|
546
|
+
});
|
|
547
|
+
|
|
532
548
|
// Tracking variables for user patterns
|
|
533
549
|
let postCreatedCount = 0;
|
|
534
550
|
let followReceivedCount = 0;
|
|
@@ -1,16 +1,23 @@
|
|
|
1
|
+
// ── TWEAK THESE ──
|
|
2
|
+
const SEED = "dm4-travel";
|
|
3
|
+
const num_days = 100;
|
|
4
|
+
const num_users = 5_000;
|
|
5
|
+
const avg_events_per_user = 120;
|
|
6
|
+
let token = "your-mixpanel-token";
|
|
7
|
+
|
|
8
|
+
// ── env overrides ──
|
|
9
|
+
if (process.env.MP_TOKEN) token = process.env.MP_TOKEN;
|
|
10
|
+
|
|
1
11
|
import dayjs from "dayjs";
|
|
2
12
|
import utc from "dayjs/plugin/utc.js";
|
|
3
13
|
import "dotenv/config";
|
|
4
14
|
import * as u from "../../lib/utils/utils.js";
|
|
5
15
|
import * as v from "ak-tools";
|
|
6
16
|
|
|
7
|
-
const SEED = "dm4-travel";
|
|
8
17
|
dayjs.extend(utc);
|
|
9
18
|
const chance = u.initChance(SEED);
|
|
10
|
-
const num_users = 5_000;
|
|
11
|
-
const days = 100;
|
|
12
19
|
const NOW = dayjs();
|
|
13
|
-
const DATASET_START = NOW.subtract(
|
|
20
|
+
const DATASET_START = NOW.subtract(num_days, "days");
|
|
14
21
|
|
|
15
22
|
/** @typedef {import("../../types").Dungeon} Config */
|
|
16
23
|
|
|
@@ -164,10 +171,10 @@ const destinationCities = ["New York", "London", "Paris", "Tokyo", "Barcelona",
|
|
|
164
171
|
* with consistent, high-frequency booking patterns.
|
|
165
172
|
*
|
|
166
173
|
* ───────────────────────────────────────────────────────────────
|
|
167
|
-
* 8. REPEAT DESTINATION CLUSTERING (
|
|
174
|
+
* 8. REPEAT DESTINATION CLUSTERING (everything hook — event filtering)
|
|
168
175
|
* ───────────────────────────────────────────────────────────────
|
|
169
|
-
* PATTERN:
|
|
170
|
-
*
|
|
176
|
+
* PATTERN: Non-business/luxury users have ~25% of "booking completed"
|
|
177
|
+
* events dropped, simulating lower funnel conversion for casual segments.
|
|
171
178
|
*
|
|
172
179
|
* HOW TO FIND IT IN MIXPANEL:
|
|
173
180
|
* Report 1: Conversion by Segment
|
|
@@ -197,10 +204,10 @@ const destinationCities = ["New York", "London", "Paris", "Tokyo", "Barcelona",
|
|
|
197
204
|
|
|
198
205
|
/** @type {Config} */
|
|
199
206
|
const config = {
|
|
200
|
-
token
|
|
207
|
+
token,
|
|
201
208
|
seed: SEED,
|
|
202
|
-
numDays:
|
|
203
|
-
numEvents: num_users *
|
|
209
|
+
numDays: num_days,
|
|
210
|
+
numEvents: num_users * avg_events_per_user,
|
|
204
211
|
numUsers: num_users,
|
|
205
212
|
hasAnonIds: false,
|
|
206
213
|
hasSessionIds: true,
|
|
@@ -445,7 +452,7 @@ const config = {
|
|
|
445
452
|
|
|
446
453
|
// ── SuperProps ──────────────────────────────────────────
|
|
447
454
|
superProps: {
|
|
448
|
-
|
|
455
|
+
Platform: ["ios", "android", "web", "web"],
|
|
449
456
|
membership_tier: ["standard", "standard", "standard", "gold", "platinum"],
|
|
450
457
|
},
|
|
451
458
|
|
|
@@ -456,6 +463,8 @@ const config = {
|
|
|
456
463
|
company_name: ["none"],
|
|
457
464
|
preferred_destination: chance.pickone.bind(chance, destinationCities),
|
|
458
465
|
avg_budget_per_night: u.weighNumRange(50, 400, 0.4, 150),
|
|
466
|
+
Platform: ["ios", "android", "web", "web"],
|
|
467
|
+
membership_tier: ["standard", "standard", "standard", "gold", "platinum"],
|
|
459
468
|
},
|
|
460
469
|
|
|
461
470
|
// ── Personas ──────────────────────────────────
|
|
@@ -632,14 +641,10 @@ const config = {
|
|
|
632
641
|
}
|
|
633
642
|
|
|
634
643
|
// ── HOOK 8: REPEAT DESTINATION CLUSTERING (funnel-pre) ─
|
|
644
|
+
// conversionRate modifications moved to everything hook (event filtering)
|
|
645
|
+
// to avoid dilution by organic events
|
|
635
646
|
if (type === "funnel-pre") {
|
|
636
|
-
|
|
637
|
-
if (meta.profile.customer_segment === "business_traveler") {
|
|
638
|
-
record.conversionRate = Math.min(record.conversionRate * 1.3, 90);
|
|
639
|
-
} else if (meta.profile.customer_segment === "luxury_seeker") {
|
|
640
|
-
record.conversionRate = Math.min(record.conversionRate * 1.15, 85);
|
|
641
|
-
}
|
|
642
|
-
}
|
|
647
|
+
// prop-setting only; no conversionRate changes
|
|
643
648
|
}
|
|
644
649
|
|
|
645
650
|
// ── HOOK 1: WEEKEND LEISURE SURGE (event) ────────────
|
|
@@ -672,6 +677,33 @@ const config = {
|
|
|
672
677
|
const events = record;
|
|
673
678
|
if (!events.length) return record;
|
|
674
679
|
|
|
680
|
+
const profile = meta.profile;
|
|
681
|
+
|
|
682
|
+
// ─── Stamp superProps from profile (consistent per user) ───
|
|
683
|
+
const stampPlatform = profile && profile.Platform ? profile.Platform : undefined;
|
|
684
|
+
const stampTier = profile && profile.membership_tier ? profile.membership_tier : undefined;
|
|
685
|
+
if (stampPlatform || stampTier) {
|
|
686
|
+
events.forEach(e => {
|
|
687
|
+
if (stampPlatform) e.Platform = stampPlatform;
|
|
688
|
+
if (stampTier) e.membership_tier = stampTier;
|
|
689
|
+
});
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
// ─── Bug 2 fix: Repeat destination clustering conversion filtering ───
|
|
693
|
+
// Drop ~25% of "booking completed" events for users who are NOT
|
|
694
|
+
// business_traveler or luxury_seeker to simulate their lower funnel
|
|
695
|
+
// conversion (was conversionRate * 1.3 / 1.15 in funnel-pre)
|
|
696
|
+
const segment = profile && profile.customer_segment;
|
|
697
|
+
if (segment !== "business_traveler" && segment !== "luxury_seeker") {
|
|
698
|
+
for (let i = events.length - 1; i >= 0; i--) {
|
|
699
|
+
if (events[i].event === "booking completed") {
|
|
700
|
+
if (chance.bool({ likelihood: 25 })) {
|
|
701
|
+
events.splice(i, 1);
|
|
702
|
+
}
|
|
703
|
+
}
|
|
704
|
+
}
|
|
705
|
+
}
|
|
706
|
+
|
|
675
707
|
// ── HOOK 3: LOYALTY TIER UPGRADE PATH ────────────
|
|
676
708
|
let bookingCount = 0;
|
|
677
709
|
events.forEach(e => { if (e.event === "booking completed") bookingCount++; });
|
|
@@ -694,7 +726,6 @@ const config = {
|
|
|
694
726
|
}
|
|
695
727
|
|
|
696
728
|
// ── HOOK 5: UPSELL SUCCESS BY SEGMENT ────────────
|
|
697
|
-
const profile = meta.profile;
|
|
698
729
|
if (profile && profile.customer_segment === "luxury_seeker") {
|
|
699
730
|
const templateUpgrade = events.find(e => e.event === "room upgrade selected");
|
|
700
731
|
if (templateUpgrade) {
|
package/index.js
CHANGED
|
@@ -109,6 +109,9 @@ async function DUNGEON_MASTER(input, overrides = {}) {
|
|
|
109
109
|
const textConfig = await loadFromText(value);
|
|
110
110
|
return await runDungeon({ ...textConfig, ...overrides });
|
|
111
111
|
}
|
|
112
|
+
|
|
113
|
+
default:
|
|
114
|
+
throw new Error(`Unexpected input type: ${type}`);
|
|
112
115
|
}
|
|
113
116
|
}
|
|
114
117
|
|
|
@@ -24,7 +24,6 @@ const FIXED_NOW = dayjs('2024-02-02').unix();
|
|
|
24
24
|
function inferFunnels(events) {
|
|
25
25
|
const createdFunnels = [];
|
|
26
26
|
const firstEvents = events.filter((e) => e.isFirstEvent).map((e) => e.event);
|
|
27
|
-
const strictEvents = events.filter((e) => e.isStrictEvent).map((e) => e.event);
|
|
28
27
|
const usageEvents = events
|
|
29
28
|
.filter((e) => !e.isFirstEvent && !e.isStrictEvent)
|
|
30
29
|
.map((e) => e.event);
|
|
@@ -106,6 +105,7 @@ export function validateDungeonConfig(config) {
|
|
|
106
105
|
lookupTables = [],
|
|
107
106
|
hasAnonIds = false,
|
|
108
107
|
hasSessionIds = false,
|
|
108
|
+
sessionTimeout = 30,
|
|
109
109
|
format = "csv",
|
|
110
110
|
token = null,
|
|
111
111
|
region = "US",
|
|
@@ -140,6 +140,14 @@ export function validateDungeonConfig(config) {
|
|
|
140
140
|
concurrency = 1;
|
|
141
141
|
}
|
|
142
142
|
|
|
143
|
+
// Throw if token is the placeholder and nothing will be written to disk
|
|
144
|
+
if (token === "your-mixpanel-token" && !writeToDisk) {
|
|
145
|
+
throw new Error(
|
|
146
|
+
"No Mixpanel token set and writeToDisk is false — nothing useful will happen.\n" +
|
|
147
|
+
"Either set process.env.MP_TOKEN, change the token in the dungeon file, or set writeToDisk to true."
|
|
148
|
+
);
|
|
149
|
+
}
|
|
150
|
+
|
|
143
151
|
// Auto-enable batch mode for large datasets to prevent OOM
|
|
144
152
|
if (numEvents >= 2_000_000 && config.batchSize === undefined) {
|
|
145
153
|
batchSize = 1_000_000;
|
|
@@ -148,7 +156,9 @@ export function validateDungeonConfig(config) {
|
|
|
148
156
|
|
|
149
157
|
// Ensure defaults for deep objects
|
|
150
158
|
if (!config.superProps) config.superProps = superProps;
|
|
151
|
-
if (!config.userProps || Object.keys(config
|
|
159
|
+
if (!config.userProps || Object.keys(config.userProps).length === 0) {
|
|
160
|
+
userProps = { spiritAnimal: chance.animal.bind(chance) };
|
|
161
|
+
}
|
|
152
162
|
|
|
153
163
|
// Setting up "TIME"
|
|
154
164
|
if (epochStart && !numDays) numDays = dayjs.unix(epochEnd).diff(dayjs.unix(epochStart), "day");
|
|
@@ -356,6 +366,7 @@ export function validateDungeonConfig(config) {
|
|
|
356
366
|
lookupTables,
|
|
357
367
|
hasAnonIds,
|
|
358
368
|
hasSessionIds,
|
|
369
|
+
sessionTimeout: (typeof sessionTimeout === 'number' && sessionTimeout > 0) ? sessionTimeout : 30,
|
|
359
370
|
format,
|
|
360
371
|
token,
|
|
361
372
|
region,
|
package/lib/generators/events.js
CHANGED
|
@@ -116,14 +116,12 @@ export async function makeEvent(
|
|
|
116
116
|
eventTemplate.time = dayjs.unix(Math.min(shiftedTimestamp, context.MAX_TIME)).toISOString();
|
|
117
117
|
}
|
|
118
118
|
|
|
119
|
-
// Add anonymous
|
|
119
|
+
// Add anonymous identifiers
|
|
120
120
|
if (anonymousIds.length) {
|
|
121
121
|
eventTemplate.device_id = u.pickRandom(anonymousIds);
|
|
122
122
|
}
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
eventTemplate.session_id = u.pickRandom(sessionIds);
|
|
126
|
-
}
|
|
123
|
+
|
|
124
|
+
// Session IDs are assigned post-hoc in user-loop.js based on temporal gaps
|
|
127
125
|
|
|
128
126
|
// Sometimes add user_id (for attribution modeling)
|
|
129
127
|
if (!isFirstEvent && chance.bool({ likelihood: 42 })) {
|
|
@@ -48,9 +48,10 @@ export async function sendToMixpanel(context) {
|
|
|
48
48
|
const commonOpts = {
|
|
49
49
|
region,
|
|
50
50
|
fixData: true,
|
|
51
|
+
v2_compat: true,
|
|
51
52
|
verbose: false,
|
|
52
53
|
forceStream: true,
|
|
53
|
-
strict:
|
|
54
|
+
strict: false,
|
|
54
55
|
epochEnd: dayjs().unix(),
|
|
55
56
|
dryRun: false,
|
|
56
57
|
abridged: false,
|
|
@@ -159,7 +160,6 @@ export async function sendToMixpanel(context) {
|
|
|
159
160
|
const imported = await mp(creds, groupEventDataToImport, {
|
|
160
161
|
recordType: "event",
|
|
161
162
|
...commonOpts,
|
|
162
|
-
strict: false
|
|
163
163
|
});
|
|
164
164
|
log(` -> ${comma(imported.success)} group events sent\n`);
|
|
165
165
|
importResults.groupEvents = imported;
|
|
@@ -198,7 +198,6 @@ export async function sendToMixpanel(context) {
|
|
|
198
198
|
scdKey,
|
|
199
199
|
scdType,
|
|
200
200
|
scdLabel: `${scdKey}`,
|
|
201
|
-
fixData: true,
|
|
202
201
|
...commonOpts,
|
|
203
202
|
};
|
|
204
203
|
|
|
@@ -227,6 +226,9 @@ export async function sendToMixpanel(context) {
|
|
|
227
226
|
}
|
|
228
227
|
}
|
|
229
228
|
|
|
229
|
+
importResults.problems = collectProblems(importResults);
|
|
230
|
+
logProblems(importResults.problems);
|
|
231
|
+
|
|
230
232
|
log(`${'─'.repeat(50)}\n`);
|
|
231
233
|
|
|
232
234
|
// Clean up batch files if needed
|
|
@@ -263,4 +265,63 @@ export async function sendToMixpanel(context) {
|
|
|
263
265
|
let _verbose = true;
|
|
264
266
|
function log(message) {
|
|
265
267
|
if (_verbose) console.log(message);
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
/**
|
|
271
|
+
* Walk importResults and collect any failure/error indicators per import.
|
|
272
|
+
* Returns an array of problem objects (empty when all imports succeeded).
|
|
273
|
+
* @param {Object} importResults
|
|
274
|
+
* @returns {Array<{label: string, failed: number, unparsable: number, serverErrors: number, clientErrors: number, errorCount: number, error?: string, errors?: any[]}>}
|
|
275
|
+
*/
|
|
276
|
+
function collectProblems(importResults) {
|
|
277
|
+
const entries = [];
|
|
278
|
+
for (const [key, value] of Object.entries(importResults)) {
|
|
279
|
+
if (!value || key === 'problems') continue;
|
|
280
|
+
if (Array.isArray(value)) {
|
|
281
|
+
value.forEach((v, i) => entries.push([`${key}[${i}]`, v]));
|
|
282
|
+
} else {
|
|
283
|
+
entries.push([key, value]);
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
const problems = [];
|
|
288
|
+
for (const [label, result] of entries) {
|
|
289
|
+
if (!result || typeof result !== 'object') continue;
|
|
290
|
+
const failed = result.failed || 0;
|
|
291
|
+
const unparsable = result.unparsable || 0;
|
|
292
|
+
const serverErrors = result.serverErrors || 0;
|
|
293
|
+
const clientErrors = result.clientErrors || 0;
|
|
294
|
+
const errorCount = Array.isArray(result.errors) ? result.errors.length : 0;
|
|
295
|
+
const error = result.error;
|
|
296
|
+
if (failed || unparsable || serverErrors || clientErrors || errorCount || error) {
|
|
297
|
+
problems.push({ label, failed, unparsable, serverErrors, clientErrors, errorCount, error, errors: result.errors });
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
return problems;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
/**
|
|
304
|
+
* Print the compact problem report. Silent when there are no problems.
|
|
305
|
+
* @param {ReturnType<typeof collectProblems>} problems
|
|
306
|
+
*/
|
|
307
|
+
function logProblems(problems) {
|
|
308
|
+
if (!problems || problems.length === 0) return;
|
|
309
|
+
|
|
310
|
+
log(` Problems`);
|
|
311
|
+
for (const p of problems) {
|
|
312
|
+
const parts = [];
|
|
313
|
+
if (p.failed) parts.push(`${comma(p.failed)} failed`);
|
|
314
|
+
if (p.unparsable) parts.push(`${comma(p.unparsable)} unparsable`);
|
|
315
|
+
if (p.serverErrors) parts.push(`${comma(p.serverErrors)} 5xx (retried)`);
|
|
316
|
+
if (p.clientErrors) parts.push(`${comma(p.clientErrors)} client errors (retried)`);
|
|
317
|
+
if (p.errorCount) parts.push(`${comma(p.errorCount)} error records`);
|
|
318
|
+
if (p.error) parts.push(`error: ${p.error}`);
|
|
319
|
+
log(` -> ${p.label}: ${parts.join(', ')}`);
|
|
320
|
+
if (Array.isArray(p.errors) && p.errors.length > 0) {
|
|
321
|
+
const sample = p.errors[0];
|
|
322
|
+
const summary = typeof sample === 'string' ? sample : JSON.stringify(sample).slice(0, 200);
|
|
323
|
+
log(` sample: ${summary}`);
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
log('');
|
|
266
327
|
}
|
|
@@ -359,6 +359,8 @@ export async function userLoop(context) {
|
|
|
359
359
|
|
|
360
360
|
// ALL SUBSEQUENT EVENTS (funnels for converted users, standalone for all)
|
|
361
361
|
let userChurned = false;
|
|
362
|
+
const sessionTimeout = config.sessionTimeout || 30;
|
|
363
|
+
|
|
362
364
|
while (numEventsPreformed < numEventsThisUserWillPreform && !cancelled) {
|
|
363
365
|
let newEvents;
|
|
364
366
|
if (usageFunnels.length && userConverted) {
|
|
@@ -448,6 +450,24 @@ export async function userLoop(context) {
|
|
|
448
450
|
}
|
|
449
451
|
}
|
|
450
452
|
|
|
453
|
+
// Session clustering: redistribute events into temporal bursts, then assign session IDs
|
|
454
|
+
if (hasSessionIds && usersEvents.length > 0) {
|
|
455
|
+
const soupCfg = /** @type {import('../../types').SoupConfig} */ (config.soup) || {};
|
|
456
|
+
const defaultPeaks = Math.max(5, (config.numDays || 30) * 2);
|
|
457
|
+
const { mean: soupMean = 0, deviation: soupDev = 2, peaks: soupPeaks = defaultPeaks,
|
|
458
|
+
dayOfWeekWeights: soupDOW, hourOfDayWeights: soupHOD } = soupCfg;
|
|
459
|
+
|
|
460
|
+
u.bunchIntoSessions(usersEvents, sessionTimeout, {
|
|
461
|
+
earliestTime: userFirstEventTime,
|
|
462
|
+
latestTime: context.FIXED_NOW,
|
|
463
|
+
peaks: soupPeaks, deviation: soupDev, mean: soupMean,
|
|
464
|
+
dayOfWeekWeights: soupDOW, hourOfDayWeights: soupHOD,
|
|
465
|
+
timeShiftSeconds: context.TIME_SHIFT_SECONDS,
|
|
466
|
+
maxTime: context.MAX_TIME
|
|
467
|
+
});
|
|
468
|
+
u.assignSessionIds(usersEvents, sessionTimeout);
|
|
469
|
+
}
|
|
470
|
+
|
|
451
471
|
// Hook for processing all user events (hooks override everything)
|
|
452
472
|
if (config.hook) {
|
|
453
473
|
const newEvents = await config.hook(usersEvents, "everything", {
|