@gurulu/cli 0.4.7 → 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 (182) 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 -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/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 -19
  45. package/dist/commands/chat.js +0 -118
  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 -371
  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 -77
  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/setup.d.ts +0 -21
  89. package/dist/commands/setup.js +0 -67
  90. package/dist/commands/sites.d.ts +0 -18
  91. package/dist/commands/sites.js +0 -139
  92. package/dist/commands/skad.d.ts +0 -18
  93. package/dist/commands/skad.js +0 -53
  94. package/dist/commands/sourcemap.d.ts +0 -33
  95. package/dist/commands/sourcemap.js +0 -204
  96. package/dist/commands/status.d.ts +0 -7
  97. package/dist/commands/status.js +0 -136
  98. package/dist/commands/upgrade.d.ts +0 -21
  99. package/dist/commands/upgrade.js +0 -183
  100. package/dist/commands/warehouse.d.ts +0 -20
  101. package/dist/commands/warehouse.js +0 -65
  102. package/dist/commands/warehouses.d.ts +0 -17
  103. package/dist/commands/warehouses.js +0 -182
  104. package/dist/commands/watch.d.ts +0 -45
  105. package/dist/commands/watch.js +0 -258
  106. package/dist/commands/whoami.d.ts +0 -9
  107. package/dist/commands/whoami.js +0 -50
  108. package/dist/config.d.ts +0 -75
  109. package/dist/config.js +0 -329
  110. package/dist/frameworks/detect.d.ts +0 -8
  111. package/dist/frameworks/detect.js +0 -458
  112. package/dist/install-intent-proposal.d.ts +0 -99
  113. package/dist/install-intent-proposal.js +0 -202
  114. package/dist/utils/api.d.ts +0 -20
  115. package/dist/utils/api.js +0 -47
  116. package/dist/utils/config.d.ts +0 -13
  117. package/dist/utils/config.js +0 -30
  118. package/dist/utils/confirm.d.ts +0 -17
  119. package/dist/utils/confirm.js +0 -40
  120. package/dist/utils/dry-run.d.ts +0 -20
  121. package/dist/utils/dry-run.js +0 -67
  122. package/dist/utils/from-file.d.ts +0 -9
  123. package/dist/utils/from-file.js +0 -72
  124. package/dist/utils/redact.d.ts +0 -14
  125. package/dist/utils/redact.js +0 -48
  126. package/dist/utils/ui.d.ts +0 -14
  127. package/dist/utils/ui.js +0 -59
  128. package/scripts/.gitkeep +0 -0
  129. package/scripts/README-gurulu-agentic-install.md +0 -114
  130. package/scripts/README-gurulu-scan.md +0 -98
  131. package/scripts/audit-cli-scopes.mjs +0 -204
  132. package/scripts/backfill-tenant-id.mjs +0 -172
  133. package/scripts/backfill-tenant-links.ts +0 -252
  134. package/scripts/backup-clickhouse.sh +0 -27
  135. package/scripts/backup-postgres.sh +0 -19
  136. package/scripts/bootstrap-runtime-schema.mjs +0 -87
  137. package/scripts/bootstrap-stripe.mjs +0 -158
  138. package/scripts/gurulu-agentic-install.lib.cjs +0 -762
  139. package/scripts/gurulu-agentic-install.mjs +0 -623
  140. package/scripts/gurulu-scan.lib.cjs +0 -1509
  141. package/scripts/gurulu-scan.mjs +0 -91
  142. package/scripts/gurulu-verify-install.lib.cjs +0 -334
  143. package/scripts/gurulu-verify-install.mjs +0 -59
  144. package/scripts/init-ssl.sh +0 -26
  145. package/scripts/migrate-flow-graph-enums.sh +0 -86
  146. package/scripts/monitor-disk.sh +0 -24
  147. package/scripts/patches/astro.patch.cjs +0 -74
  148. package/scripts/patches/auto-instrument/ast-helper.cjs +0 -480
  149. package/scripts/patches/auto-instrument/astro.cjs +0 -273
  150. package/scripts/patches/auto-instrument/express.cjs +0 -383
  151. package/scripts/patches/auto-instrument/fastify.cjs +0 -262
  152. package/scripts/patches/auto-instrument/hono.cjs +0 -392
  153. package/scripts/patches/auto-instrument/index.cjs +0 -80
  154. package/scripts/patches/auto-instrument/nestjs.cjs +0 -286
  155. package/scripts/patches/auto-instrument/nextjs-app-router.cjs +0 -345
  156. package/scripts/patches/auto-instrument/nextjs-pages.cjs +0 -361
  157. package/scripts/patches/auto-instrument/remix.cjs +0 -168
  158. package/scripts/patches/auto-instrument/sdk-helper-map.cjs +0 -241
  159. package/scripts/patches/auto-instrument/singleton-helper.cjs +0 -193
  160. package/scripts/patches/auto-instrument/sveltekit.cjs +0 -161
  161. package/scripts/patches/auto-instrument/vite-react.cjs +0 -37
  162. package/scripts/patches/auto-instrument/vue.cjs +0 -196
  163. package/scripts/patches/express.patch.cjs +0 -99
  164. package/scripts/patches/fastify.patch.cjs +0 -108
  165. package/scripts/patches/index.cjs +0 -300
  166. package/scripts/patches/nestjs.patch.cjs +0 -112
  167. package/scripts/patches/nextjs-app-router.patch.cjs +0 -97
  168. package/scripts/patches/nextjs-pages.patch.cjs +0 -97
  169. package/scripts/patches/remix.patch.cjs +0 -75
  170. package/scripts/patches/sveltekit.patch.cjs +0 -72
  171. package/scripts/patches/vite-react.patch.cjs +0 -73
  172. package/scripts/patches/vue.patch.cjs +0 -82
  173. package/scripts/renew-ssl.sh +0 -14
  174. package/scripts/resolve-migration.sh +0 -23
  175. package/scripts/seed-cli-dev-keys.mjs +0 -130
  176. package/scripts/seed-test-data.mjs +0 -391
  177. package/scripts/spike-browserless.ts +0 -65
  178. package/scripts/tenant-pivot-consistency-check.mjs +0 -205
  179. package/scripts/tenant-pivot-phase-3-cleanup.lib.cjs +0 -258
  180. package/scripts/tenant-pivot-phase-3-cleanup.mjs +0 -98
  181. package/scripts/test-identity-resolution.ts +0 -804
  182. 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
- }