@ak--47/dungeon-master 1.5.0 → 1.5.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/.claude/skills/create-dungeon/SKILL.md +139 -46
- package/.claude/skills/verify-dungeon/references/counting-semantics.md +31 -6
- package/.claude/skills/verify-dungeon/references/sql-recipes.md +44 -25
- package/.claude/skills/write-hooks/SKILL.md +31 -3
- package/CHANGELOG.md +85 -0
- package/HOOKS.md +13 -0
- package/dungeons/technical/ad-spend.js +41 -49
- package/dungeons/technical/anonymous-users.js +38 -36
- package/dungeons/technical/array-of-object-lookup.js +136 -153
- package/dungeons/technical/datagen-v15-verify.js +24 -11
- package/dungeons/technical/experiments.js +42 -40
- package/dungeons/technical/foobar.js +114 -118
- package/dungeons/technical/group-analytics.js +42 -40
- package/dungeons/technical/hook-helpers-verify.js +69 -50
- package/dungeons/technical/identity-model-verify.js +22 -12
- package/dungeons/technical/mirror-strategies.js +37 -39
- package/dungeons/technical/nested-objects.js +119 -118
- package/dungeons/technical/pattern-aggregate-by-bin.js +21 -8
- package/dungeons/technical/pattern-attributed-by-source.js +23 -9
- package/dungeons/technical/pattern-frequency-by-frequency.js +21 -8
- package/dungeons/technical/pattern-funnel-frequency.js +30 -15
- package/dungeons/technical/pattern-ttc-by-segment.js +21 -8
- package/dungeons/technical/retention-cadence.js +115 -112
- package/dungeons/technical/sanity.js +86 -80
- package/dungeons/technical/scale-test.js +34 -38
- package/dungeons/technical/scd.js +111 -128
- package/dungeons/technical/simple.js +134 -141
- package/dungeons/technical/simplest.js +54 -62
- package/dungeons/technical/text-generation.js +110 -146
- package/dungeons/vertical/ai-platform.js +296 -333
- package/dungeons/vertical/community.js +284 -255
- package/dungeons/vertical/crypto.js +395 -391
- package/dungeons/vertical/dating.js +411 -378
- package/dungeons/vertical/devtools.js +336 -298
- package/dungeons/vertical/ecommerce.js +316 -394
- package/dungeons/vertical/education.js +369 -325
- package/dungeons/vertical/fintech.js +358 -325
- package/dungeons/vertical/fitness.js +335 -291
- package/dungeons/vertical/food-delivery.js +343 -307
- package/dungeons/vertical/gaming.js +480 -444
- package/dungeons/vertical/healthcare.js +306 -262
- package/dungeons/vertical/insurance-application.js +427 -409
- package/dungeons/vertical/logistics.js +271 -252
- package/dungeons/vertical/marketplace.js +333 -323
- package/dungeons/vertical/media.js +382 -335
- package/dungeons/vertical/real-estate.js +395 -346
- package/dungeons/vertical/sass.js +319 -333
- package/dungeons/vertical/social.js +368 -316
- package/dungeons/vertical/travel.js +297 -295
- package/index.js +46 -4
- package/lib/core/config-validator.js +126 -28
- package/lib/generators/funnels.js +4 -1
- package/lib/orchestrators/mixpanel-sender.js +7 -0
- package/lib/orchestrators/user-loop.js +132 -31
- package/lib/templates/defaults.js +59 -59
- package/lib/templates/macro-presets.js +14 -2
- package/lib/utils/dataset-context.js +103 -0
- package/lib/utils/retention-curve.js +140 -0
- package/lib/utils/utils.js +149 -38
- package/lib/verify/counting.js +40 -0
- package/lib/verify/emulate-breakdown.js +20 -1
- package/lib/verify/index.js +1 -0
- package/lib/verify/schema-validator.js +3 -1
- package/package.json +11 -2
- package/scripts/run-dungeon.mjs +12 -1
- package/types.d.ts +117 -1
|
@@ -1,59 +1,51 @@
|
|
|
1
|
-
// ──
|
|
2
|
-
|
|
3
|
-
const num_days = 90;
|
|
4
|
-
const num_users = 1_000;
|
|
5
|
-
const avg_events_per_user_per_day = 0.56;
|
|
6
|
-
let token = "";
|
|
7
|
-
|
|
8
|
-
// ── env overrides ──
|
|
9
|
-
if (process.env.MP_TOKEN) token = process.env.MP_TOKEN;
|
|
10
|
-
|
|
11
|
-
import Chance from 'chance';
|
|
12
|
-
let chance = new Chance();
|
|
13
|
-
import dayjs from "dayjs";
|
|
14
|
-
import utc from "dayjs/plugin/utc.js";
|
|
15
|
-
dayjs.extend(utc);
|
|
16
|
-
import { uid, comma } from 'ak-tools';
|
|
17
|
-
import { weighNumRange, date, integer, weighChoices } from "../../lib/utils/utils.js";
|
|
18
|
-
|
|
1
|
+
// ── IMPORTS ──
|
|
2
|
+
import { weighNumRange } from "../../lib/utils/utils.js";
|
|
19
3
|
/** @typedef {import("../../types").Dungeon} Config */
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
* - hasCampaigns: true — adds UTM parameters to events
|
|
29
|
-
* - hasBrowser: true — required for campaign tracking
|
|
30
|
-
* - 6 events: signup, page view, purchase, ad click, search, share
|
|
31
|
-
*
|
|
32
|
-
* No hooks. Focus is on verifying ad spend event structure
|
|
33
|
-
* and campaign attribution properties flow through correctly.
|
|
4
|
+
|
|
5
|
+
// ── OVERVIEW ──
|
|
6
|
+
/*
|
|
7
|
+
* NAME: ad-spend
|
|
8
|
+
* PURPOSE: exercises hasAdSpend + hasCampaigns — generates cost/CPC/CTR/impressions/clicks + UTM attribution
|
|
9
|
+
* SCALE: 1,000 users, ~50K events, 90 days
|
|
10
|
+
* EVENTS (6): sign up, page view, purchase, ad click, search, share
|
|
11
|
+
* FUNNELS (0): none
|
|
34
12
|
*/
|
|
35
13
|
|
|
36
|
-
|
|
14
|
+
// ── SCALE ──
|
|
15
|
+
const SEED = "ad spend test";
|
|
16
|
+
const NUM_DAYS = 90;
|
|
17
|
+
const NUM_USERS = 1_000;
|
|
18
|
+
const EVENTS_PER_DAY = 0.56;
|
|
19
|
+
const token = process.env.MP_TOKEN || "";
|
|
20
|
+
|
|
21
|
+
// ── CONFIG ──
|
|
22
|
+
/** @type {Config} */
|
|
37
23
|
const config = {
|
|
38
|
-
token,
|
|
39
24
|
seed: SEED,
|
|
40
25
|
name: "ad-spend",
|
|
41
|
-
numDays:
|
|
42
|
-
avgEventsPerUserPerDay:
|
|
43
|
-
numUsers:
|
|
26
|
+
numDays: NUM_DAYS,
|
|
27
|
+
avgEventsPerUserPerDay: EVENTS_PER_DAY,
|
|
28
|
+
numUsers: NUM_USERS,
|
|
44
29
|
format: 'json',
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
30
|
+
credentials: {
|
|
31
|
+
token,
|
|
32
|
+
region: "US",
|
|
33
|
+
},
|
|
34
|
+
switches: {
|
|
35
|
+
hasSessionIds: true,
|
|
36
|
+
hasAdSpend: true,
|
|
37
|
+
hasLocation: false,
|
|
38
|
+
hasAndroidDevices: false,
|
|
39
|
+
hasIOSDevices: false,
|
|
40
|
+
hasDesktopDevices: true,
|
|
41
|
+
hasBrowser: true,
|
|
42
|
+
hasCampaigns: true,
|
|
43
|
+
isAnonymous: false,
|
|
44
|
+
alsoInferFunnels: false,
|
|
45
|
+
},
|
|
46
|
+
identity: {
|
|
47
|
+
avgDevicePerUser: 1,
|
|
48
|
+
},
|
|
57
49
|
concurrency: 1,
|
|
58
50
|
writeToDisk: false,
|
|
59
51
|
|
|
@@ -1,50 +1,52 @@
|
|
|
1
|
-
// ──
|
|
1
|
+
// ── IMPORTS ──
|
|
2
|
+
import Chance from 'chance';
|
|
3
|
+
let chance = new Chance();
|
|
4
|
+
import { weighNumRange, weighChoices } from "../../lib/utils/utils.js";
|
|
5
|
+
/** @typedef {import("../../types").Dungeon} Config */
|
|
6
|
+
|
|
7
|
+
// ── OVERVIEW ──
|
|
8
|
+
/*
|
|
9
|
+
* NAME: anonymous-users
|
|
10
|
+
* PURPOSE: Anonymous-user mode fixture — exercises isAnonymous + hasAnonIds + anon-to-identified flow
|
|
11
|
+
* SCALE: 1,000 users, ~50K events, 180 days
|
|
12
|
+
* EVENTS (5): page view, signup, feature used, purchase, button click
|
|
13
|
+
* FUNNELS (0): none
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
// ── SCALE ──
|
|
2
17
|
const SEED = "anonymous-users";
|
|
3
18
|
const num_days = 180;
|
|
4
19
|
const num_users = 1_000;
|
|
5
20
|
const avg_events_per_user_per_day = 0.28;
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
// ── env overrides ──
|
|
9
|
-
if (process.env.MP_TOKEN) token = process.env.MP_TOKEN;
|
|
10
|
-
|
|
11
|
-
/**
|
|
12
|
-
* Anonymous Users — tests anonymous user mode, anonIds, and identity merge.
|
|
13
|
-
*
|
|
14
|
-
* Exercises: isAnonymous mode, hasAnonIds for anonymous-to-identified
|
|
15
|
-
* user flows, long 180-day timespan for tail behavior.
|
|
16
|
-
*
|
|
17
|
-
* - 1000 users, 50K events, 180 days
|
|
18
|
-
* - isAnonymous: true, hasAnonIds: true
|
|
19
|
-
* - Simple events with signup as isFirstEvent (identity resolution point)
|
|
20
|
-
* - No hooks, minimal config
|
|
21
|
-
*/
|
|
22
|
-
|
|
23
|
-
import Chance from 'chance';
|
|
24
|
-
let chance = new Chance();
|
|
25
|
-
import { weighNumRange, weighChoices } from "../../lib/utils/utils.js";
|
|
21
|
+
const token = process.env.MP_TOKEN || "";
|
|
26
22
|
|
|
27
|
-
|
|
28
|
-
/** @type {
|
|
23
|
+
// ── CONFIG ──
|
|
24
|
+
/** @type {Config} */
|
|
29
25
|
const config = {
|
|
30
|
-
token,
|
|
31
26
|
seed: SEED,
|
|
32
27
|
numDays: num_days,
|
|
33
28
|
avgEventsPerUserPerDay: avg_events_per_user_per_day,
|
|
34
29
|
numUsers: num_users,
|
|
35
30
|
format: "json",
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
31
|
+
credentials: {
|
|
32
|
+
token,
|
|
33
|
+
region: "US",
|
|
34
|
+
},
|
|
35
|
+
switches: {
|
|
36
|
+
isAnonymous: true,
|
|
37
|
+
hasSessionIds: true,
|
|
38
|
+
hasAdSpend: false,
|
|
39
|
+
hasLocation: false,
|
|
40
|
+
hasAndroidDevices: true,
|
|
41
|
+
hasIOSDevices: true,
|
|
42
|
+
hasDesktopDevices: true,
|
|
43
|
+
hasBrowser: true,
|
|
44
|
+
hasCampaigns: false,
|
|
45
|
+
alsoInferFunnels: false,
|
|
46
|
+
},
|
|
47
|
+
identity: {
|
|
48
|
+
avgDevicePerUser: 1,
|
|
49
|
+
},
|
|
48
50
|
concurrency: 1,
|
|
49
51
|
writeToDisk: false,
|
|
50
52
|
|
|
@@ -1,40 +1,24 @@
|
|
|
1
|
-
// ──
|
|
2
|
-
|
|
3
|
-
const num_days = 60;
|
|
4
|
-
const num_users = 1_000;
|
|
5
|
-
const avg_events_per_user_per_day = 1.67;
|
|
6
|
-
let token = "";
|
|
7
|
-
|
|
8
|
-
// ── env overrides ──
|
|
9
|
-
if (process.env.MP_TOKEN) token = process.env.MP_TOKEN;
|
|
10
|
-
|
|
11
|
-
import Chance from 'chance';
|
|
12
|
-
let chance = new Chance();
|
|
1
|
+
// ── IMPORTS ──
|
|
2
|
+
import Chance from "chance";
|
|
13
3
|
import dayjs from "dayjs";
|
|
14
4
|
import utc from "dayjs/plugin/utc.js";
|
|
15
5
|
dayjs.extend(utc);
|
|
16
|
-
import {
|
|
17
|
-
import { weighNumRange, date, integer, weighChoices } from "../../lib/utils/utils.js";
|
|
18
|
-
|
|
6
|
+
import { weighNumRange, integer, weighChoices } from "../../lib/utils/utils.js";
|
|
19
7
|
/** @typedef {import("../../types").Dungeon} Config */
|
|
20
|
-
const videoCategories = ["funny", "educational", "inspirational", "music", "news", "sports", "cooking", "DIY", "travel", "gaming"];
|
|
21
|
-
const spiritAnimals = ["duck", "dog", "otter", "penguin", "cat", "elephant", "lion", "cheetah", "giraffe", "zebra", "rhino", "hippo", "whale", "dolphin", "shark", "octopus", "squid", "jellyfish", "starfish", "seahorse", "crab", "lobster", "shrimp", "clam", "snail", "slug", "butterfly", "moth", "bee", "wasp", "ant", "beetle", "ladybug", "caterpillar", "centipede", "millipede", "scorpion", "spider", "tarantula", "tick", "mite", "mosquito", "fly", "dragonfly", "damselfly", "grasshopper", "cricket", "locust", "mantis", "cockroach", "termite", "praying mantis", "walking stick", "stick bug", "leaf insect", "lacewing", "aphid", "cicada", "thrips", "psyllid", "scale insect", "whitefly", "mealybug", "planthopper", "leafhopper", "treehopper", "flea", "louse", "bedbug", "flea beetle", "weevil", "longhorn beetle", "leaf beetle", "tiger beetle", "ground beetle", "lady beetle", "firefly", "click beetle", "rove beetle", "scarab beetle", "dung beetle", "stag beetle", "rhinoceros beetle", "hercules beetle", "goliath beetle", "jewel beetle", "tortoise beetle"];
|
|
22
8
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
*
|
|
31
|
-
*
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
* ═══════════════════════════════════════════════════════════════
|
|
37
|
-
*
|
|
9
|
+
// ── OVERVIEW ──
|
|
10
|
+
/*
|
|
11
|
+
* NAME: array-of-object-lookup
|
|
12
|
+
* PURPOSE: Exercises nested product arrays in events plus a lookup table
|
|
13
|
+
* keyed by product_id with rich attributes.
|
|
14
|
+
* SCALE: 1,000 users, ~100K events, 60 days
|
|
15
|
+
* EVENTS (4): checkout (cart array), add to cart, view item, save item (single item)
|
|
16
|
+
* FUNNELS (0): none
|
|
17
|
+
* LOOKUPS (1): product_id (1,000 entries; amount, quantity, featured, category, descriptor)
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
// ── HOOK STORIES ──
|
|
21
|
+
/*
|
|
38
22
|
* 1. COUPON DISCOUNT TAGGING (event hook)
|
|
39
23
|
* Checkout events with coupons get discount_applied: true and
|
|
40
24
|
* discount_percent extracted from the coupon string.
|
|
@@ -48,27 +32,124 @@ const spiritAnimals = ["duck", "dog", "otter", "penguin", "cat", "elephant", "li
|
|
|
48
32
|
* events tagged user_segment: "window_shopper".
|
|
49
33
|
*/
|
|
50
34
|
|
|
51
|
-
|
|
35
|
+
// ── SCALE ──
|
|
36
|
+
const SEED = "dm4-array-of-object-lookup";
|
|
37
|
+
const NUM_DAYS = 60;
|
|
38
|
+
const NUM_USERS = 1_000;
|
|
39
|
+
const EVENTS_PER_DAY = 1.67;
|
|
40
|
+
const token = process.env.MP_TOKEN || "";
|
|
41
|
+
|
|
42
|
+
const chance = new Chance();
|
|
43
|
+
|
|
44
|
+
// ── DATA ARRAYS ──
|
|
45
|
+
const spiritAnimals = ["duck", "dog", "otter", "penguin", "cat", "elephant", "lion", "cheetah", "giraffe", "zebra", "rhino", "hippo", "whale", "dolphin", "shark", "octopus", "squid", "jellyfish", "starfish", "seahorse", "crab", "lobster", "shrimp", "clam", "snail", "slug", "butterfly", "moth", "bee", "wasp", "ant", "beetle", "ladybug", "caterpillar", "centipede", "millipede", "scorpion", "spider", "tarantula", "tick", "mite", "mosquito", "fly", "dragonfly", "damselfly", "grasshopper", "cricket", "locust", "mantis", "cockroach", "termite", "praying mantis", "walking stick", "stick bug", "leaf insect", "lacewing", "aphid", "cicada", "thrips", "psyllid", "scale insect", "whitefly", "mealybug", "planthopper", "leafhopper", "treehopper", "flea", "louse", "bedbug", "flea beetle", "weevil", "longhorn beetle", "leaf beetle", "tiger beetle", "ground beetle", "lady beetle", "firefly", "click beetle", "rove beetle", "scarab beetle", "dung beetle", "stag beetle", "rhinoceros beetle", "hercules beetle", "goliath beetle", "jewel beetle", "tortoise beetle"];
|
|
46
|
+
|
|
47
|
+
// ── HELPER FUNCTIONS ──
|
|
48
|
+
function makeProducts(maxItems = 5) {
|
|
49
|
+
return function () {
|
|
50
|
+
const categories = ["electronics", "books", "clothing", "home", "garden", "toys", "sports", "automotive", "beauty", "health", "grocery", "jewelry", "shoes", "tools", "office supplies"];
|
|
51
|
+
const descriptors = ["brand new", "open box", "refurbished", "used", "like new", "vintage", "antique", "collectible"];
|
|
52
|
+
const suffix = ["item", "product", "good", "merchandise", "thing", "object", "widget", "gadget", "device", "apparatus", "contraption", "instrument", "tool", "implement", "utensil", "appliance", "machine", "equipment", "gear", "kit", "set", "package"];
|
|
53
|
+
const assetPreview = ['.png', '.jpg', '.jpeg', '.heic', '.mp4', '.mov', '.avi'];
|
|
54
|
+
const data = [];
|
|
55
|
+
const numOfItems = integer(1, maxItems);
|
|
56
|
+
|
|
57
|
+
for (var i = 0; i < numOfItems; i++) {
|
|
58
|
+
const category = chance.pickone(categories);
|
|
59
|
+
const descriptor = chance.pickone(descriptors);
|
|
60
|
+
const suffixWord = chance.pickone(suffix);
|
|
61
|
+
const slug = `${descriptor.replace(/\s+/g, '-').toLowerCase()}-${suffixWord.replace(/\s+/g, '-').toLowerCase()}`;
|
|
62
|
+
const asset = chance.pickone(assetPreview);
|
|
63
|
+
|
|
64
|
+
const price = integer(1, 100);
|
|
65
|
+
const quantity = integer(1, 5);
|
|
66
|
+
const product_id = integer(1, 1_000);
|
|
67
|
+
|
|
68
|
+
const item = {
|
|
69
|
+
product_id: product_id,
|
|
70
|
+
product_url: `https://example.com/assets/${product_id}`,
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
data.push(item);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
return () => [data];
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function flip(likelihood = 50) {
|
|
81
|
+
return chance.bool({ likelihood });
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function handleEventHooks(record) {
|
|
85
|
+
// Pattern 1: Checkouts with coupons get a discount_applied flag and adjusted total
|
|
86
|
+
if (record.event === "checkout" && record.coupon && record.coupon !== "none") {
|
|
87
|
+
record.discount_applied = true;
|
|
88
|
+
const pctMatch = record.coupon.match(/(\d+)%/);
|
|
89
|
+
if (pctMatch) {
|
|
90
|
+
record.discount_percent = parseInt(pctMatch[1]);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// Pattern 2: "save item" events on weekends are tagged as wishlist behavior
|
|
95
|
+
if (record.event === "save item") {
|
|
96
|
+
const dow = dayjs(record.time).day();
|
|
97
|
+
if (dow === 0 || dow === 6) {
|
|
98
|
+
record.save_context = "weekend_browse";
|
|
99
|
+
} else {
|
|
100
|
+
record.save_context = "weekday_intent";
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
return record;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function handleEverythingHooks(record, meta) {
|
|
108
|
+
const profile = meta.profile;
|
|
109
|
+
record.forEach(e => {
|
|
110
|
+
e.theme = profile.theme;
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
// Pattern 3: Users who view 5+ items but never checkout are tagged as window shoppers
|
|
114
|
+
const views = record.filter(e => e.event === "view item").length;
|
|
115
|
+
const checkouts = record.filter(e => e.event === "checkout").length;
|
|
116
|
+
if (views >= 5 && checkouts === 0) {
|
|
117
|
+
for (const e of record) {
|
|
118
|
+
e.user_segment = "window_shopper";
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
return record;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// ── CONFIG ──
|
|
126
|
+
/** @type {Config} */
|
|
52
127
|
const config = {
|
|
53
|
-
token,
|
|
54
128
|
seed: SEED,
|
|
55
129
|
name: "array-of-object-lookup",
|
|
56
|
-
numDays:
|
|
57
|
-
avgEventsPerUserPerDay:
|
|
58
|
-
numUsers:
|
|
59
|
-
format: 'json',
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
130
|
+
numDays: NUM_DAYS,
|
|
131
|
+
avgEventsPerUserPerDay: EVENTS_PER_DAY,
|
|
132
|
+
numUsers: NUM_USERS,
|
|
133
|
+
format: 'json',
|
|
134
|
+
credentials: {
|
|
135
|
+
token,
|
|
136
|
+
region: "US",
|
|
137
|
+
},
|
|
138
|
+
switches: {
|
|
139
|
+
hasSessionIds: true,
|
|
140
|
+
hasAdSpend: false,
|
|
141
|
+
hasLocation: true,
|
|
142
|
+
hasAndroidDevices: false,
|
|
143
|
+
hasIOSDevices: false,
|
|
144
|
+
hasDesktopDevices: true,
|
|
145
|
+
hasBrowser: true,
|
|
146
|
+
hasCampaigns: false,
|
|
147
|
+
isAnonymous: false,
|
|
148
|
+
alsoInferFunnels: true,
|
|
149
|
+
},
|
|
150
|
+
identity: {
|
|
151
|
+
avgDevicePerUser: 1,
|
|
152
|
+
},
|
|
72
153
|
concurrency: 1,
|
|
73
154
|
writeToDisk: false,
|
|
74
155
|
events: [
|
|
@@ -108,28 +189,18 @@ const config = {
|
|
|
108
189
|
save_context: ["weekday_intent"],
|
|
109
190
|
user_segment: ["regular"],
|
|
110
191
|
}
|
|
111
|
-
}
|
|
192
|
+
}
|
|
112
193
|
],
|
|
113
|
-
funnels: [
|
|
194
|
+
funnels: [],
|
|
114
195
|
superProps: {
|
|
115
196
|
theme: ["light", "dark", "custom", "light", "dark"],
|
|
116
197
|
},
|
|
117
|
-
/*
|
|
118
|
-
user properties work the same as event properties
|
|
119
|
-
each key should be an array or function reference
|
|
120
|
-
*/
|
|
121
198
|
userProps: {
|
|
122
199
|
theme: ["light", "dark", "custom", "light", "dark"],
|
|
123
|
-
// title: chance.profession.bind(chance),
|
|
124
|
-
// luckyNumber: weighNumRange(1, 500, .3),
|
|
125
200
|
spiritAnimal: spiritAnimals
|
|
126
201
|
},
|
|
127
202
|
scdProps: {},
|
|
128
203
|
mirrorProps: {},
|
|
129
|
-
/*
|
|
130
|
-
for group analytics keys, we need an array of arrays [[],[],[]]
|
|
131
|
-
each pair represents a group_key and the number of profiles for that key
|
|
132
|
-
*/
|
|
133
204
|
groupKeys: [],
|
|
134
205
|
groupProps: {},
|
|
135
206
|
lookupTables: [{
|
|
@@ -141,101 +212,13 @@ const config = {
|
|
|
141
212
|
featured: flip,
|
|
142
213
|
category: ["electronics", "books", "clothing", "home", "garden", "toys", "sports", "automotive", "beauty", "health", "grocery", "jewelry", "shoes", "tools", "office supplies"],
|
|
143
214
|
descriptor: ["brand new", "open box", "refurbished", "used", "like new", "vintage", "antique", "collectible"]
|
|
144
|
-
|
|
145
215
|
}
|
|
146
|
-
|
|
147
216
|
}],
|
|
148
217
|
hook: function (record, type, meta) {
|
|
149
|
-
|
|
150
|
-
if (type === "
|
|
151
|
-
// Pattern 1: Checkouts with coupons get a discount_applied flag and adjusted total
|
|
152
|
-
if (record.event === "checkout" && record.coupon && record.coupon !== "none") {
|
|
153
|
-
record.discount_applied = true;
|
|
154
|
-
const pctMatch = record.coupon.match(/(\d+)%/);
|
|
155
|
-
if (pctMatch) {
|
|
156
|
-
record.discount_percent = parseInt(pctMatch[1]);
|
|
157
|
-
}
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
// Pattern 2: "save item" events on weekends are tagged as wishlist behavior
|
|
161
|
-
if (record.event === "save item") {
|
|
162
|
-
const dow = dayjs(record.time).day();
|
|
163
|
-
if (dow === 0 || dow === 6) {
|
|
164
|
-
record.save_context = "weekend_browse";
|
|
165
|
-
} else {
|
|
166
|
-
record.save_context = "weekday_intent";
|
|
167
|
-
}
|
|
168
|
-
}
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
if (type === "everything") {
|
|
172
|
-
const profile = meta.profile;
|
|
173
|
-
record.forEach(e => {
|
|
174
|
-
e.theme = profile.theme;
|
|
175
|
-
});
|
|
176
|
-
|
|
177
|
-
// Pattern 3: Users who view 5+ items but never checkout are tagged as window shoppers
|
|
178
|
-
const views = record.filter(e => e.event === "view item").length;
|
|
179
|
-
const checkouts = record.filter(e => e.event === "checkout").length;
|
|
180
|
-
if (views >= 5 && checkouts === 0) {
|
|
181
|
-
for (const e of record) {
|
|
182
|
-
e.user_segment = "window_shopper";
|
|
183
|
-
}
|
|
184
|
-
}
|
|
185
|
-
}
|
|
186
|
-
|
|
218
|
+
if (type === "event") return handleEventHooks(record);
|
|
219
|
+
if (type === "everything") return handleEverythingHooks(record, meta);
|
|
187
220
|
return record;
|
|
188
221
|
}
|
|
189
222
|
};
|
|
190
223
|
|
|
191
|
-
|
|
192
|
-
return function () {
|
|
193
|
-
const categories = ["electronics", "books", "clothing", "home", "garden", "toys", "sports", "automotive", "beauty", "health", "grocery", "jewelry", "shoes", "tools", "office supplies"];
|
|
194
|
-
const descriptors = ["brand new", "open box", "refurbished", "used", "like new", "vintage", "antique", "collectible"];
|
|
195
|
-
const suffix = ["item", "product", "good", "merchandise", "thing", "object", "widget", "gadget", "device", "apparatus", "contraption", "instrument", "tool", "implement", "utensil", "appliance", "machine", "equipment", "gear", "kit", "set", "package"];
|
|
196
|
-
const assetPreview = ['.png', '.jpg', '.jpeg', '.heic', '.mp4', '.mov', '.avi'];
|
|
197
|
-
const data = [];
|
|
198
|
-
const numOfItems = integer(1, maxItems);
|
|
199
|
-
|
|
200
|
-
for (var i = 0; i < numOfItems; i++) {
|
|
201
|
-
const category = chance.pickone(categories);
|
|
202
|
-
const descriptor = chance.pickone(descriptors);
|
|
203
|
-
const suffixWord = chance.pickone(suffix);
|
|
204
|
-
const slug = `${descriptor.replace(/\s+/g, '-').toLowerCase()}-${suffixWord.replace(/\s+/g, '-').toLowerCase()}`;
|
|
205
|
-
const asset = chance.pickone(assetPreview);
|
|
206
|
-
|
|
207
|
-
// const product_id = chance.guid();
|
|
208
|
-
const price = integer(1, 100);
|
|
209
|
-
const quantity = integer(1, 5);
|
|
210
|
-
const product_id = integer(1, 1_000);
|
|
211
|
-
|
|
212
|
-
const item = {
|
|
213
|
-
product_id: product_id,
|
|
214
|
-
product_url: `https://example.com/assets/${product_id}`,
|
|
215
|
-
// sku: integer(11111, 99999),
|
|
216
|
-
// amount: price,
|
|
217
|
-
// quantity: quantity,
|
|
218
|
-
// total_value: price * quantity,
|
|
219
|
-
// featured: chance.pickone([true, false, false]),
|
|
220
|
-
// category: category,
|
|
221
|
-
// descriptor: descriptor,
|
|
222
|
-
// slug: slug,
|
|
223
|
-
|
|
224
|
-
// assetType: asset
|
|
225
|
-
|
|
226
|
-
};
|
|
227
|
-
|
|
228
|
-
data.push(item);
|
|
229
|
-
}
|
|
230
|
-
|
|
231
|
-
return () => [data];
|
|
232
|
-
};
|
|
233
|
-
};
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
function flip(likelihood = 50) {
|
|
237
|
-
return chance.bool({ likelihood });
|
|
238
|
-
}
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
export default config;
|
|
224
|
+
export default config;
|
|
@@ -1,5 +1,14 @@
|
|
|
1
|
-
|
|
2
|
-
*
|
|
1
|
+
// ── IMPORTS ──
|
|
2
|
+
import * as u from "../../lib/utils/utils.js";
|
|
3
|
+
/** @typedef {import("../../types").Dungeon} Config */
|
|
4
|
+
|
|
5
|
+
// ── OVERVIEW ──
|
|
6
|
+
/*
|
|
7
|
+
* NAME: datagen-v1.5-verify
|
|
8
|
+
* PURPOSE: Canonical v1.5 fixture — active-days, conversion window, touchpoint cap, auto-sort, determinism
|
|
9
|
+
* SCALE: 500 users, ~60K events, 30 days
|
|
10
|
+
* EVENTS (4): page view, click, sign up, purchase
|
|
11
|
+
* FUNNELS (1): page view → sign up → purchase (50%, 14d window)
|
|
3
12
|
*
|
|
4
13
|
* Used by:
|
|
5
14
|
* - tests/active-days.test.js → asserts distinct-day distribution shape
|
|
@@ -8,21 +17,21 @@
|
|
|
8
17
|
* - tests/datagen-determinism.test.js → byte-equal verification across runs
|
|
9
18
|
* - tests/auto-sort.test.js → relies on sorted output
|
|
10
19
|
*
|
|
11
|
-
* Realistic small-scale config
|
|
12
|
-
* `
|
|
13
|
-
*
|
|
20
|
+
* Realistic small-scale config with `hasCampaigns: true` + an explicit
|
|
21
|
+
* `isAttributionEvent` flagged event so the touchpoint cap has eligible
|
|
22
|
+
* candidates beyond the default ~25% legacy fallback.
|
|
14
23
|
*
|
|
15
24
|
* **DO NOT enable `engagementDecay`** — it interacts with `avgActiveDaysPerUser`
|
|
16
25
|
* by dropping events on late picked days, eroding the effective active-day count
|
|
17
26
|
* below the configured target. See HOOKS.md §2.5.
|
|
18
27
|
*/
|
|
19
28
|
|
|
20
|
-
|
|
21
|
-
|
|
29
|
+
// ── SCALE ──
|
|
22
30
|
const SEED = "datagen-v1.5-verify";
|
|
23
31
|
u.initChance(SEED);
|
|
24
32
|
|
|
25
|
-
|
|
33
|
+
// ── CONFIG ──
|
|
34
|
+
/** @type {Config} */
|
|
26
35
|
const config = {
|
|
27
36
|
seed: SEED,
|
|
28
37
|
// Pin the dataset window for full determinism (independent of run date).
|
|
@@ -34,9 +43,13 @@ const config = {
|
|
|
34
43
|
avgActiveDaysPerUser: 6,
|
|
35
44
|
// v1.5 attribution cap — explicit (default is also 10).
|
|
36
45
|
maxTouchpointsPerUser: 10,
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
46
|
+
switches: {
|
|
47
|
+
hasCampaigns: true,
|
|
48
|
+
hasSessionIds: true,
|
|
49
|
+
},
|
|
50
|
+
identity: {
|
|
51
|
+
avgDevicePerUser: 1,
|
|
52
|
+
},
|
|
40
53
|
autoSortAfterEverything: true,
|
|
41
54
|
events: [
|
|
42
55
|
{ event: "page view", weight: 5, isAttributionEvent: true },
|