@ak--47/dungeon-master 1.0.0 → 1.2.0
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/README.md +7 -7
- package/dungeons/technical/ad-spend-schema.json +128 -0
- package/dungeons/technical/ad-spend.js +116 -0
- package/dungeons/technical/anonymous-users-schema.json +92 -0
- package/dungeons/technical/anonymous-users.js +102 -0
- package/dungeons/technical/array-of-object-lookup-schema.json +191 -0
- package/dungeons/{array-of-object-lookup.js → technical/array-of-object-lookup.js} +12 -5
- package/dungeons/technical/experiments-schema.json +203 -0
- package/dungeons/technical/experiments.js +168 -0
- package/dungeons/technical/foobar-schema.json +362 -0
- package/dungeons/{foobar.js → technical/foobar.js} +3 -3
- package/dungeons/technical/group-analytics-schema.json +241 -0
- package/dungeons/technical/group-analytics.js +151 -0
- package/dungeons/technical/mirror-strategies-schema.json +84 -0
- package/dungeons/technical/mirror-strategies.js +126 -0
- package/dungeons/technical/nested-objects-schema.json +145 -0
- package/dungeons/technical/nested-objects.js +207 -0
- package/dungeons/technical/retention-cadence-schema.json +37 -0
- package/dungeons/{retention-cadence.js → technical/retention-cadence.js} +2 -3
- package/dungeons/technical/sanity-schema.json +185 -0
- package/dungeons/{sanity.js → technical/sanity.js} +4 -2
- package/dungeons/technical/scale-test-schema.json +70 -0
- package/dungeons/technical/scale-test.js +90 -0
- package/dungeons/technical/scd-schema.json +467 -0
- package/dungeons/{scd.js → technical/scd.js} +20 -10
- package/dungeons/technical/simple-schema.json +362 -0
- package/dungeons/{simple.js → technical/simple.js} +7 -7
- package/dungeons/technical/simplest-schema.json +724 -0
- package/dungeons/{simplest.js → technical/simplest.js} +7 -7
- package/dungeons/technical/text-generation-schema.json +1062 -0
- package/dungeons/{text-generation.js → technical/text-generation.js} +24 -11
- package/dungeons/vertical/community-schema.json +579 -0
- package/dungeons/vertical/community.js +794 -0
- package/dungeons/vertical/devtools-schema.json +601 -0
- package/dungeons/vertical/devtools.js +843 -0
- package/dungeons/vertical/ecommerce-schema.json +604 -0
- package/dungeons/{ecommerce.js → vertical/ecommerce.js} +43 -5
- package/dungeons/vertical/education-schema.json +5686 -0
- package/dungeons/{education.js → vertical/education.js} +70 -39
- package/dungeons/vertical/fintech-schema.json +630 -0
- package/dungeons/{fintech.js → vertical/fintech.js} +107 -56
- package/dungeons/vertical/fitness-schema.json +530 -0
- package/dungeons/vertical/fitness.js +808 -0
- package/dungeons/vertical/food-delivery-schema.json +36728 -0
- package/dungeons/{food.js → vertical/food-delivery.js} +67 -26
- package/dungeons/vertical/gaming-schema.json +438 -0
- package/dungeons/{gaming.js → vertical/gaming.js} +29 -16
- package/dungeons/vertical/healthcare-schema.json +549 -0
- package/dungeons/vertical/healthcare.js +775 -0
- package/dungeons/vertical/insurance-application-schema.json +485 -0
- package/dungeons/{insurance-application.js → vertical/insurance-application.js} +41 -31
- package/dungeons/vertical/logistics-schema.json +574 -0
- package/dungeons/vertical/logistics.js +799 -0
- package/dungeons/vertical/marketplace-schema.json +533 -0
- package/dungeons/vertical/marketplace.js +850 -0
- package/dungeons/vertical/media-schema.json +4749 -0
- package/dungeons/{media.js → vertical/media.js} +103 -19
- package/dungeons/vertical/rpg-schema.json +2491 -0
- package/dungeons/{rpg.js → vertical/rpg.js} +87 -60
- package/dungeons/vertical/sass-schema.json +3128 -0
- package/dungeons/{sass.js → vertical/sass.js} +65 -26
- package/dungeons/vertical/social-schema.json +620 -0
- package/dungeons/{social.js → vertical/social.js} +76 -17
- package/dungeons/vertical/travel-schema.json +580 -0
- package/dungeons/vertical/travel.js +742 -0
- package/index.js +20 -16
- package/lib/core/config-validator.js +286 -1
- package/lib/core/context.js +16 -5
- package/lib/core/storage.js +2 -0
- package/lib/generators/adspend.js +3 -3
- package/lib/generators/events.js +141 -14
- package/lib/generators/funnels.js +67 -7
- package/lib/generators/text.js +2 -2
- package/lib/orchestrators/user-loop.js +444 -22
- package/lib/templates/soup-presets.js +2 -2
- package/lib/utils/utils.js +4 -5
- package/package.json +4 -3
- package/scripts/dungeon-to-json.mjs +163 -0
- package/scripts/extract-dungeon-schema.mjs +343 -0
- package/scripts/json-to-dungeon.mjs +200 -0
- package/scripts/run-dungeon.mjs +76 -0
- package/scripts/run-many.mjs +118 -0
- package/scripts/verify-runner.mjs +42 -0
- package/types.d.ts +316 -1
- package/dungeons/array-of-object-lookup-schema.json +0 -327
- package/dungeons/ecommerce-schema.json +0 -462
- package/dungeons/education-schema.json +0 -2409
- package/dungeons/fintech-schema.json +0 -14034
- package/dungeons/foobar-schema.json +0 -403
- package/dungeons/food-delivery-schema.json +0 -192
- package/dungeons/food-delivery.js +0 -602
- package/dungeons/food-schema.json +0 -1152
- package/dungeons/gaming-schema.json +0 -1270
- package/dungeons/insurance-application-schema.json +0 -204
- package/dungeons/media-schema.json +0 -906
- package/dungeons/retention-cadence-schema.json +0 -78
- package/dungeons/rpg-schema.json +0 -4526
- package/dungeons/sanity-schema.json +0 -255
- package/dungeons/sass-schema.json +0 -1291
- package/dungeons/scd-schema.json +0 -919
- package/dungeons/simple-schema.json +0 -608
- package/dungeons/simplest-schema.json +0 -1418
- package/dungeons/social-schema.json +0 -1118
- package/dungeons/text-generation-schema.json +0 -3096
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Group Analytics — tests multiple group keys with varied entity counts.
|
|
3
|
+
*
|
|
4
|
+
* Exercises: 3 group keys (company, team, project) at different scales,
|
|
5
|
+
* each with distinct event associations and group-level properties.
|
|
6
|
+
*
|
|
7
|
+
* - 500 users, 30K events, 60 days
|
|
8
|
+
* - 3 group keys: company_id (50), team_id (200), project_id (500)
|
|
9
|
+
* - groupProps with varied property types per key
|
|
10
|
+
* - No hooks
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import Chance from 'chance';
|
|
14
|
+
let chance = new Chance();
|
|
15
|
+
import { weighNumRange, weighChoices } from "../../lib/utils/utils.js";
|
|
16
|
+
|
|
17
|
+
/** @typedef {import("../../types").Dungeon} Config */
|
|
18
|
+
/** @type {import('../../types').Dungeon} */
|
|
19
|
+
const config = {
|
|
20
|
+
token: "",
|
|
21
|
+
seed: "group-analytics",
|
|
22
|
+
numDays: 60,
|
|
23
|
+
numEvents: 30_000,
|
|
24
|
+
numUsers: 500,
|
|
25
|
+
format: "json",
|
|
26
|
+
region: "US",
|
|
27
|
+
hasAnonIds: false,
|
|
28
|
+
hasSessionIds: false,
|
|
29
|
+
hasAdSpend: false,
|
|
30
|
+
hasLocation: false,
|
|
31
|
+
hasAndroidDevices: false,
|
|
32
|
+
hasIOSDevices: false,
|
|
33
|
+
hasDesktopDevices: true,
|
|
34
|
+
hasBrowser: true,
|
|
35
|
+
hasCampaigns: false,
|
|
36
|
+
isAnonymous: false,
|
|
37
|
+
alsoInferFunnels: false,
|
|
38
|
+
concurrency: 1,
|
|
39
|
+
writeToDisk: false,
|
|
40
|
+
|
|
41
|
+
events: [
|
|
42
|
+
{
|
|
43
|
+
event: "feature used",
|
|
44
|
+
weight: 8,
|
|
45
|
+
properties: {
|
|
46
|
+
feature: ["dashboard", "reports", "settings", "api", "integrations"],
|
|
47
|
+
}
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
event: "subscription change",
|
|
51
|
+
weight: 2,
|
|
52
|
+
properties: {
|
|
53
|
+
plan: ["free", "starter", "business", "enterprise"],
|
|
54
|
+
action: ["upgrade", "downgrade", "renew"],
|
|
55
|
+
}
|
|
56
|
+
},
|
|
57
|
+
{
|
|
58
|
+
event: "support ticket",
|
|
59
|
+
weight: 3,
|
|
60
|
+
properties: {
|
|
61
|
+
priority: weighChoices(["low", "low", "medium", "medium", "high"]),
|
|
62
|
+
category: ["billing", "technical", "onboarding", "feature request"],
|
|
63
|
+
}
|
|
64
|
+
},
|
|
65
|
+
{
|
|
66
|
+
event: "task completed",
|
|
67
|
+
weight: 7,
|
|
68
|
+
properties: {
|
|
69
|
+
type: ["bug", "feature", "chore", "docs"],
|
|
70
|
+
points: weighNumRange(1, 13),
|
|
71
|
+
}
|
|
72
|
+
},
|
|
73
|
+
{
|
|
74
|
+
event: "meeting scheduled",
|
|
75
|
+
weight: 4,
|
|
76
|
+
properties: {
|
|
77
|
+
duration: [15, 30, 45, 60],
|
|
78
|
+
type: ["standup", "planning", "retro", "1:1", "all-hands"],
|
|
79
|
+
}
|
|
80
|
+
},
|
|
81
|
+
{
|
|
82
|
+
event: "code committed",
|
|
83
|
+
weight: 6,
|
|
84
|
+
properties: {
|
|
85
|
+
language: ["javascript", "python", "go", "rust", "java"],
|
|
86
|
+
linesChanged: weighNumRange(1, 500, .25),
|
|
87
|
+
}
|
|
88
|
+
},
|
|
89
|
+
{
|
|
90
|
+
event: "deployment run",
|
|
91
|
+
weight: 3,
|
|
92
|
+
properties: {
|
|
93
|
+
environment: ["dev", "staging", "production"],
|
|
94
|
+
status: weighChoices(["success", "success", "success", "success", "failure"]),
|
|
95
|
+
durationSec: weighNumRange(10, 300, .25),
|
|
96
|
+
}
|
|
97
|
+
},
|
|
98
|
+
{
|
|
99
|
+
event: "login",
|
|
100
|
+
weight: 5,
|
|
101
|
+
isFirstEvent: true,
|
|
102
|
+
properties: {
|
|
103
|
+
method: ["password", "sso", "oauth"],
|
|
104
|
+
}
|
|
105
|
+
},
|
|
106
|
+
],
|
|
107
|
+
|
|
108
|
+
funnels: [],
|
|
109
|
+
|
|
110
|
+
superProps: {},
|
|
111
|
+
|
|
112
|
+
userProps: {
|
|
113
|
+
role: ["engineer", "designer", "pm", "manager", "exec"],
|
|
114
|
+
},
|
|
115
|
+
|
|
116
|
+
scdProps: {},
|
|
117
|
+
mirrorProps: {},
|
|
118
|
+
|
|
119
|
+
groupKeys: [
|
|
120
|
+
["company_id", 50, ["feature used", "subscription change", "support ticket"]],
|
|
121
|
+
["team_id", 200, ["feature used", "task completed", "meeting scheduled"]],
|
|
122
|
+
["project_id", 500, ["task completed", "code committed", "deployment run"]],
|
|
123
|
+
],
|
|
124
|
+
|
|
125
|
+
groupProps: {
|
|
126
|
+
company_id: {
|
|
127
|
+
name: () => chance.company(),
|
|
128
|
+
industry: ["technology", "finance", "healthcare", "retail", "education", "manufacturing"],
|
|
129
|
+
plan: ["free", "starter", "business", "enterprise"],
|
|
130
|
+
employee_count: weighNumRange(10, 5000, .25),
|
|
131
|
+
},
|
|
132
|
+
team_id: {
|
|
133
|
+
name: () => `${chance.pickone(["Alpha", "Beta", "Gamma", "Delta", "Epsilon", "Zeta", "Theta", "Omega"])} ${chance.pickone(["Squad", "Team", "Pod", "Crew"])}`,
|
|
134
|
+
department: ["engineering", "design", "product", "marketing", "sales", "support"],
|
|
135
|
+
size: weighNumRange(3, 20),
|
|
136
|
+
},
|
|
137
|
+
project_id: {
|
|
138
|
+
name: () => `${chance.pickone(["project", "initiative", "sprint"])} ${chance.word()}`,
|
|
139
|
+
status: ["active", "paused", "completed", "archived"],
|
|
140
|
+
priority: weighChoices(["low", "medium", "medium", "high", "critical"]),
|
|
141
|
+
},
|
|
142
|
+
},
|
|
143
|
+
|
|
144
|
+
lookupTables: [],
|
|
145
|
+
|
|
146
|
+
hook: function (record, type, meta) {
|
|
147
|
+
return record;
|
|
148
|
+
}
|
|
149
|
+
};
|
|
150
|
+
|
|
151
|
+
export default config;
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
{
|
|
2
|
+
"events": [
|
|
3
|
+
{
|
|
4
|
+
"event": "page view",
|
|
5
|
+
"weight": 10,
|
|
6
|
+
"properties": {
|
|
7
|
+
"page": [
|
|
8
|
+
"/",
|
|
9
|
+
"/help",
|
|
10
|
+
"/account",
|
|
11
|
+
"/pricing",
|
|
12
|
+
"/product"
|
|
13
|
+
]
|
|
14
|
+
}
|
|
15
|
+
},
|
|
16
|
+
{
|
|
17
|
+
"event": "checkout",
|
|
18
|
+
"weight": 3,
|
|
19
|
+
"properties": {
|
|
20
|
+
"amount": {
|
|
21
|
+
"$range": [
|
|
22
|
+
44,
|
|
23
|
+
157
|
|
24
|
+
]
|
|
25
|
+
},
|
|
26
|
+
"currency": [
|
|
27
|
+
"USD",
|
|
28
|
+
"EUR",
|
|
29
|
+
"GBP"
|
|
30
|
+
]
|
|
31
|
+
}
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
"event": "profile update",
|
|
35
|
+
"weight": 4,
|
|
36
|
+
"properties": {
|
|
37
|
+
"field": [
|
|
38
|
+
"name",
|
|
39
|
+
"email",
|
|
40
|
+
"avatar",
|
|
41
|
+
"address",
|
|
42
|
+
"phone"
|
|
43
|
+
]
|
|
44
|
+
}
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
"event": "subscription change",
|
|
48
|
+
"weight": 2,
|
|
49
|
+
"properties": {
|
|
50
|
+
"plan": [
|
|
51
|
+
"free",
|
|
52
|
+
"starter",
|
|
53
|
+
"pro",
|
|
54
|
+
"enterprise"
|
|
55
|
+
],
|
|
56
|
+
"action": [
|
|
57
|
+
"upgrade",
|
|
58
|
+
"downgrade",
|
|
59
|
+
"cancel",
|
|
60
|
+
"renew"
|
|
61
|
+
]
|
|
62
|
+
}
|
|
63
|
+
},
|
|
64
|
+
{
|
|
65
|
+
"event": "login",
|
|
66
|
+
"weight": 8,
|
|
67
|
+
"properties": {
|
|
68
|
+
"method": [
|
|
69
|
+
"password",
|
|
70
|
+
"google",
|
|
71
|
+
"sso"
|
|
72
|
+
]
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
],
|
|
76
|
+
"userProps": {
|
|
77
|
+
"plan": [
|
|
78
|
+
"free",
|
|
79
|
+
"starter",
|
|
80
|
+
"pro",
|
|
81
|
+
"enterprise"
|
|
82
|
+
]
|
|
83
|
+
}
|
|
84
|
+
}
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Mirror Strategies — tests all 4 mirror data strategies.
|
|
3
|
+
*
|
|
4
|
+
* Exercises: create, update, fill, delete mirror transformations.
|
|
5
|
+
* Each mirror prop targets a different event and uses a different
|
|
6
|
+
* strategy so the output can be diffed against the original data.
|
|
7
|
+
*
|
|
8
|
+
* - 500 users, 30K events, 30 days
|
|
9
|
+
* - 5 simple events, no funnels, no hooks
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import Chance from 'chance';
|
|
13
|
+
let chance = new Chance();
|
|
14
|
+
import { weighNumRange, weighChoices } from "../../lib/utils/utils.js";
|
|
15
|
+
|
|
16
|
+
/** @typedef {import("../../types").Dungeon} Config */
|
|
17
|
+
/** @type {import('../../types').Dungeon} */
|
|
18
|
+
const config = {
|
|
19
|
+
token: "",
|
|
20
|
+
seed: "mirror-strategies",
|
|
21
|
+
numDays: 30,
|
|
22
|
+
numEvents: 30_000,
|
|
23
|
+
numUsers: 500,
|
|
24
|
+
format: "json",
|
|
25
|
+
region: "US",
|
|
26
|
+
hasAnonIds: false,
|
|
27
|
+
hasSessionIds: false,
|
|
28
|
+
hasAdSpend: false,
|
|
29
|
+
hasLocation: false,
|
|
30
|
+
hasAndroidDevices: false,
|
|
31
|
+
hasIOSDevices: false,
|
|
32
|
+
hasDesktopDevices: false,
|
|
33
|
+
hasBrowser: false,
|
|
34
|
+
hasCampaigns: false,
|
|
35
|
+
isAnonymous: false,
|
|
36
|
+
alsoInferFunnels: false,
|
|
37
|
+
concurrency: 1,
|
|
38
|
+
writeToDisk: false,
|
|
39
|
+
|
|
40
|
+
events: [
|
|
41
|
+
{
|
|
42
|
+
event: "page view",
|
|
43
|
+
weight: 10,
|
|
44
|
+
properties: {
|
|
45
|
+
page: ["/", "/help", "/account", "/pricing", "/product"],
|
|
46
|
+
}
|
|
47
|
+
},
|
|
48
|
+
{
|
|
49
|
+
event: "checkout",
|
|
50
|
+
weight: 3,
|
|
51
|
+
properties: {
|
|
52
|
+
amount: weighNumRange(5, 200, .25),
|
|
53
|
+
currency: ["USD", "EUR", "GBP"],
|
|
54
|
+
}
|
|
55
|
+
},
|
|
56
|
+
{
|
|
57
|
+
event: "profile update",
|
|
58
|
+
weight: 4,
|
|
59
|
+
properties: {
|
|
60
|
+
field: ["name", "email", "avatar", "address", "phone"],
|
|
61
|
+
}
|
|
62
|
+
},
|
|
63
|
+
{
|
|
64
|
+
event: "subscription change",
|
|
65
|
+
weight: 2,
|
|
66
|
+
properties: {
|
|
67
|
+
plan: ["free", "starter", "pro", "enterprise"],
|
|
68
|
+
action: ["upgrade", "downgrade", "cancel", "renew"],
|
|
69
|
+
}
|
|
70
|
+
},
|
|
71
|
+
{
|
|
72
|
+
event: "login",
|
|
73
|
+
weight: 8,
|
|
74
|
+
properties: {
|
|
75
|
+
method: ["password", "google", "sso"],
|
|
76
|
+
}
|
|
77
|
+
},
|
|
78
|
+
],
|
|
79
|
+
|
|
80
|
+
funnels: [],
|
|
81
|
+
|
|
82
|
+
superProps: {},
|
|
83
|
+
|
|
84
|
+
userProps: {
|
|
85
|
+
plan: ["free", "starter", "pro", "enterprise"],
|
|
86
|
+
},
|
|
87
|
+
|
|
88
|
+
scdProps: {},
|
|
89
|
+
|
|
90
|
+
mirrorProps: {
|
|
91
|
+
// "update" — adds a status property to profile update events; keeps existing values
|
|
92
|
+
user_status: {
|
|
93
|
+
events: ["profile update"],
|
|
94
|
+
strategy: "update",
|
|
95
|
+
values: ["active", "inactive", "suspended", "pending"],
|
|
96
|
+
},
|
|
97
|
+
// "create" — always creates a new tier property on subscription change events
|
|
98
|
+
subscription_tier: {
|
|
99
|
+
events: ["subscription change"],
|
|
100
|
+
strategy: "create",
|
|
101
|
+
values: ["bronze", "silver", "gold", "platinum"],
|
|
102
|
+
},
|
|
103
|
+
// "fill" — fills login_source on login events only if older than 10 days
|
|
104
|
+
login_source: {
|
|
105
|
+
events: ["login"],
|
|
106
|
+
strategy: "fill",
|
|
107
|
+
values: ["web", "mobile", "api", "desktop"],
|
|
108
|
+
daysUnfilled: 10,
|
|
109
|
+
},
|
|
110
|
+
// "delete" — removes the amount property from checkout events in the mirror
|
|
111
|
+
amount: {
|
|
112
|
+
events: ["checkout"],
|
|
113
|
+
strategy: "delete",
|
|
114
|
+
},
|
|
115
|
+
},
|
|
116
|
+
|
|
117
|
+
groupKeys: [],
|
|
118
|
+
groupProps: {},
|
|
119
|
+
lookupTables: [],
|
|
120
|
+
|
|
121
|
+
hook: function (record, type, meta) {
|
|
122
|
+
return record;
|
|
123
|
+
}
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
export default config;
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
{
|
|
2
|
+
"events": [
|
|
3
|
+
{
|
|
4
|
+
"event": "checkout",
|
|
5
|
+
"weight": 3,
|
|
6
|
+
"properties": {
|
|
7
|
+
"cart": {
|
|
8
|
+
"$type": "object[]",
|
|
9
|
+
"$keys": [
|
|
10
|
+
"product_id",
|
|
11
|
+
"name",
|
|
12
|
+
"price",
|
|
13
|
+
"quantity",
|
|
14
|
+
"category"
|
|
15
|
+
]
|
|
16
|
+
},
|
|
17
|
+
"total_amount": {
|
|
18
|
+
"$range": [
|
|
19
|
+
408,
|
|
20
|
+
1563
|
|
21
|
+
]
|
|
22
|
+
},
|
|
23
|
+
"currency": [
|
|
24
|
+
"USD",
|
|
25
|
+
"EUR",
|
|
26
|
+
"GBP",
|
|
27
|
+
"JPY",
|
|
28
|
+
"CAD"
|
|
29
|
+
],
|
|
30
|
+
"session_id": {
|
|
31
|
+
"$type": "string"
|
|
32
|
+
},
|
|
33
|
+
"metadata": {
|
|
34
|
+
"$type": "object"
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
"event": "search performed",
|
|
40
|
+
"weight": 5,
|
|
41
|
+
"properties": {
|
|
42
|
+
"query": [
|
|
43
|
+
"laptop",
|
|
44
|
+
"shoes",
|
|
45
|
+
"headphones",
|
|
46
|
+
"jacket",
|
|
47
|
+
"phone case",
|
|
48
|
+
"coffee maker",
|
|
49
|
+
"backpack",
|
|
50
|
+
"monitor",
|
|
51
|
+
"keyboard",
|
|
52
|
+
"mouse"
|
|
53
|
+
],
|
|
54
|
+
"results": {
|
|
55
|
+
"$type": "object[]",
|
|
56
|
+
"$keys": [
|
|
57
|
+
"result_id",
|
|
58
|
+
"title",
|
|
59
|
+
"score",
|
|
60
|
+
"sponsored"
|
|
61
|
+
]
|
|
62
|
+
},
|
|
63
|
+
"result_count": {
|
|
64
|
+
"$range": [
|
|
65
|
+
6,
|
|
66
|
+
46
|
|
67
|
+
]
|
|
68
|
+
},
|
|
69
|
+
"top_result_id": {
|
|
70
|
+
"$type": "string"
|
|
71
|
+
},
|
|
72
|
+
"session_id": {
|
|
73
|
+
"$type": "string"
|
|
74
|
+
},
|
|
75
|
+
"metadata": {
|
|
76
|
+
"$type": "object"
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
},
|
|
80
|
+
{
|
|
81
|
+
"event": "form submitted",
|
|
82
|
+
"weight": 4,
|
|
83
|
+
"properties": {
|
|
84
|
+
"form_name": [
|
|
85
|
+
"checkout",
|
|
86
|
+
"signup",
|
|
87
|
+
"contact",
|
|
88
|
+
"feedback",
|
|
89
|
+
"settings",
|
|
90
|
+
"profile"
|
|
91
|
+
],
|
|
92
|
+
"fields": {
|
|
93
|
+
"$type": "object[]",
|
|
94
|
+
"$keys": [
|
|
95
|
+
"field_name",
|
|
96
|
+
"value",
|
|
97
|
+
"valid"
|
|
98
|
+
]
|
|
99
|
+
},
|
|
100
|
+
"submission_valid": [
|
|
101
|
+
true,
|
|
102
|
+
false
|
|
103
|
+
],
|
|
104
|
+
"session_id": {
|
|
105
|
+
"$type": "string"
|
|
106
|
+
},
|
|
107
|
+
"metadata": {
|
|
108
|
+
"$type": "object"
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
},
|
|
112
|
+
{
|
|
113
|
+
"event": "page view",
|
|
114
|
+
"weight": 8,
|
|
115
|
+
"properties": {
|
|
116
|
+
"page": [
|
|
117
|
+
"/",
|
|
118
|
+
"/products",
|
|
119
|
+
"/cart",
|
|
120
|
+
"/search",
|
|
121
|
+
"/account",
|
|
122
|
+
"/help",
|
|
123
|
+
"/about"
|
|
124
|
+
],
|
|
125
|
+
"product_id": {
|
|
126
|
+
"$type": "string"
|
|
127
|
+
},
|
|
128
|
+
"session_id": {
|
|
129
|
+
"$type": "string"
|
|
130
|
+
},
|
|
131
|
+
"metadata": {
|
|
132
|
+
"$type": "object"
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
],
|
|
137
|
+
"userProps": {
|
|
138
|
+
"plan": [
|
|
139
|
+
"free",
|
|
140
|
+
"basic",
|
|
141
|
+
"pro",
|
|
142
|
+
"enterprise"
|
|
143
|
+
]
|
|
144
|
+
}
|
|
145
|
+
}
|
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
import Chance from 'chance';
|
|
2
|
+
let chance = new Chance();
|
|
3
|
+
import dayjs from "dayjs";
|
|
4
|
+
import utc from "dayjs/plugin/utc.js";
|
|
5
|
+
dayjs.extend(utc);
|
|
6
|
+
import { uid, comma } from 'ak-tools';
|
|
7
|
+
import { weighNumRange, date, integer, weighChoices } from "../../lib/utils/utils.js";
|
|
8
|
+
|
|
9
|
+
/** @typedef {import("../../types").Dungeon} Config */
|
|
10
|
+
/**
|
|
11
|
+
* ═══════════════════════════════════════════════════════════════
|
|
12
|
+
* TECHNICAL TEST: Nested Objects & High Cardinality
|
|
13
|
+
* ═══════════════════════════════════════════════════════════════
|
|
14
|
+
*
|
|
15
|
+
* Tests Mixpanel's handling of:
|
|
16
|
+
* - Array-of-object event properties (cart items, search results, form fields)
|
|
17
|
+
* - Deeply nested object properties (metadata.browser.name, etc.)
|
|
18
|
+
* - High cardinality string properties (10K product IDs, unique session IDs)
|
|
19
|
+
*
|
|
20
|
+
* 500 users, 30K events, 30 days. No hooks.
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
const productCategories = ["electronics", "books", "clothing", "home", "garden", "toys", "sports", "automotive", "beauty", "health", "grocery", "jewelry", "shoes", "tools", "office"];
|
|
24
|
+
const browsers = ["Chrome", "Firefox", "Safari", "Edge", "Opera", "Brave"];
|
|
25
|
+
const browserVersions = ["120.0", "121.0", "119.0", "118.0", "117.0", "116.0", "115.0"];
|
|
26
|
+
const deviceTypes = ["desktop", "mobile", "tablet"];
|
|
27
|
+
const osList = ["Windows 11", "macOS 14", "iOS 17", "Android 14", "Linux", "ChromeOS"];
|
|
28
|
+
const cities = ["New York", "London", "Tokyo", "Berlin", "Sydney", "Toronto", "Paris", "Mumbai", "Seoul", "Mexico City", "Cairo", "Lagos", "Dubai", "Bangkok", "Istanbul"];
|
|
29
|
+
const countries = ["US", "UK", "JP", "DE", "AU", "CA", "FR", "IN", "KR", "MX", "EG", "NG", "AE", "TH", "TR"];
|
|
30
|
+
|
|
31
|
+
/** @type {import('../../types').Dungeon} */
|
|
32
|
+
const config = {
|
|
33
|
+
seed: "nested objects test",
|
|
34
|
+
name: "nested-objects",
|
|
35
|
+
numDays: 30,
|
|
36
|
+
numEvents: 30_000,
|
|
37
|
+
numUsers: 500,
|
|
38
|
+
format: 'json',
|
|
39
|
+
region: "US",
|
|
40
|
+
hasAnonIds: false,
|
|
41
|
+
hasSessionIds: false,
|
|
42
|
+
hasAdSpend: false,
|
|
43
|
+
hasLocation: false,
|
|
44
|
+
hasAndroidDevices: false,
|
|
45
|
+
hasIOSDevices: false,
|
|
46
|
+
hasDesktopDevices: true,
|
|
47
|
+
hasBrowser: false,
|
|
48
|
+
hasCampaigns: false,
|
|
49
|
+
isAnonymous: false,
|
|
50
|
+
alsoInferFunnels: false,
|
|
51
|
+
concurrency: 1,
|
|
52
|
+
writeToDisk: false,
|
|
53
|
+
|
|
54
|
+
events: [
|
|
55
|
+
{
|
|
56
|
+
event: "checkout",
|
|
57
|
+
weight: 3,
|
|
58
|
+
properties: {
|
|
59
|
+
// Array of objects: cart items
|
|
60
|
+
cart: makeCartItems(),
|
|
61
|
+
total_amount: weighNumRange(10, 2000, .25),
|
|
62
|
+
currency: ["USD", "EUR", "GBP", "JPY", "CAD"],
|
|
63
|
+
// High cardinality: unique session per event
|
|
64
|
+
session_id: () => chance.guid(),
|
|
65
|
+
// Deeply nested metadata
|
|
66
|
+
metadata: makeMetadata(),
|
|
67
|
+
}
|
|
68
|
+
},
|
|
69
|
+
{
|
|
70
|
+
event: "search performed",
|
|
71
|
+
weight: 5,
|
|
72
|
+
properties: {
|
|
73
|
+
query: ["laptop", "shoes", "headphones", "jacket", "phone case", "coffee maker", "backpack", "monitor", "keyboard", "mouse"],
|
|
74
|
+
// Array of objects: search results
|
|
75
|
+
results: makeSearchResults(),
|
|
76
|
+
result_count: weighNumRange(0, 50),
|
|
77
|
+
// High cardinality product ID
|
|
78
|
+
top_result_id: () => `prod_${integer(1, 10000)}`,
|
|
79
|
+
session_id: () => chance.guid(),
|
|
80
|
+
metadata: makeMetadata(),
|
|
81
|
+
}
|
|
82
|
+
},
|
|
83
|
+
{
|
|
84
|
+
event: "form submitted",
|
|
85
|
+
weight: 4,
|
|
86
|
+
properties: {
|
|
87
|
+
form_name: ["checkout", "signup", "contact", "feedback", "settings", "profile"],
|
|
88
|
+
// Array of objects: form fields
|
|
89
|
+
fields: makeFormFields(),
|
|
90
|
+
submission_valid: [true, true, true, false],
|
|
91
|
+
session_id: () => chance.guid(),
|
|
92
|
+
metadata: makeMetadata(),
|
|
93
|
+
}
|
|
94
|
+
},
|
|
95
|
+
{
|
|
96
|
+
event: "page view",
|
|
97
|
+
weight: 8,
|
|
98
|
+
properties: {
|
|
99
|
+
page: ["/", "/products", "/cart", "/search", "/account", "/help", "/about"],
|
|
100
|
+
// High cardinality
|
|
101
|
+
product_id: () => `prod_${integer(1, 10000)}`,
|
|
102
|
+
session_id: () => chance.guid(),
|
|
103
|
+
metadata: makeMetadata(),
|
|
104
|
+
}
|
|
105
|
+
},
|
|
106
|
+
],
|
|
107
|
+
funnels: [],
|
|
108
|
+
superProps: {},
|
|
109
|
+
userProps: {
|
|
110
|
+
plan: ["free", "basic", "pro", "enterprise"],
|
|
111
|
+
},
|
|
112
|
+
scdProps: {},
|
|
113
|
+
mirrorProps: {},
|
|
114
|
+
groupKeys: [],
|
|
115
|
+
groupProps: {},
|
|
116
|
+
lookupTables: [],
|
|
117
|
+
hook: function (record, type, meta) {
|
|
118
|
+
return record;
|
|
119
|
+
}
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Generates an array-of-objects property for cart items.
|
|
124
|
+
* Each cart has 1-5 items with product_id, name, price, quantity, category.
|
|
125
|
+
*/
|
|
126
|
+
function makeCartItems() {
|
|
127
|
+
return function () {
|
|
128
|
+
const items = [];
|
|
129
|
+
const count = integer(1, 5);
|
|
130
|
+
for (let i = 0; i < count; i++) {
|
|
131
|
+
items.push({
|
|
132
|
+
product_id: `prod_${integer(1, 10000)}`,
|
|
133
|
+
name: `${chance.pickone(["Premium", "Basic", "Ultra", "Eco", "Pro"])} ${chance.pickone(["Widget", "Gadget", "Device", "Tool", "Kit"])}`,
|
|
134
|
+
price: integer(5, 500),
|
|
135
|
+
quantity: integer(1, 4),
|
|
136
|
+
category: chance.pickone(productCategories),
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
return () => [items];
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Generates an array-of-objects property for search results.
|
|
145
|
+
* Each search has 1-5 results with result_id, title, score, sponsored.
|
|
146
|
+
*/
|
|
147
|
+
function makeSearchResults() {
|
|
148
|
+
return function () {
|
|
149
|
+
const results = [];
|
|
150
|
+
const count = integer(1, 5);
|
|
151
|
+
for (let i = 0; i < count; i++) {
|
|
152
|
+
results.push({
|
|
153
|
+
result_id: `res_${integer(1, 50000)}`,
|
|
154
|
+
title: `${chance.pickone(["Best", "Top", "New", "Sale"])} ${chance.pickone(productCategories)} item`,
|
|
155
|
+
score: parseFloat((Math.random() * 100).toFixed(2)),
|
|
156
|
+
sponsored: chance.pickone([true, false, false, false]),
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
return () => [results];
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Generates an array-of-objects property for form fields.
|
|
165
|
+
* Each form has 2-6 fields with field_name, value, valid.
|
|
166
|
+
*/
|
|
167
|
+
function makeFormFields() {
|
|
168
|
+
return function () {
|
|
169
|
+
const fieldNames = ["email", "name", "phone", "address", "city", "zip", "country", "card_number", "expiry", "cvv"];
|
|
170
|
+
const fields = [];
|
|
171
|
+
const count = integer(2, 6);
|
|
172
|
+
const picked = chance.pickset(fieldNames, count);
|
|
173
|
+
for (const name of picked) {
|
|
174
|
+
fields.push({
|
|
175
|
+
field_name: name,
|
|
176
|
+
value: name === "email" ? chance.email() : chance.word(),
|
|
177
|
+
valid: chance.pickone([true, true, true, false]),
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
return () => [fields];
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* Generates a deeply nested metadata object:
|
|
186
|
+
* metadata: { browser: { name, version }, device: { type, os }, location: { city, country } }
|
|
187
|
+
*/
|
|
188
|
+
function makeMetadata() {
|
|
189
|
+
return function () {
|
|
190
|
+
return {
|
|
191
|
+
browser: {
|
|
192
|
+
name: chance.pickone(browsers),
|
|
193
|
+
version: chance.pickone(browserVersions),
|
|
194
|
+
},
|
|
195
|
+
device: {
|
|
196
|
+
type: chance.pickone(deviceTypes),
|
|
197
|
+
os: chance.pickone(osList),
|
|
198
|
+
},
|
|
199
|
+
location: {
|
|
200
|
+
city: chance.pickone(cities),
|
|
201
|
+
country: chance.pickone(countries),
|
|
202
|
+
},
|
|
203
|
+
};
|
|
204
|
+
};
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
export default config;
|