@gurulu/cli 0.4.7 → 1.0.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.
Files changed (190) hide show
  1. package/LICENSE +92 -0
  2. package/README.md +35 -106
  3. package/dist/bin.d.ts +3 -0
  4. package/dist/bin.d.ts.map +1 -0
  5. package/dist/bin.js +25751 -0
  6. package/dist/commands/auth.d.ts +23 -20
  7. package/dist/commands/auth.d.ts.map +1 -0
  8. package/dist/commands/doctor.d.ts +20 -6
  9. package/dist/commands/doctor.d.ts.map +1 -0
  10. package/dist/commands/init.d.ts +33 -11
  11. package/dist/commands/init.d.ts.map +1 -0
  12. package/dist/commands/pull.d.ts +13 -0
  13. package/dist/commands/pull.d.ts.map +1 -0
  14. package/dist/commands/push.d.ts +40 -0
  15. package/dist/commands/push.d.ts.map +1 -0
  16. package/dist/commands/validate.d.ts +36 -0
  17. package/dist/commands/validate.d.ts.map +1 -0
  18. package/dist/index.d.ts +4 -1
  19. package/dist/index.d.ts.map +1 -0
  20. package/dist/index.js +25326 -876
  21. package/dist/lib/api.d.ts +139 -0
  22. package/dist/lib/api.d.ts.map +1 -0
  23. package/dist/lib/codegen.d.ts +4 -0
  24. package/dist/lib/codegen.d.ts.map +1 -0
  25. package/dist/lib/config.d.ts +43 -0
  26. package/dist/lib/config.d.ts.map +1 -0
  27. package/dist/lib/detect.d.ts +27 -0
  28. package/dist/lib/detect.d.ts.map +1 -0
  29. package/dist/lib/detect.js +106 -0
  30. package/dist/lib/exec-install.d.ts +21 -0
  31. package/dist/lib/exec-install.d.ts.map +1 -0
  32. package/dist/lib/install-plan.d.ts +25 -0
  33. package/dist/lib/install-plan.d.ts.map +1 -0
  34. package/dist/lib/install-plan.js +161 -0
  35. package/package.json +51 -20
  36. package/bin/gurulu.js +0 -2
  37. package/dist/api-client.d.ts +0 -33
  38. package/dist/api-client.js +0 -175
  39. package/dist/commands/add-server.d.ts +0 -9
  40. package/dist/commands/add-server.js +0 -162
  41. package/dist/commands/alerts.d.ts +0 -27
  42. package/dist/commands/alerts.js +0 -309
  43. package/dist/commands/api-keys.d.ts +0 -20
  44. package/dist/commands/api-keys.js +0 -130
  45. package/dist/commands/attribution.d.ts +0 -22
  46. package/dist/commands/attribution.js +0 -111
  47. package/dist/commands/audiences.d.ts +0 -23
  48. package/dist/commands/audiences.js +0 -243
  49. package/dist/commands/audit.d.ts +0 -20
  50. package/dist/commands/audit.js +0 -130
  51. package/dist/commands/auth.js +0 -249
  52. package/dist/commands/chat.d.ts +0 -19
  53. package/dist/commands/chat.js +0 -118
  54. package/dist/commands/config.d.ts +0 -10
  55. package/dist/commands/config.js +0 -92
  56. package/dist/commands/consent.d.ts +0 -27
  57. package/dist/commands/consent.js +0 -233
  58. package/dist/commands/conversion-paths.d.ts +0 -19
  59. package/dist/commands/conversion-paths.js +0 -55
  60. package/dist/commands/db.d.ts +0 -25
  61. package/dist/commands/db.js +0 -330
  62. package/dist/commands/destinations.d.ts +0 -20
  63. package/dist/commands/destinations.js +0 -191
  64. package/dist/commands/doctor.js +0 -360
  65. package/dist/commands/errors.d.ts +0 -27
  66. package/dist/commands/errors.js +0 -121
  67. package/dist/commands/events.d.ts +0 -33
  68. package/dist/commands/events.js +0 -371
  69. package/dist/commands/experiments.d.ts +0 -22
  70. package/dist/commands/experiments.js +0 -264
  71. package/dist/commands/funnels.d.ts +0 -17
  72. package/dist/commands/funnels.js +0 -203
  73. package/dist/commands/goals.d.ts +0 -18
  74. package/dist/commands/goals.js +0 -214
  75. package/dist/commands/heatmap.d.ts +0 -27
  76. package/dist/commands/heatmap.js +0 -112
  77. package/dist/commands/identity.d.ts +0 -29
  78. package/dist/commands/identity.js +0 -328
  79. package/dist/commands/init.js +0 -215
  80. package/dist/commands/insights.d.ts +0 -10
  81. package/dist/commands/insights.js +0 -77
  82. package/dist/commands/install.d.ts +0 -259
  83. package/dist/commands/install.js +0 -1590
  84. package/dist/commands/login.d.ts +0 -20
  85. package/dist/commands/login.js +0 -170
  86. package/dist/commands/logout.d.ts +0 -10
  87. package/dist/commands/logout.js +0 -41
  88. package/dist/commands/playground.d.ts +0 -11
  89. package/dist/commands/playground.js +0 -47
  90. package/dist/commands/releases.d.ts +0 -17
  91. package/dist/commands/releases.js +0 -54
  92. package/dist/commands/replay.d.ts +0 -18
  93. package/dist/commands/replay.js +0 -64
  94. package/dist/commands/secrets.d.ts +0 -19
  95. package/dist/commands/secrets.js +0 -145
  96. package/dist/commands/setup.d.ts +0 -21
  97. package/dist/commands/setup.js +0 -67
  98. package/dist/commands/sites.d.ts +0 -18
  99. package/dist/commands/sites.js +0 -139
  100. package/dist/commands/skad.d.ts +0 -18
  101. package/dist/commands/skad.js +0 -53
  102. package/dist/commands/sourcemap.d.ts +0 -33
  103. package/dist/commands/sourcemap.js +0 -204
  104. package/dist/commands/status.d.ts +0 -7
  105. package/dist/commands/status.js +0 -136
  106. package/dist/commands/upgrade.d.ts +0 -21
  107. package/dist/commands/upgrade.js +0 -183
  108. package/dist/commands/warehouse.d.ts +0 -20
  109. package/dist/commands/warehouse.js +0 -65
  110. package/dist/commands/warehouses.d.ts +0 -17
  111. package/dist/commands/warehouses.js +0 -182
  112. package/dist/commands/watch.d.ts +0 -45
  113. package/dist/commands/watch.js +0 -258
  114. package/dist/commands/whoami.d.ts +0 -9
  115. package/dist/commands/whoami.js +0 -50
  116. package/dist/config.d.ts +0 -75
  117. package/dist/config.js +0 -329
  118. package/dist/frameworks/detect.d.ts +0 -8
  119. package/dist/frameworks/detect.js +0 -458
  120. package/dist/install-intent-proposal.d.ts +0 -99
  121. package/dist/install-intent-proposal.js +0 -202
  122. package/dist/utils/api.d.ts +0 -20
  123. package/dist/utils/api.js +0 -47
  124. package/dist/utils/config.d.ts +0 -13
  125. package/dist/utils/config.js +0 -30
  126. package/dist/utils/confirm.d.ts +0 -17
  127. package/dist/utils/confirm.js +0 -40
  128. package/dist/utils/dry-run.d.ts +0 -20
  129. package/dist/utils/dry-run.js +0 -67
  130. package/dist/utils/from-file.d.ts +0 -9
  131. package/dist/utils/from-file.js +0 -72
  132. package/dist/utils/redact.d.ts +0 -14
  133. package/dist/utils/redact.js +0 -48
  134. package/dist/utils/ui.d.ts +0 -14
  135. package/dist/utils/ui.js +0 -59
  136. package/scripts/.gitkeep +0 -0
  137. package/scripts/README-gurulu-agentic-install.md +0 -114
  138. package/scripts/README-gurulu-scan.md +0 -98
  139. package/scripts/audit-cli-scopes.mjs +0 -204
  140. package/scripts/backfill-tenant-id.mjs +0 -172
  141. package/scripts/backfill-tenant-links.ts +0 -252
  142. package/scripts/backup-clickhouse.sh +0 -27
  143. package/scripts/backup-postgres.sh +0 -19
  144. package/scripts/bootstrap-runtime-schema.mjs +0 -87
  145. package/scripts/bootstrap-stripe.mjs +0 -158
  146. package/scripts/gurulu-agentic-install.lib.cjs +0 -762
  147. package/scripts/gurulu-agentic-install.mjs +0 -623
  148. package/scripts/gurulu-scan.lib.cjs +0 -1509
  149. package/scripts/gurulu-scan.mjs +0 -91
  150. package/scripts/gurulu-verify-install.lib.cjs +0 -334
  151. package/scripts/gurulu-verify-install.mjs +0 -59
  152. package/scripts/init-ssl.sh +0 -26
  153. package/scripts/migrate-flow-graph-enums.sh +0 -86
  154. package/scripts/monitor-disk.sh +0 -24
  155. package/scripts/patches/astro.patch.cjs +0 -74
  156. package/scripts/patches/auto-instrument/ast-helper.cjs +0 -480
  157. package/scripts/patches/auto-instrument/astro.cjs +0 -273
  158. package/scripts/patches/auto-instrument/express.cjs +0 -383
  159. package/scripts/patches/auto-instrument/fastify.cjs +0 -262
  160. package/scripts/patches/auto-instrument/hono.cjs +0 -392
  161. package/scripts/patches/auto-instrument/index.cjs +0 -80
  162. package/scripts/patches/auto-instrument/nestjs.cjs +0 -286
  163. package/scripts/patches/auto-instrument/nextjs-app-router.cjs +0 -345
  164. package/scripts/patches/auto-instrument/nextjs-pages.cjs +0 -361
  165. package/scripts/patches/auto-instrument/remix.cjs +0 -168
  166. package/scripts/patches/auto-instrument/sdk-helper-map.cjs +0 -241
  167. package/scripts/patches/auto-instrument/singleton-helper.cjs +0 -193
  168. package/scripts/patches/auto-instrument/sveltekit.cjs +0 -161
  169. package/scripts/patches/auto-instrument/vite-react.cjs +0 -37
  170. package/scripts/patches/auto-instrument/vue.cjs +0 -196
  171. package/scripts/patches/express.patch.cjs +0 -99
  172. package/scripts/patches/fastify.patch.cjs +0 -108
  173. package/scripts/patches/index.cjs +0 -300
  174. package/scripts/patches/nestjs.patch.cjs +0 -112
  175. package/scripts/patches/nextjs-app-router.patch.cjs +0 -97
  176. package/scripts/patches/nextjs-pages.patch.cjs +0 -97
  177. package/scripts/patches/remix.patch.cjs +0 -75
  178. package/scripts/patches/sveltekit.patch.cjs +0 -72
  179. package/scripts/patches/vite-react.patch.cjs +0 -73
  180. package/scripts/patches/vue.patch.cjs +0 -82
  181. package/scripts/renew-ssl.sh +0 -14
  182. package/scripts/resolve-migration.sh +0 -23
  183. package/scripts/seed-cli-dev-keys.mjs +0 -130
  184. package/scripts/seed-test-data.mjs +0 -391
  185. package/scripts/spike-browserless.ts +0 -65
  186. package/scripts/tenant-pivot-consistency-check.mjs +0 -205
  187. package/scripts/tenant-pivot-phase-3-cleanup.lib.cjs +0 -258
  188. package/scripts/tenant-pivot-phase-3-cleanup.mjs +0 -98
  189. package/scripts/test-identity-resolution.ts +0 -804
  190. package/scripts/validate-gurulu-schemas.mjs +0 -79
