@gurulu/cli 0.4.6 → 1.0.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 (180) 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 +25410 -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 +25 -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 +24985 -853
  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/package.json +40 -20
  28. package/bin/gurulu.js +0 -2
  29. package/dist/api-client.d.ts +0 -33
  30. package/dist/api-client.js +0 -175
  31. package/dist/commands/add-server.d.ts +0 -9
  32. package/dist/commands/add-server.js +0 -162
  33. package/dist/commands/alerts.d.ts +0 -27
  34. package/dist/commands/alerts.js +0 -309
  35. package/dist/commands/api-keys.d.ts +0 -20
  36. package/dist/commands/api-keys.js +0 -130
  37. package/dist/commands/attribution.d.ts +0 -22
  38. package/dist/commands/attribution.js +0 -111
  39. package/dist/commands/audiences.d.ts +0 -23
  40. package/dist/commands/audiences.js +0 -243
  41. package/dist/commands/audit.d.ts +0 -20
  42. package/dist/commands/audit.js +0 -130
  43. package/dist/commands/auth.js +0 -249
  44. package/dist/commands/chat.d.ts +0 -18
  45. package/dist/commands/chat.js +0 -117
  46. package/dist/commands/config.d.ts +0 -10
  47. package/dist/commands/config.js +0 -92
  48. package/dist/commands/consent.d.ts +0 -27
  49. package/dist/commands/consent.js +0 -233
  50. package/dist/commands/conversion-paths.d.ts +0 -19
  51. package/dist/commands/conversion-paths.js +0 -55
  52. package/dist/commands/db.d.ts +0 -25
  53. package/dist/commands/db.js +0 -330
  54. package/dist/commands/destinations.d.ts +0 -20
  55. package/dist/commands/destinations.js +0 -191
  56. package/dist/commands/doctor.js +0 -360
  57. package/dist/commands/errors.d.ts +0 -27
  58. package/dist/commands/errors.js +0 -121
  59. package/dist/commands/events.d.ts +0 -33
  60. package/dist/commands/events.js +0 -349
  61. package/dist/commands/experiments.d.ts +0 -22
  62. package/dist/commands/experiments.js +0 -264
  63. package/dist/commands/funnels.d.ts +0 -17
  64. package/dist/commands/funnels.js +0 -203
  65. package/dist/commands/goals.d.ts +0 -18
  66. package/dist/commands/goals.js +0 -214
  67. package/dist/commands/heatmap.d.ts +0 -27
  68. package/dist/commands/heatmap.js +0 -112
  69. package/dist/commands/identity.d.ts +0 -29
  70. package/dist/commands/identity.js +0 -328
  71. package/dist/commands/init.js +0 -215
  72. package/dist/commands/insights.d.ts +0 -10
  73. package/dist/commands/insights.js +0 -65
  74. package/dist/commands/install.d.ts +0 -259
  75. package/dist/commands/install.js +0 -1590
  76. package/dist/commands/login.d.ts +0 -20
  77. package/dist/commands/login.js +0 -170
  78. package/dist/commands/logout.d.ts +0 -10
  79. package/dist/commands/logout.js +0 -41
  80. package/dist/commands/playground.d.ts +0 -11
  81. package/dist/commands/playground.js +0 -47
  82. package/dist/commands/releases.d.ts +0 -17
  83. package/dist/commands/releases.js +0 -54
  84. package/dist/commands/replay.d.ts +0 -18
  85. package/dist/commands/replay.js +0 -64
  86. package/dist/commands/secrets.d.ts +0 -19
  87. package/dist/commands/secrets.js +0 -145
  88. package/dist/commands/sites.d.ts +0 -18
  89. package/dist/commands/sites.js +0 -139
  90. package/dist/commands/skad.d.ts +0 -18
  91. package/dist/commands/skad.js +0 -53
  92. package/dist/commands/sourcemap.d.ts +0 -33
  93. package/dist/commands/sourcemap.js +0 -204
  94. package/dist/commands/status.d.ts +0 -7
  95. package/dist/commands/status.js +0 -136
  96. package/dist/commands/upgrade.d.ts +0 -21
  97. package/dist/commands/upgrade.js +0 -183
  98. package/dist/commands/warehouse.d.ts +0 -20
  99. package/dist/commands/warehouse.js +0 -65
  100. package/dist/commands/warehouses.d.ts +0 -17
  101. package/dist/commands/warehouses.js +0 -182
  102. package/dist/commands/watch.d.ts +0 -45
  103. package/dist/commands/watch.js +0 -258
  104. package/dist/commands/whoami.d.ts +0 -9
  105. package/dist/commands/whoami.js +0 -50
  106. package/dist/config.d.ts +0 -75
  107. package/dist/config.js +0 -329
  108. package/dist/frameworks/detect.d.ts +0 -8
  109. package/dist/frameworks/detect.js +0 -444
  110. package/dist/install-intent-proposal.d.ts +0 -99
  111. package/dist/install-intent-proposal.js +0 -202
  112. package/dist/utils/api.d.ts +0 -20
  113. package/dist/utils/api.js +0 -47
  114. package/dist/utils/config.d.ts +0 -13
  115. package/dist/utils/config.js +0 -30
  116. package/dist/utils/confirm.d.ts +0 -17
  117. package/dist/utils/confirm.js +0 -40
  118. package/dist/utils/dry-run.d.ts +0 -20
  119. package/dist/utils/dry-run.js +0 -67
  120. package/dist/utils/from-file.d.ts +0 -9
  121. package/dist/utils/from-file.js +0 -72
  122. package/dist/utils/redact.d.ts +0 -14
  123. package/dist/utils/redact.js +0 -48
  124. package/dist/utils/ui.d.ts +0 -14
  125. package/dist/utils/ui.js +0 -59
  126. package/scripts/.gitkeep +0 -0
  127. package/scripts/README-gurulu-agentic-install.md +0 -114
  128. package/scripts/README-gurulu-scan.md +0 -98
  129. package/scripts/audit-cli-scopes.mjs +0 -204
  130. package/scripts/backfill-tenant-id.mjs +0 -172
  131. package/scripts/backfill-tenant-links.ts +0 -252
  132. package/scripts/backup-clickhouse.sh +0 -27
  133. package/scripts/backup-postgres.sh +0 -19
  134. package/scripts/bootstrap-runtime-schema.mjs +0 -87
  135. package/scripts/bootstrap-stripe.mjs +0 -158
  136. package/scripts/gurulu-agentic-install.lib.cjs +0 -762
  137. package/scripts/gurulu-agentic-install.mjs +0 -623
  138. package/scripts/gurulu-scan.lib.cjs +0 -1509
  139. package/scripts/gurulu-scan.mjs +0 -91
  140. package/scripts/gurulu-verify-install.lib.cjs +0 -334
  141. package/scripts/gurulu-verify-install.mjs +0 -59
  142. package/scripts/init-ssl.sh +0 -26
  143. package/scripts/migrate-flow-graph-enums.sh +0 -86
  144. package/scripts/monitor-disk.sh +0 -24
  145. package/scripts/patches/astro.patch.cjs +0 -74
  146. package/scripts/patches/auto-instrument/ast-helper.cjs +0 -480
  147. package/scripts/patches/auto-instrument/astro.cjs +0 -273
  148. package/scripts/patches/auto-instrument/express.cjs +0 -383
  149. package/scripts/patches/auto-instrument/fastify.cjs +0 -262
  150. package/scripts/patches/auto-instrument/hono.cjs +0 -392
  151. package/scripts/patches/auto-instrument/index.cjs +0 -80
  152. package/scripts/patches/auto-instrument/nestjs.cjs +0 -286
  153. package/scripts/patches/auto-instrument/nextjs-app-router.cjs +0 -345
  154. package/scripts/patches/auto-instrument/nextjs-pages.cjs +0 -361
  155. package/scripts/patches/auto-instrument/remix.cjs +0 -168
  156. package/scripts/patches/auto-instrument/sdk-helper-map.cjs +0 -241
  157. package/scripts/patches/auto-instrument/singleton-helper.cjs +0 -193
  158. package/scripts/patches/auto-instrument/sveltekit.cjs +0 -161
  159. package/scripts/patches/auto-instrument/vite-react.cjs +0 -37
  160. package/scripts/patches/auto-instrument/vue.cjs +0 -196
  161. package/scripts/patches/express.patch.cjs +0 -99
  162. package/scripts/patches/fastify.patch.cjs +0 -108
  163. package/scripts/patches/index.cjs +0 -300
  164. package/scripts/patches/nestjs.patch.cjs +0 -112
  165. package/scripts/patches/nextjs-app-router.patch.cjs +0 -97
  166. package/scripts/patches/nextjs-pages.patch.cjs +0 -97
  167. package/scripts/patches/remix.patch.cjs +0 -75
  168. package/scripts/patches/sveltekit.patch.cjs +0 -72
  169. package/scripts/patches/vite-react.patch.cjs +0 -73
  170. package/scripts/patches/vue.patch.cjs +0 -82
  171. package/scripts/renew-ssl.sh +0 -14
  172. package/scripts/resolve-migration.sh +0 -23
  173. package/scripts/seed-cli-dev-keys.mjs +0 -130
  174. package/scripts/seed-test-data.mjs +0 -391
  175. package/scripts/spike-browserless.ts +0 -65
  176. package/scripts/tenant-pivot-consistency-check.mjs +0 -205
  177. package/scripts/tenant-pivot-phase-3-cleanup.lib.cjs +0 -258
  178. package/scripts/tenant-pivot-phase-3-cleanup.mjs +0 -98
  179. package/scripts/test-identity-resolution.ts +0 -804
  180. 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
- }