@ak--47/dungeon-master 1.5.0 → 1.5.1
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/CHANGELOG.md +43 -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,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 },
|
|
@@ -1,50 +1,52 @@
|
|
|
1
|
-
// ──
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
const avg_events_per_user_per_day = 0.83;
|
|
6
|
-
let token = "";
|
|
7
|
-
|
|
8
|
-
// ── env overrides ──
|
|
9
|
-
if (process.env.MP_TOKEN) token = process.env.MP_TOKEN;
|
|
1
|
+
// ── IMPORTS ──
|
|
2
|
+
import Chance from "chance";
|
|
3
|
+
import { weighNumRange, weighChoices } from "../../lib/utils/utils.js";
|
|
4
|
+
/** @typedef {import("../../types").Dungeon} Config */
|
|
10
5
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
*
|
|
14
|
-
* Exercises
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
* - No hooks
|
|
6
|
+
// ── OVERVIEW ──
|
|
7
|
+
/*
|
|
8
|
+
* NAME: experiments
|
|
9
|
+
* PURPOSE: Exercises funnel experiments (A/B/C variants), bindPropsIndex,
|
|
10
|
+
* and multiple funnel ordering modes (sequential, first-and-last-fixed, random).
|
|
11
|
+
* SCALE: 2,000 users, ~100K events, 60 days
|
|
12
|
+
* EVENTS (7): page view, signup, feature viewed, action taken, checkout, button click, help viewed
|
|
13
|
+
* FUNNELS (4): Onboarding Flow (sequential, experiment), Purchase Funnel (first-and-last-fixed, experiment),
|
|
14
|
+
* Feature Adoption (random, experiment, bindPropsIndex), Quick Signup (sequential, control)
|
|
21
15
|
*/
|
|
22
16
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
17
|
+
// ── SCALE ──
|
|
18
|
+
const SEED = "experiments";
|
|
19
|
+
const NUM_DAYS = 60;
|
|
20
|
+
const NUM_USERS = 2_000;
|
|
21
|
+
const EVENTS_PER_DAY = 0.83;
|
|
22
|
+
const token = process.env.MP_TOKEN || "";
|
|
26
23
|
|
|
27
|
-
|
|
28
|
-
|
|
24
|
+
const chance = new Chance();
|
|
25
|
+
|
|
26
|
+
// ── CONFIG ──
|
|
27
|
+
/** @type {Config} */
|
|
29
28
|
const config = {
|
|
30
|
-
token,
|
|
31
29
|
seed: SEED,
|
|
32
|
-
numDays:
|
|
33
|
-
avgEventsPerUserPerDay:
|
|
34
|
-
numUsers:
|
|
30
|
+
numDays: NUM_DAYS,
|
|
31
|
+
avgEventsPerUserPerDay: EVENTS_PER_DAY,
|
|
32
|
+
numUsers: NUM_USERS,
|
|
35
33
|
format: "json",
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
34
|
+
credentials: {
|
|
35
|
+
token,
|
|
36
|
+
region: "US",
|
|
37
|
+
},
|
|
38
|
+
switches: {
|
|
39
|
+
hasSessionIds: true,
|
|
40
|
+
hasAdSpend: false,
|
|
41
|
+
hasLocation: false,
|
|
42
|
+
hasAndroidDevices: true,
|
|
43
|
+
hasIOSDevices: true,
|
|
44
|
+
hasDesktopDevices: true,
|
|
45
|
+
hasBrowser: true,
|
|
46
|
+
hasCampaigns: false,
|
|
47
|
+
isAnonymous: false,
|
|
48
|
+
alsoInferFunnels: false,
|
|
49
|
+
},
|
|
48
50
|
concurrency: 1,
|
|
49
51
|
writeToDisk: false,
|
|
50
52
|
|
|
@@ -1,63 +1,39 @@
|
|
|
1
|
-
// ──
|
|
2
|
-
|
|
3
|
-
const num_days = 30;
|
|
4
|
-
const num_users = 4_000_000;
|
|
5
|
-
const avg_events_per_user_per_day = 16.67;
|
|
6
|
-
let token = "";
|
|
1
|
+
// ── IMPORTS ──
|
|
2
|
+
/** @typedef {import("../../types").Dungeon} Config */
|
|
7
3
|
|
|
8
|
-
// ──
|
|
9
|
-
|
|
4
|
+
// ── OVERVIEW ──
|
|
5
|
+
/*
|
|
6
|
+
* NAME: foobar
|
|
7
|
+
* PURPOSE: minimal stress/sanity dungeon — abstract event names, weight-driven distribution, no funnels, no event properties
|
|
8
|
+
* SCALE: 4,000,000 users, ~2B events, 30 days (configurable for stress testing)
|
|
9
|
+
* EVENTS (10): foo, bar, baz, qux, garply, durtle, linny, fonk, crumn, yak
|
|
10
|
+
* FUNNELS (0): none
|
|
11
|
+
*/
|
|
10
12
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
* DATASET OVERVIEW
|
|
14
|
-
* ═══════════════════════════════════════════════════════════════
|
|
15
|
-
*
|
|
16
|
-
* Foobar — minimal test/sanity dungeon with abstract event names.
|
|
17
|
-
* - Configurable scale (default 2 billion events for stress testing)
|
|
18
|
-
* - 10 events: foo, bar, baz, qux, garply, durtle, linny, fonk, crumn, yak
|
|
19
|
-
* - Super props: string, number, boolean, date
|
|
20
|
-
* - No funnels, no properties on events — pure weight-driven distribution
|
|
21
|
-
*
|
|
22
|
-
* ═══════════════════════════════════════════════════════════════
|
|
23
|
-
* ANALYTICS HOOKS (2 patterns)
|
|
24
|
-
* ═══════════════════════════════════════════════════════════════
|
|
25
|
-
*
|
|
13
|
+
// ── HOOK STORIES ──
|
|
14
|
+
/*
|
|
26
15
|
* 1. TEMPERATURE TAGGING (event hook)
|
|
27
16
|
* foo/bar/baz = "hot", crumn/yak = "cold", everything else = "warm".
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
* - Insights: any event, total events, breakdown by temperature
|
|
31
|
-
* Expected: "hot" dominates (highest-weight events)
|
|
17
|
+
* Mixpanel report: Insights, any event, total events, breakdown by temperature.
|
|
18
|
+
* Expected: "hot" dominates (highest-weight events).
|
|
32
19
|
*
|
|
33
20
|
* 2. HASH-BASED POWER USERS (everything hook)
|
|
34
|
-
* ~10% of users (by distinct_id
|
|
35
|
-
*
|
|
36
|
-
* Mixpanel
|
|
37
|
-
*
|
|
38
|
-
* Expected: ~10% of users have extra events tagged is_duplicate=true
|
|
21
|
+
* ~10% of users (by distinct_id charCode mod 10) get 3 extra duplicate events
|
|
22
|
+
* tagged is_duplicate=true.
|
|
23
|
+
* Mixpanel report: Insights, any event, total per user, breakdown by is_duplicate.
|
|
24
|
+
* Expected: ~10% of users have extra events tagged is_duplicate=true.
|
|
39
25
|
*/
|
|
40
26
|
|
|
27
|
+
// ── SCALE ──
|
|
28
|
+
const SEED = "dm4-foobar";
|
|
29
|
+
const NUM_DAYS = 30;
|
|
30
|
+
const NUM_USERS = 4_000_000;
|
|
31
|
+
const EVENTS_PER_DAY = 16.67;
|
|
32
|
+
const token = process.env.MP_TOKEN || "";
|
|
41
33
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
// If min equals max, just return the value
|
|
46
|
-
if (min === max) {
|
|
47
|
-
return min;
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
// Swap min and max if min is greater than max
|
|
51
|
-
if (min > max) {
|
|
52
|
-
[min, max] = [max, min];
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
// Generate a random integer between min and max (inclusive)
|
|
56
|
-
return Math.floor(Math.random() * (max - min + 1)) + min;
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
/** @typedef {import("../../types").Dungeon} Config */
|
|
60
|
-
|
|
34
|
+
// ── DATA ARRAYS ──
|
|
35
|
+
const colors = ["red", "orange", "yellow", "green", "blue", "indigo", "violet"];
|
|
36
|
+
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"];
|
|
61
37
|
const dates = [
|
|
62
38
|
"2024-09-18T21:48:28.173Z",
|
|
63
39
|
"2024-12-09T03:17:46.661Z",
|
|
@@ -161,26 +137,88 @@ const dates = [
|
|
|
161
137
|
"2024-06-03T06:52:21.652Z"
|
|
162
138
|
];
|
|
163
139
|
|
|
164
|
-
|
|
140
|
+
// ── HELPER FUNCTIONS ──
|
|
141
|
+
function integer(min = 1, max = 100) {
|
|
142
|
+
// If min equals max, just return the value
|
|
143
|
+
if (min === max) {
|
|
144
|
+
return min;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// Swap min and max if min is greater than max
|
|
148
|
+
if (min > max) {
|
|
149
|
+
[min, max] = [max, min];
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// Generate a random integer between min and max (inclusive)
|
|
153
|
+
return Math.floor(Math.random() * (max - min + 1)) + min;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function handleEventHook(record) {
|
|
157
|
+
// H1: temperature tagging — high-weight events "hot", low-weight "cold", rest "warm"
|
|
158
|
+
const hotEvents = ["foo", "bar", "baz"];
|
|
159
|
+
const coldEvents = ["crumn", "yak"];
|
|
160
|
+
if (hotEvents.includes(record.event)) {
|
|
161
|
+
record.temperature = "hot";
|
|
162
|
+
} else if (coldEvents.includes(record.event)) {
|
|
163
|
+
record.temperature = "cold";
|
|
164
|
+
} else {
|
|
165
|
+
record.temperature = "warm";
|
|
166
|
+
}
|
|
167
|
+
return record;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function handleEverythingHook(record, meta) {
|
|
171
|
+
// stamp profile-derived superProps for consistency
|
|
172
|
+
const profile = meta.profile;
|
|
173
|
+
record.forEach(e => {
|
|
174
|
+
e.string = profile.string;
|
|
175
|
+
e.number = profile.number;
|
|
176
|
+
e.boolean = profile.boolean;
|
|
177
|
+
e.date = profile.date;
|
|
178
|
+
e.temperature = profile.temperature;
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
// H2: hash-based power users — ~10% of users (distinct_id charCode mod 10) get 3 extra duplicate events
|
|
182
|
+
if (record.length > 0) {
|
|
183
|
+
const userId = record[0].user_id || record[0].distinct_id || "";
|
|
184
|
+
if (userId && userId.charCodeAt(0) % 10 === 0) {
|
|
185
|
+
const extras = record.slice(0, 3).map(e => ({
|
|
186
|
+
...e,
|
|
187
|
+
event: e.event,
|
|
188
|
+
user_id: e.user_id,
|
|
189
|
+
time: e.time,
|
|
190
|
+
is_duplicate: true,
|
|
191
|
+
}));
|
|
192
|
+
return record.concat(extras);
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
return record;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// ── CONFIG ──
|
|
199
|
+
/** @type {Config} */
|
|
165
200
|
const config = {
|
|
166
|
-
token,
|
|
167
201
|
seed: SEED,
|
|
168
|
-
numDays:
|
|
169
|
-
avgEventsPerUserPerDay:
|
|
170
|
-
numUsers:
|
|
202
|
+
numDays: NUM_DAYS,
|
|
203
|
+
avgEventsPerUserPerDay: EVENTS_PER_DAY,
|
|
204
|
+
numUsers: NUM_USERS,
|
|
171
205
|
format: 'json', //csv or json
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
206
|
+
credentials: {
|
|
207
|
+
token,
|
|
208
|
+
region: "US",
|
|
209
|
+
},
|
|
210
|
+
switches: {
|
|
211
|
+
hasSessionIds: false, //if true, hasSessionIds are created for each user
|
|
212
|
+
hasAdSpend: false,
|
|
213
|
+
hasAvatar: false,
|
|
214
|
+
hasBrowser: false,
|
|
215
|
+
hasCampaigns: false,
|
|
216
|
+
hasIOSDevices: false,
|
|
217
|
+
hasLocation: false,
|
|
218
|
+
isAnonymous: true,
|
|
219
|
+
hasAndroidDevices: false,
|
|
220
|
+
hasDesktopDevices: false,
|
|
221
|
+
},
|
|
184
222
|
writeToDisk: false,
|
|
185
223
|
concurrency: 1,
|
|
186
224
|
|
|
@@ -237,7 +275,7 @@ const config = {
|
|
|
237
275
|
}
|
|
238
276
|
],
|
|
239
277
|
superProps: {
|
|
240
|
-
string:
|
|
278
|
+
string: colors,
|
|
241
279
|
number: integer,
|
|
242
280
|
boolean: [true, false],
|
|
243
281
|
date: dates,
|
|
@@ -245,9 +283,9 @@ const config = {
|
|
|
245
283
|
},
|
|
246
284
|
userProps: {
|
|
247
285
|
luckyNumber: integer,
|
|
248
|
-
spiritAnimal:
|
|
286
|
+
spiritAnimal: spiritAnimals,
|
|
249
287
|
created: dates,
|
|
250
|
-
string:
|
|
288
|
+
string: colors,
|
|
251
289
|
number: integer,
|
|
252
290
|
boolean: [true, false],
|
|
253
291
|
date: dates,
|
|
@@ -257,56 +295,14 @@ const config = {
|
|
|
257
295
|
scdProps: {},
|
|
258
296
|
mirrorProps: {},
|
|
259
297
|
lookupTables: [],
|
|
260
|
-
groupKeys: [
|
|
261
|
-
|
|
262
|
-
groupProps: {
|
|
263
|
-
},
|
|
298
|
+
groupKeys: [],
|
|
299
|
+
groupProps: {},
|
|
264
300
|
|
|
265
301
|
hook: function (record, type, meta) {
|
|
266
|
-
|
|
267
|
-
if (type === "
|
|
268
|
-
const hotEvents = ["foo", "bar", "baz"];
|
|
269
|
-
const coldEvents = ["crumn", "yak"];
|
|
270
|
-
if (hotEvents.includes(record.event)) {
|
|
271
|
-
record.temperature = "hot";
|
|
272
|
-
} else if (coldEvents.includes(record.event)) {
|
|
273
|
-
record.temperature = "cold";
|
|
274
|
-
} else {
|
|
275
|
-
record.temperature = "warm";
|
|
276
|
-
}
|
|
277
|
-
}
|
|
278
|
-
|
|
279
|
-
// everything hook: hash-based cohort — 10% of users (by distinct_id) get doubled events
|
|
280
|
-
if (type === "everything") {
|
|
281
|
-
const profile = meta.profile;
|
|
282
|
-
record.forEach(e => {
|
|
283
|
-
e.string = profile.string;
|
|
284
|
-
e.number = profile.number;
|
|
285
|
-
e.boolean = profile.boolean;
|
|
286
|
-
e.date = profile.date;
|
|
287
|
-
e.temperature = profile.temperature;
|
|
288
|
-
});
|
|
289
|
-
if (record.length > 0) {
|
|
290
|
-
const userId = record[0].user_id || record[0].distinct_id || "";
|
|
291
|
-
if (userId && userId.charCodeAt(0) % 10 === 0) {
|
|
292
|
-
// power user: duplicate each event with a slight time offset
|
|
293
|
-
const extras = record.slice(0, 3).map(e => ({
|
|
294
|
-
...e,
|
|
295
|
-
event: e.event,
|
|
296
|
-
user_id: e.user_id,
|
|
297
|
-
time: e.time,
|
|
298
|
-
is_duplicate: true,
|
|
299
|
-
}));
|
|
300
|
-
return record.concat(extras);
|
|
301
|
-
}
|
|
302
|
-
}
|
|
303
|
-
return record;
|
|
304
|
-
}
|
|
305
|
-
|
|
302
|
+
if (type === "event") return handleEventHook(record);
|
|
303
|
+
if (type === "everything") return handleEverythingHook(record, meta);
|
|
306
304
|
return record;
|
|
307
305
|
}
|
|
308
306
|
};
|
|
309
307
|
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
export default config;
|
|
308
|
+
export default config;
|
|
@@ -1,50 +1,52 @@
|
|
|
1
|
-
// ──
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
const avg_events_per_user_per_day = 1;
|
|
6
|
-
let token = "";
|
|
7
|
-
|
|
8
|
-
// ── env overrides ──
|
|
9
|
-
if (process.env.MP_TOKEN) token = process.env.MP_TOKEN;
|
|
1
|
+
// ── IMPORTS ──
|
|
2
|
+
import Chance from 'chance';
|
|
3
|
+
import { weighNumRange, weighChoices } from "../../lib/utils/utils.js";
|
|
4
|
+
/** @typedef {import("../../types").Dungeon} Config */
|
|
10
5
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
* - No hooks
|
|
6
|
+
// ── OVERVIEW ──
|
|
7
|
+
/*
|
|
8
|
+
* NAME: group-analytics
|
|
9
|
+
* PURPOSE: exercises 3 groupKeys (company/team/project) at varied scales with per-key groupProps
|
|
10
|
+
* SCALE: 500 users, ~30K events, 60 days
|
|
11
|
+
* EVENTS (8): feature used, subscription change, support ticket, task completed,
|
|
12
|
+
* meeting scheduled, code committed, deployment run, login
|
|
13
|
+
* FUNNELS (0): none
|
|
14
|
+
* GROUPS: company_id (50), team_id (200), project_id (500)
|
|
21
15
|
*/
|
|
22
16
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
17
|
+
// ── SCALE ──
|
|
18
|
+
const SEED = "group-analytics";
|
|
19
|
+
const NUM_DAYS = 60;
|
|
20
|
+
const NUM_USERS = 500;
|
|
21
|
+
const EVENTS_PER_DAY = 1;
|
|
22
|
+
const token = process.env.MP_TOKEN || "";
|
|
26
23
|
|
|
27
|
-
|
|
28
|
-
|
|
24
|
+
const chance = new Chance();
|
|
25
|
+
|
|
26
|
+
// ── CONFIG ──
|
|
27
|
+
/** @type {Config} */
|
|
29
28
|
const config = {
|
|
30
|
-
token,
|
|
31
29
|
seed: SEED,
|
|
32
|
-
numDays:
|
|
33
|
-
avgEventsPerUserPerDay:
|
|
34
|
-
numUsers:
|
|
30
|
+
numDays: NUM_DAYS,
|
|
31
|
+
avgEventsPerUserPerDay: EVENTS_PER_DAY,
|
|
32
|
+
numUsers: NUM_USERS,
|
|
35
33
|
format: "json",
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
34
|
+
credentials: {
|
|
35
|
+
token,
|
|
36
|
+
region: "US",
|
|
37
|
+
},
|
|
38
|
+
switches: {
|
|
39
|
+
hasSessionIds: false,
|
|
40
|
+
hasAdSpend: false,
|
|
41
|
+
hasLocation: false,
|
|
42
|
+
hasAndroidDevices: false,
|
|
43
|
+
hasIOSDevices: false,
|
|
44
|
+
hasDesktopDevices: true,
|
|
45
|
+
hasBrowser: true,
|
|
46
|
+
hasCampaigns: false,
|
|
47
|
+
isAnonymous: false,
|
|
48
|
+
alsoInferFunnels: false,
|
|
49
|
+
},
|
|
48
50
|
concurrency: 1,
|
|
49
51
|
writeToDisk: false,
|
|
50
52
|
|