@@ -1,391 +0,0 @@
1
- import { randomUUID } from 'node:crypto';
2
-
3
- const CLICKHOUSE_URL = process.env.CLICKHOUSE_URL || 'http://localhost:8123';
4
- const SITE_ID = 'cmnuiqwnt0003dfu9tqt81l9d';
5
- const NUM_USERS = 25;
6
- const DAYS_BACK = 7;
7
-
8
- // ---------------------------------------------------------------------------
9
- // Reference data
10
- // ---------------------------------------------------------------------------
11
-
12
- const PAGES = [
13
- { url: '/', title: 'Gurulu - Analytics Platform' },
14
- { url: '/pricing', title: 'Pricing - Gurulu' },
15
- { url: '/blog', title: 'Blog - Gurulu' },
16
- { url: '/blog/getting-started', title: 'Getting Started with Gurulu' },
17
- { url: '/blog/privacy-first-analytics', title: 'Privacy-First Analytics' },
18
- { url: '/docs', title: 'Documentation - Gurulu' },
19
- { url: '/docs/installation', title: 'Installation Guide - Gurulu' },
20
- { url: '/docs/api-reference', title: 'API Reference - Gurulu' },
21
- { url: '/login', title: 'Login - Gurulu' },
22
- { url: '/dashboard', title: 'Dashboard - Gurulu' },
23
- { url: '/features', title: 'Features - Gurulu' },
24
- { url: '/about', title: 'About Us - Gurulu' },
25
- { url: '/contact', title: 'Contact - Gurulu' },
26
- ];
27
-
28
- const EVENT_TYPES = ['pageview', 'click', 'scroll_depth', 'custom'];
29
- const CUSTOM_EVENT_NAMES = [
30
- 'signup',
31
- 'purchase',
32
- 'download',
33
- 'contact_form',
34
- 'newsletter_subscribe',
35
- ];
36
-
37
- const UTM_SOURCES = [
38
- { source: '', medium: '', campaign: '' }, // direct
39
- { source: '', medium: '', campaign: '' }, // direct (weighted)
40
- { source: 'google', medium: 'organic', campaign: '' },
41
- { source: 'google', medium: 'cpc', campaign: 'spring_2026' },
42
- { source: 'google', medium: 'cpc', campaign: 'brand_awareness' },
43
- { source: 'twitter', medium: 'social', campaign: 'launch_tweet' },
44
- { source: 'linkedin', medium: 'social', campaign: 'b2b_outreach' },
45
- { source: 'newsletter', medium: 'email', campaign: 'weekly_digest' },
46
- { source: 'newsletter', medium: 'email', campaign: 'product_update' },
47
- { source: 'producthunt', medium: 'referral', campaign: '' },
48
- { source: 'reddit', medium: 'social', campaign: '' },
49
- ];
50
-
51
- const REFERRERS = [
52
- '',
53
- '',
54
- 'https://www.google.com/',
55
- 'https://www.google.com/search?q=analytics+platform',
56
- 'https://twitter.com/',
57
- 'https://www.linkedin.com/feed/',
58
- 'https://news.ycombinator.com/',
59
- 'https://www.reddit.com/r/analytics/',
60
- 'https://www.producthunt.com/',
61
- 'https://mail.google.com/',
62
- ];
63
-
64
- const DEVICES = [
65
- { type: 'desktop', width: 1920, height: 1080 },
66
- { type: 'desktop', width: 2560, height: 1440 },
67
- { type: 'desktop', width: 1440, height: 900 },
68
- { type: 'mobile', width: 390, height: 844 },
69
- { type: 'mobile', width: 412, height: 915 },
70
- { type: 'mobile', width: 375, height: 812 },
71
- { type: 'tablet', width: 820, height: 1180 },
72
- { type: 'tablet', width: 1024, height: 1366 },
73
- ];
74
-
75
- const BROWSERS = ['Chrome', 'Firefox', 'Safari', 'Edge', 'Arc'];
76
- const OSES = ['Windows', 'macOS', 'Linux', 'iOS', 'Android'];
77
- const COUNTRIES = ['US', 'TR', 'GB', 'DE', 'FR', 'NL', 'CA', 'AU', 'JP', 'BR'];
78
- const CITIES_BY_COUNTRY = {
79
- US: ['New York', 'San Francisco', 'Chicago', 'Austin', 'Seattle'],
80
- TR: ['Istanbul', 'Ankara', 'Izmir'],
81
- GB: ['London', 'Manchester', 'Edinburgh'],
82
- DE: ['Berlin', 'Munich', 'Hamburg'],
83
- FR: ['Paris', 'Lyon', 'Marseille'],
84
- NL: ['Amsterdam', 'Rotterdam'],
85
- CA: ['Toronto', 'Vancouver'],
86
- AU: ['Sydney', 'Melbourne'],
87
- JP: ['Tokyo', 'Osaka'],
88
- BR: ['Sao Paulo', 'Rio de Janeiro'],
89
- };
90
- const LANGUAGES = ['en-US', 'tr-TR', 'en-GB', 'de-DE', 'fr-FR', 'ja-JP', 'pt-BR'];
91
-
92
- // ---------------------------------------------------------------------------
93
- // Helpers
94
- // ---------------------------------------------------------------------------
95
-
96
- function pick(arr) {
97
- return arr[Math.floor(Math.random() * arr.length)];
98
- }
99
-
100
- function weightedPick(arr, weights) {
101
- const total = weights.reduce((a, b) => a + b, 0);
102
- let r = Math.random() * total;
103
- for (let i = 0; i < arr.length; i++) {
104
- r -= weights[i];
105
- if (r <= 0) return arr[i];
106
- }
107
- return arr[arr.length - 1];
108
- }
109
-
110
- function randInt(min, max) {
111
- return Math.floor(Math.random() * (max - min + 1)) + min;
112
- }
113
-
114
- /** Format Date to ClickHouse DateTime64(3) — no T, no Z, space-separated. */
115
- function formatCH(date) {
116
- const y = date.getUTCFullYear();
117
- const mo = String(date.getUTCMonth() + 1).padStart(2, '0');
118
- const d = String(date.getUTCDate()).padStart(2, '0');
119
- const h = String(date.getUTCHours()).padStart(2, '0');
120
- const mi = String(date.getUTCMinutes()).padStart(2, '0');
121
- const s = String(date.getUTCSeconds()).padStart(2, '0');
122
- const ms = String(date.getUTCMilliseconds()).padStart(3, '0');
123
- return `${y}-${mo}-${d} ${h}:${mi}:${s}.${ms}`;
124
- }
125
-
126
- // ---------------------------------------------------------------------------
127
- // User profile generation
128
- // ---------------------------------------------------------------------------
129
-
130
- function createUserProfile(index) {
131
- const device = pick(DEVICES);
132
- const country = pick(COUNTRIES);
133
- const city = pick(CITIES_BY_COUNTRY[country]);
134
- const os =
135
- device.type === 'mobile'
136
- ? pick(['iOS', 'Android'])
137
- : device.type === 'tablet'
138
- ? pick(['iOS', 'Android'])
139
- : pick(['Windows', 'macOS', 'Linux']);
140
- const browser =
141
- os === 'iOS' || os === 'macOS'
142
- ? pick(['Safari', 'Chrome', 'Arc'])
143
- : pick(['Chrome', 'Firefox', 'Edge']);
144
-
145
- return {
146
- anonymous_id: randomUUID(),
147
- canonical_id: index < 6 ? `user_${randomUUID().slice(0, 8)}` : '', // ~25% identified
148
- device,
149
- browser,
150
- os,
151
- country,
152
- city,
153
- language: pick(LANGUAGES),
154
- };
155
- }
156
-
157
- // ---------------------------------------------------------------------------
158
- // Event generation
159
- // ---------------------------------------------------------------------------
160
-
161
- function generateSessionEvents(user, sessionStart) {
162
- const events = [];
163
- const session_id = randomUUID();
164
- const utm = pick(UTM_SOURCES);
165
- const referrer = utm.source
166
- ? pick(REFERRERS.filter((r) => r !== '')) || ''
167
- : pick(['', '']);
168
-
169
- // Each session has 1–12 pageviews + some interactions
170
- const pageCount = randInt(1, 12);
171
- let cursor = new Date(sessionStart);
172
-
173
- for (let p = 0; p < pageCount; p++) {
174
- const page = p === 0 ? pick(PAGES.slice(0, 5)) : pick(PAGES); // entry pages biased to top-level
175
-
176
- // Pageview event
177
- events.push(makeEvent(user, session_id, 'pageview', 'pageview', page, cursor, utm, referrer));
178
-
179
- // Advance time 5–60s
180
- cursor = new Date(cursor.getTime() + randInt(5000, 60000));
181
-
182
- // Possibly add scroll depth (60% chance)
183
- if (Math.random() < 0.6) {
184
- const depth = weightedPick([25, 50, 75, 100], [3, 4, 2, 1]);
185
- events.push(
186
- makeEvent(user, session_id, 'scroll_depth', 'scroll_depth', page, cursor, utm, referrer, {
187
- depth,
188
- }),
189
- );
190
- cursor = new Date(cursor.getTime() + randInt(2000, 10000));
191
- }
192
-
193
- // Possibly add a click (40% chance)
194
- if (Math.random() < 0.4) {
195
- const clickTargets = ['cta_button', 'nav_link', 'pricing_toggle', 'faq_accordion', 'hero_cta'];
196
- events.push(
197
- makeEvent(user, session_id, 'click', 'click', page, cursor, utm, referrer, {
198
- target: pick(clickTargets),
199
- }),
200
- );
201
- cursor = new Date(cursor.getTime() + randInt(1000, 5000));
202
- }
203
-
204
- // On certain pages, add custom events
205
- if (page.url === '/contact' && Math.random() < 0.3) {
206
- events.push(
207
- makeEvent(user, session_id, 'custom', 'contact_form', page, cursor, utm, referrer, {
208
- form_type: 'inquiry',
209
- }),
210
- );
211
- cursor = new Date(cursor.getTime() + randInt(2000, 5000));
212
- }
213
-
214
- if (page.url === '/pricing' && Math.random() < 0.15) {
215
- events.push(
216
- makeEvent(user, session_id, 'custom', 'purchase', page, cursor, utm, referrer, {
217
- plan: pick(['starter', 'pro', 'enterprise']),
218
- value: pick([29, 79, 199]),
219
- }),
220
- );
221
- cursor = new Date(cursor.getTime() + randInt(2000, 5000));
222
- }
223
-
224
- if (page.url === '/login' && Math.random() < 0.4) {
225
- events.push(
226
- makeEvent(user, session_id, 'custom', 'signup', page, cursor, utm, referrer, {
227
- method: pick(['email', 'google', 'github']),
228
- }),
229
- );
230
- cursor = new Date(cursor.getTime() + randInt(2000, 5000));
231
- }
232
-
233
- if (page.url === '/docs' && Math.random() < 0.25) {
234
- events.push(
235
- makeEvent(user, session_id, 'custom', 'download', page, cursor, utm, referrer, {
236
- asset: pick(['sdk_js', 'sdk_react', 'whitepaper']),
237
- }),
238
- );
239
- cursor = new Date(cursor.getTime() + randInt(2000, 5000));
240
- }
241
-
242
- if (page.url === '/blog' && Math.random() < 0.2) {
243
- events.push(
244
- makeEvent(user, session_id, 'custom', 'newsletter_subscribe', page, cursor, utm, referrer, {
245
- source: 'blog_sidebar',
246
- }),
247
- );
248
- cursor = new Date(cursor.getTime() + randInt(2000, 5000));
249
- }
250
-
251
- // Advance between pages 3–90s
252
- cursor = new Date(cursor.getTime() + randInt(3000, 90000));
253
- }
254
-
255
- return events;
256
- }
257
-
258
- function makeEvent(user, session_id, event_type, event_name, page, timestamp, utm, referrer, props = {}) {
259
- return {
260
- site_id: SITE_ID,
261
- event_id: randomUUID(),
262
- session_id,
263
- canonical_id: user.canonical_id,
264
- anonymous_id: user.anonymous_id,
265
- event_type,
266
- event_name,
267
- page_url: `https://gurulu.io${page.url}`,
268
- page_title: page.title,
269
- referrer,
270
- utm_source: utm.source,
271
- utm_medium: utm.medium,
272
- utm_campaign: utm.campaign,
273
- utm_term: '',
274
- utm_content: '',
275
- device_type: user.device.type,
276
- browser: user.browser,
277
- os: user.os,
278
- country: user.country,
279
- city: user.city,
280
- language: user.language,
281
- screen_width: user.device.width,
282
- screen_height: user.device.height,
283
- properties: JSON.stringify(props),
284
- consent_level: pick(['necessary', 'analytics', 'full']),
285
- timestamp: formatCH(timestamp),
286
- received_at: formatCH(new Date(timestamp.getTime() + randInt(50, 500))),
287
- is_bot: 0,
288
- };
289
- }
290
-
291
- // ---------------------------------------------------------------------------
292
- // Main
293
- // ---------------------------------------------------------------------------
294
-
295
- async function main() {
296
- console.log('Generating test events...');
297
-
298
- const now = Date.now();
299
- const users = Array.from({ length: NUM_USERS }, (_, i) => createUserProfile(i));
300
- const allEvents = [];
301
-
302
- for (const user of users) {
303
- // Each user has 1–5 sessions across the 7 days
304
- const sessionCount = randInt(1, 5);
305
- for (let s = 0; s < sessionCount; s++) {
306
- const dayOffset = randInt(0, DAYS_BACK - 1);
307
- const hourOffset = randInt(6, 23); // bias toward waking hours
308
- const minuteOffset = randInt(0, 59);
309
-
310
- const sessionStart = new Date(now - dayOffset * 86400000);
311
- sessionStart.setUTCHours(hourOffset, minuteOffset, 0, 0);
312
-
313
- const events = generateSessionEvents(user, sessionStart);
314
- allEvents.push(...events);
315
- }
316
- }
317
-
318
- // Sort by timestamp
319
- allEvents.sort((a, b) => a.timestamp.localeCompare(b.timestamp));
320
-
321
- console.log(`Generated ${allEvents.length} events from ${NUM_USERS} users.`);
322
-
323
- // Insert into ClickHouse in batches
324
- const BATCH_SIZE = 200;
325
- let inserted = 0;
326
-
327
- for (let i = 0; i < allEvents.length; i += BATCH_SIZE) {
328
- const batch = allEvents.slice(i, i + BATCH_SIZE);
329
- const body = batch.map((e) => JSON.stringify(e)).join('\n');
330
-
331
- const res = await fetch(
332
- `${CLICKHOUSE_URL}/?database=gurulu&query=${encodeURIComponent('INSERT INTO events_raw FORMAT JSONEachRow')}`,
333
- {
334
- method: 'POST',
335
- headers: { 'Content-Type': 'application/json' },
336
- body,
337
- },
338
- );
339
-
340
- if (!res.ok) {
341
- const text = await res.text();
342
- console.error(`ClickHouse error (batch ${Math.floor(i / BATCH_SIZE) + 1}):`, text);
343
- process.exit(1);
344
- }
345
-
346
- inserted += batch.length;
347
- process.stdout.write(`\rInserted ${inserted}/${allEvents.length} events`);
348
- }
349
-
350
- console.log('\n');
351
-
352
- // Summary
353
- const eventTypes = {};
354
- const utmSources = {};
355
- const deviceTypes = {};
356
- const identifiedCount = allEvents.filter((e) => e.canonical_id).length;
357
- const uniqueSessions = new Set(allEvents.map((e) => e.session_id)).size;
358
-
359
- for (const e of allEvents) {
360
- eventTypes[e.event_name] = (eventTypes[e.event_name] || 0) + 1;
361
- if (e.utm_source) utmSources[e.utm_source] = (utmSources[e.utm_source] || 0) + 1;
362
- else utmSources['(direct)'] = (utmSources['(direct)'] || 0) + 1;
363
- deviceTypes[e.device_type] = (deviceTypes[e.device_type] || 0) + 1;
364
- }
365
-
366
- console.log('=== Seed Summary ===');
367
- console.log(`Total events: ${allEvents.length}`);
368
- console.log(`Unique users: ${NUM_USERS}`);
369
- console.log(`Unique sessions: ${uniqueSessions}`);
370
- console.log(`Identified: ${identifiedCount} events (${((identifiedCount / allEvents.length) * 100).toFixed(1)}%)`);
371
- console.log(`Date range: ${allEvents[0].timestamp} → ${allEvents[allEvents.length - 1].timestamp}`);
372
- console.log('\nEvent types:');
373
- for (const [k, v] of Object.entries(eventTypes).sort((a, b) => b[1] - a[1])) {
374
- console.log(` ${k.padEnd(24)} ${v}`);
375
- }
376
- console.log('\nUTM sources:');
377
- for (const [k, v] of Object.entries(utmSources).sort((a, b) => b[1] - a[1])) {
378
- console.log(` ${k.padEnd(24)} ${v}`);
379
- }
380
- console.log('\nDevice types:');
381
- for (const [k, v] of Object.entries(deviceTypes).sort((a, b) => b[1] - a[1])) {
382
- console.log(` ${k.padEnd(24)} ${v}`);
383
- }
384
-
385
- console.log('\nDone!');
386
- }
387
-
388
- main().catch((err) => {
389
- console.error('Fatal error:', err);
390
- process.exit(1);
391
- });
@@ -1,65 +0,0 @@
1
- /**
2
- * Phase 11 Track B — B1 spike script
3
- *
4
- * Run with: npx tsx scripts/spike-browserless.ts
5
- *
6
- * Prereq: `docker compose up -d browserless`. Connects to the local
7
- * browserless service, loads https://example.com, captures a screenshot
8
- * to /tmp/spike-browserless.png, then closes the session.
9
- */
10
-
11
- import { chromium } from 'playwright-core';
12
- import { statSync } from 'node:fs';
13
- import {
14
- getBrowserlessBaseUrl,
15
- getBrowserlessToken,
16
- spawnRemoteSession,
17
- } from '../src/lib/playground/browserless';
18
-
19
- const SCREENSHOT_PATH = '/tmp/spike-browserless.png';
20
- const TARGET_URL = 'https://example.com';
21
- const SESSION_ID = 'spike-test';
22
-
23
- async function main() {
24
- const startedAt = Date.now();
25
- console.log('[spike] browserless base URL:', getBrowserlessBaseUrl());
26
- console.log('[spike] using token prefix :', getBrowserlessToken().slice(0, 6) + '…');
27
-
28
- console.log('[spike] spawning remote session…');
29
- const session = await spawnRemoteSession({
30
- url: TARGET_URL,
31
- sessionId: SESSION_ID,
32
- });
33
- console.log('[spike] session ready:', {
34
- sessionId: session.sessionId,
35
- wsEndpoint: session.wsEndpoint,
36
- startedAt: session.startedAt.toISOString(),
37
- });
38
-
39
- // Reconnect with the returned CDP endpoint to prove the contract: the
40
- // caller can take the endpoint and drive Playwright on their own.
41
- const browser = await chromium.connectOverCDP(session.wsEndpoint);
42
- try {
43
- const context = browser.contexts()[0] ?? (await browser.newContext());
44
- const page = context.pages()[0] ?? (await context.newPage());
45
- console.log('[spike] capturing screenshot →', SCREENSHOT_PATH);
46
- await page.screenshot({ path: SCREENSHOT_PATH, fullPage: true });
47
- } finally {
48
- await browser.close().catch(() => {});
49
- }
50
-
51
- await session.close();
52
- const size = statSync(SCREENSHOT_PATH).size;
53
- const durationMs = Date.now() - startedAt;
54
- if (size === 0) {
55
- throw new Error('Screenshot file is empty');
56
- }
57
- console.log(
58
- `[spike] ✓ success — screenshot ${size} bytes at ${SCREENSHOT_PATH} (${durationMs}ms)`,
59
- );
60
- }
61
-
62
- main().catch((err) => {
63
- console.error('[spike] ✗ failed:', err);
64
- process.exit(1);
65
- });
@@ -1,205 +0,0 @@
1
- #!/usr/bin/env node
2
- /**
3
- * Phase 16 B2 — Tenant pivot consistency check.
4
- *
5
- * For a given tenant, runs the analytics read path twice:
6
- * 1. LEGACY: WHERE site_id IN (<sites from Prisma>)
7
- * 2. TENANT: WHERE tenant_id = <tenantId>
8
- * and diffs the row counts + a handful of key aggregates. Prints a report.
9
- *
10
- * Operators use this to validate that the tenant_id column on `events_raw` is
11
- * fully backfilled and consistent with the Prisma site→tenant mapping before
12
- * flipping `ANALYTICS_TENANT_READ_PATH=1` in prod.
13
- *
14
- * Usage:
15
- * node scripts/tenant-pivot-consistency-check.mjs --tenant-id <id> \
16
- * [--from <iso>] [--to <iso>]
17
- *
18
- * Environment:
19
- * CLICKHOUSE_URL (default http://localhost:8123)
20
- * CLICKHOUSE_DATABASE (default gurulu)
21
- *
22
- * Safe to run in prod — read-only, no side effects.
23
- */
24
-
25
- // ---------------------------------------------------------------------------
26
- // CLI arg parsing
27
- // ---------------------------------------------------------------------------
28
-
29
- function parseArgs(argv) {
30
- const args = {};
31
- for (let i = 2; i < argv.length; i++) {
32
- const a = argv[i];
33
- if (a === '--tenant-id') args.tenantId = argv[++i];
34
- else if (a === '--from') args.from = argv[++i];
35
- else if (a === '--to') args.to = argv[++i];
36
- else if (a === '--help' || a === '-h') args.help = true;
37
- }
38
- return args;
39
- }
40
-
41
- function printHelp() {
42
- console.log(
43
- `\nUsage: node scripts/tenant-pivot-consistency-check.mjs --tenant-id <id> [--from <iso>] [--to <iso>]\n\n` +
44
- ` --tenant-id Required. The tenant id to audit.\n` +
45
- ` --from Optional. ISO timestamp for lower bound (default: 7d ago).\n` +
46
- ` --to Optional. ISO timestamp for upper bound (default: now).\n`
47
- );
48
- }
49
-
50
- // ---------------------------------------------------------------------------
51
- // Diff helper — exported for unit tests.
52
- // ---------------------------------------------------------------------------
53
-
54
- /**
55
- * Compare two aggregate result bags and return a structured diff.
56
- *
57
- * Each bag is a flat object like:
58
- * { events: 1234, visitors: 56, sessions: 78, pageviews: 910 }
59
- *
60
- * The diff returns:
61
- * { ok: boolean, mismatches: Array<{key, legacy, tenant, absDelta, relDelta}> }
62
- *
63
- * A key is considered a mismatch when abs(legacy - tenant) > absTolerance AND
64
- * abs((legacy - tenant) / legacy) > relTolerance (if legacy != 0). This keeps
65
- * the check resilient to harmless rounding in floating-point aggregates.
66
- */
67
- export function diffAggregates(legacy, tenant, opts = {}) {
68
- const absTolerance = opts.absTolerance ?? 0;
69
- const relTolerance = opts.relTolerance ?? 0;
70
-
71
- const keys = new Set([...Object.keys(legacy || {}), ...Object.keys(tenant || {})]);
72
- const mismatches = [];
73
-
74
- for (const key of keys) {
75
- const l = Number(legacy?.[key] ?? 0);
76
- const t = Number(tenant?.[key] ?? 0);
77
- const absDelta = Math.abs(l - t);
78
- const relDelta = l !== 0 ? Math.abs((l - t) / l) : absDelta > 0 ? 1 : 0;
79
-
80
- const overAbs = absDelta > absTolerance;
81
- const overRel = relDelta > relTolerance;
82
- if (overAbs && overRel) {
83
- mismatches.push({ key, legacy: l, tenant: t, absDelta, relDelta });
84
- }
85
- }
86
-
87
- return { ok: mismatches.length === 0, mismatches };
88
- }
89
-
90
- // ---------------------------------------------------------------------------
91
- // ClickHouse helper (mirrors src/lib/clickhouse.ts — kept inline so this
92
- // script is `node`-runnable without a TS toolchain).
93
- // ---------------------------------------------------------------------------
94
-
95
- async function clickhouseQuery(query, params = {}) {
96
- const CLICKHOUSE_URL = process.env.CLICKHOUSE_URL || 'http://localhost:8123';
97
- const CLICKHOUSE_DB = process.env.CLICKHOUSE_DATABASE || 'gurulu';
98
-
99
- const url = new URL(CLICKHOUSE_URL);
100
- url.searchParams.set('database', CLICKHOUSE_DB);
101
- url.searchParams.set('default_format', 'JSON');
102
- for (const [k, v] of Object.entries(params)) {
103
- let s = String(v);
104
- if (/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}.*Z$/.test(s)) {
105
- s = s.replace('T', ' ').replace('Z', '');
106
- }
107
- url.searchParams.set(`param_${k}`, s);
108
- }
109
-
110
- const res = await fetch(url.toString(), {
111
- method: 'POST',
112
- body: query,
113
- headers: { 'Content-Type': 'text/plain' },
114
- });
115
- if (!res.ok) throw new Error(`ClickHouse error ${res.status}: ${await res.text()}`);
116
- return res.json();
117
- }
118
-
119
- function escapeSiteId(s) {
120
- const cleaned = String(s).replace(/\\/g, '\\\\').replace(/'/g, "\\'");
121
- return `'${cleaned}'`;
122
- }
123
-
124
- // ---------------------------------------------------------------------------
125
- // Main.
126
- // ---------------------------------------------------------------------------
127
-
128
- async function main() {
129
- const args = parseArgs(process.argv);
130
- if (args.help || !args.tenantId) {
131
- printHelp();
132
- if (!args.tenantId) process.exit(1);
133
- return;
134
- }
135
-
136
- const to = args.to || new Date().toISOString();
137
- const from = args.from || new Date(Date.now() - 7 * 24 * 60 * 60 * 1000).toISOString();
138
-
139
- console.log(`[consistency-check] tenant=${args.tenantId} window=${from}..${to}`);
140
-
141
- // Resolve site ids via Prisma — lazy-loaded so the test importing
142
- // `diffAggregates` doesn't have to pay the Prisma client cost.
143
- const { PrismaClient } = await import('@prisma/client');
144
- const prisma = new PrismaClient();
145
- const sites = await prisma.site.findMany({
146
- where: { tenantId: args.tenantId },
147
- select: { id: true },
148
- });
149
- await prisma.$disconnect();
150
-
151
- if (sites.length === 0) {
152
- console.error(`[consistency-check] no sites found for tenant ${args.tenantId}`);
153
- process.exit(2);
154
- }
155
- const siteIds = sites.map((s) => s.id);
156
- const inList = siteIds.map(escapeSiteId).join(',');
157
-
158
- const AGG = `
159
- SELECT
160
- count() AS events,
161
- uniqExact(anonymous_id) AS visitors,
162
- uniqExact(session_id) AS sessions,
163
- countIf(event_type = 'pageview') AS pageviews
164
- `;
165
- const WINDOW = `timestamp >= {from:DateTime64(3)} AND timestamp <= {to:DateTime64(3)} AND is_bot = 0`;
166
-
167
- const [legacyRes, tenantRes] = await Promise.all([
168
- clickhouseQuery(
169
- `${AGG} FROM events_raw WHERE site_id IN (${inList}) AND ${WINDOW}`,
170
- { from, to }
171
- ),
172
- clickhouseQuery(
173
- `${AGG} FROM events_raw WHERE tenant_id = {tenantId:String} AND ${WINDOW}`,
174
- { from, to, tenantId: args.tenantId }
175
- ),
176
- ]);
177
-
178
- const legacy = legacyRes.data[0] || {};
179
- const tenant = tenantRes.data[0] || {};
180
-
181
- console.log('[consistency-check] legacy path:', legacy);
182
- console.log('[consistency-check] tenant path:', tenant);
183
-
184
- const diff = diffAggregates(legacy, tenant);
185
- if (diff.ok) {
186
- console.log('[consistency-check] OK — both paths agree.');
187
- process.exit(0);
188
- }
189
- console.error('[consistency-check] MISMATCH:');
190
- for (const m of diff.mismatches) {
191
- console.error(
192
- ` ${m.key}: legacy=${m.legacy} tenant=${m.tenant} (Δ=${m.absDelta}, rel=${(m.relDelta * 100).toFixed(2)}%)`
193
- );
194
- }
195
- process.exit(3);
196
- }
197
-
198
- // Only run main() when invoked directly, never when imported by tests.
199
- const isMain = import.meta.url === `file://${process.argv[1]}`;
200
- if (isMain) {
201
- main().catch((err) => {
202
- console.error('[consistency-check] fatal:', err);
203
- process.exit(10);
204
- });
205
- }