@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.
Files changed (104) hide show
  1. package/README.md +7 -7
  2. package/dungeons/technical/ad-spend-schema.json +128 -0
  3. package/dungeons/technical/ad-spend.js +116 -0
  4. package/dungeons/technical/anonymous-users-schema.json +92 -0
  5. package/dungeons/technical/anonymous-users.js +102 -0
  6. package/dungeons/technical/array-of-object-lookup-schema.json +191 -0
  7. package/dungeons/{array-of-object-lookup.js → technical/array-of-object-lookup.js} +12 -5
  8. package/dungeons/technical/experiments-schema.json +203 -0
  9. package/dungeons/technical/experiments.js +168 -0
  10. package/dungeons/technical/foobar-schema.json +362 -0
  11. package/dungeons/{foobar.js → technical/foobar.js} +3 -3
  12. package/dungeons/technical/group-analytics-schema.json +241 -0
  13. package/dungeons/technical/group-analytics.js +151 -0
  14. package/dungeons/technical/mirror-strategies-schema.json +84 -0
  15. package/dungeons/technical/mirror-strategies.js +126 -0
  16. package/dungeons/technical/nested-objects-schema.json +145 -0
  17. package/dungeons/technical/nested-objects.js +207 -0
  18. package/dungeons/technical/retention-cadence-schema.json +37 -0
  19. package/dungeons/{retention-cadence.js → technical/retention-cadence.js} +2 -3
  20. package/dungeons/technical/sanity-schema.json +185 -0
  21. package/dungeons/{sanity.js → technical/sanity.js} +4 -2
  22. package/dungeons/technical/scale-test-schema.json +70 -0
  23. package/dungeons/technical/scale-test.js +90 -0
  24. package/dungeons/technical/scd-schema.json +467 -0
  25. package/dungeons/{scd.js → technical/scd.js} +20 -10
  26. package/dungeons/technical/simple-schema.json +362 -0
  27. package/dungeons/{simple.js → technical/simple.js} +7 -7
  28. package/dungeons/technical/simplest-schema.json +724 -0
  29. package/dungeons/{simplest.js → technical/simplest.js} +7 -7
  30. package/dungeons/technical/text-generation-schema.json +1062 -0
  31. package/dungeons/{text-generation.js → technical/text-generation.js} +24 -11
  32. package/dungeons/vertical/community-schema.json +579 -0
  33. package/dungeons/vertical/community.js +794 -0
  34. package/dungeons/vertical/devtools-schema.json +601 -0
  35. package/dungeons/vertical/devtools.js +843 -0
  36. package/dungeons/vertical/ecommerce-schema.json +604 -0
  37. package/dungeons/{ecommerce.js → vertical/ecommerce.js} +43 -5
  38. package/dungeons/vertical/education-schema.json +5686 -0
  39. package/dungeons/{education.js → vertical/education.js} +70 -39
  40. package/dungeons/vertical/fintech-schema.json +630 -0
  41. package/dungeons/{fintech.js → vertical/fintech.js} +107 -56
  42. package/dungeons/vertical/fitness-schema.json +530 -0
  43. package/dungeons/vertical/fitness.js +808 -0
  44. package/dungeons/vertical/food-delivery-schema.json +36728 -0
  45. package/dungeons/{food.js → vertical/food-delivery.js} +67 -26
  46. package/dungeons/vertical/gaming-schema.json +438 -0
  47. package/dungeons/{gaming.js → vertical/gaming.js} +29 -16
  48. package/dungeons/vertical/healthcare-schema.json +549 -0
  49. package/dungeons/vertical/healthcare.js +775 -0
  50. package/dungeons/vertical/insurance-application-schema.json +485 -0
  51. package/dungeons/{insurance-application.js → vertical/insurance-application.js} +41 -31
  52. package/dungeons/vertical/logistics-schema.json +574 -0
  53. package/dungeons/vertical/logistics.js +799 -0
  54. package/dungeons/vertical/marketplace-schema.json +533 -0
  55. package/dungeons/vertical/marketplace.js +850 -0
  56. package/dungeons/vertical/media-schema.json +4749 -0
  57. package/dungeons/{media.js → vertical/media.js} +103 -19
  58. package/dungeons/vertical/rpg-schema.json +2491 -0
  59. package/dungeons/{rpg.js → vertical/rpg.js} +87 -60
  60. package/dungeons/vertical/sass-schema.json +3128 -0
  61. package/dungeons/{sass.js → vertical/sass.js} +65 -26
  62. package/dungeons/vertical/social-schema.json +620 -0
  63. package/dungeons/{social.js → vertical/social.js} +76 -17
  64. package/dungeons/vertical/travel-schema.json +580 -0
  65. package/dungeons/vertical/travel.js +742 -0
  66. package/index.js +20 -16
  67. package/lib/core/config-validator.js +286 -1
  68. package/lib/core/context.js +16 -5
  69. package/lib/core/storage.js +2 -0
  70. package/lib/generators/adspend.js +3 -3
  71. package/lib/generators/events.js +141 -14
  72. package/lib/generators/funnels.js +67 -7
  73. package/lib/generators/text.js +2 -2
  74. package/lib/orchestrators/user-loop.js +444 -22
  75. package/lib/templates/soup-presets.js +2 -2
  76. package/lib/utils/utils.js +4 -5
  77. package/package.json +4 -3
  78. package/scripts/dungeon-to-json.mjs +163 -0
  79. package/scripts/extract-dungeon-schema.mjs +343 -0
  80. package/scripts/json-to-dungeon.mjs +200 -0
  81. package/scripts/run-dungeon.mjs +76 -0
  82. package/scripts/run-many.mjs +118 -0
  83. package/scripts/verify-runner.mjs +42 -0
  84. package/types.d.ts +316 -1
  85. package/dungeons/array-of-object-lookup-schema.json +0 -327
  86. package/dungeons/ecommerce-schema.json +0 -462
  87. package/dungeons/education-schema.json +0 -2409
  88. package/dungeons/fintech-schema.json +0 -14034
  89. package/dungeons/foobar-schema.json +0 -403
  90. package/dungeons/food-delivery-schema.json +0 -192
  91. package/dungeons/food-delivery.js +0 -602
  92. package/dungeons/food-schema.json +0 -1152
  93. package/dungeons/gaming-schema.json +0 -1270
  94. package/dungeons/insurance-application-schema.json +0 -204
  95. package/dungeons/media-schema.json +0 -906
  96. package/dungeons/retention-cadence-schema.json +0 -78
  97. package/dungeons/rpg-schema.json +0 -4526
  98. package/dungeons/sanity-schema.json +0 -255
  99. package/dungeons/sass-schema.json +0 -1291
  100. package/dungeons/scd-schema.json +0 -919
  101. package/dungeons/simple-schema.json +0 -608
  102. package/dungeons/simplest-schema.json +0 -1418
  103. package/dungeons/social-schema.json +0 -1118
  104. 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;