lytx 0.3.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 (213) hide show
  1. package/.env.example +37 -0
  2. package/README.md +486 -0
  3. package/alchemy.run.ts +155 -0
  4. package/cli/bootstrap-admin.ts +284 -0
  5. package/cli/deploy-staging.ts +692 -0
  6. package/cli/import-events.ts +628 -0
  7. package/cli/import-sites.ts +518 -0
  8. package/cli/index.ts +609 -0
  9. package/cli/init-db.ts +269 -0
  10. package/cli/migrate-to-durable-objects.ts +564 -0
  11. package/cli/migration-worker.ts +300 -0
  12. package/cli/performance-test.ts +588 -0
  13. package/cli/pg/client.ts +4 -0
  14. package/cli/pg/new-site.ts +153 -0
  15. package/cli/rollback-durable-objects.ts +622 -0
  16. package/cli/seed-data.ts +459 -0
  17. package/cli/setup.js +18 -0
  18. package/cli/setup.ts +463 -0
  19. package/cli/validate-migration.ts +200 -0
  20. package/cli/wrangler-migration.jsonc +28 -0
  21. package/db/adapter.ts +166 -0
  22. package/db/analytics_engine/client.ts +0 -0
  23. package/db/analytics_engine/sites.ts +0 -0
  24. package/db/client.ts +16 -0
  25. package/db/d1/client.ts +8 -0
  26. package/db/d1/drizzle.config.ts +35 -0
  27. package/db/d1/migrations/0000_true_maelstrom.sql +165 -0
  28. package/db/d1/migrations/0001_wonderful_bloodaxe.sql +12 -0
  29. package/db/d1/migrations/0002_late_frightful_four.sql +1 -0
  30. package/db/d1/migrations/0003_cuddly_obadiah_stane.sql +16 -0
  31. package/db/d1/migrations/0004_mute_stardust.sql +1 -0
  32. package/db/d1/migrations/0005_awesome_silvermane.sql +3 -0
  33. package/db/d1/migrations/0006_volatile_shriek.sql +2 -0
  34. package/db/d1/migrations/0007_superb_lila_cheney.sql +1 -0
  35. package/db/d1/migrations/0008_bitter_longshot.sql +17 -0
  36. package/db/d1/migrations/0009_wonderful_madame_masque.sql +28 -0
  37. package/db/d1/migrations/meta/0000_snapshot.json +1112 -0
  38. package/db/d1/migrations/meta/0001_snapshot.json +1187 -0
  39. package/db/d1/migrations/meta/0002_snapshot.json +1194 -0
  40. package/db/d1/migrations/meta/0003_snapshot.json +1296 -0
  41. package/db/d1/migrations/meta/0004_snapshot.json +1303 -0
  42. package/db/d1/migrations/meta/0005_snapshot.json +1325 -0
  43. package/db/d1/migrations/meta/0006_snapshot.json +1339 -0
  44. package/db/d1/migrations/meta/0007_snapshot.json +1347 -0
  45. package/db/d1/migrations/meta/0008_snapshot.json +1464 -0
  46. package/db/d1/migrations/meta/0009_snapshot.json +1648 -0
  47. package/db/d1/migrations/meta/_journal.json +76 -0
  48. package/db/d1/schema.ts +407 -0
  49. package/db/d1/sites.ts +374 -0
  50. package/db/d1/teamAiUsage.ts +101 -0
  51. package/db/d1/teams.ts +127 -0
  52. package/db/durable/drizzle.config.ts +8 -0
  53. package/db/durable/durableObjectClient.ts +480 -0
  54. package/db/durable/events.ts +100 -0
  55. package/db/durable/migrations/0000_fair_bucky.sql +38 -0
  56. package/db/durable/migrations/meta/0000_snapshot.json +278 -0
  57. package/db/durable/migrations/meta/_journal.json +13 -0
  58. package/db/durable/migrations/migrations.js +10 -0
  59. package/db/durable/schema.ts +5 -0
  60. package/db/durable/siteDurableObject.ts +1352 -0
  61. package/db/durable/types.ts +53 -0
  62. package/db/postgres/client.ts +13 -0
  63. package/db/postgres/drizzle.config.ts +12 -0
  64. package/db/postgres/migrations/0000_brainy_sprite.sql +116 -0
  65. package/db/postgres/migrations/meta/0000_snapshot.json +681 -0
  66. package/db/postgres/migrations/meta/_journal.json +13 -0
  67. package/db/postgres/schema.ts +145 -0
  68. package/db/postgres/sites.ts +118 -0
  69. package/db/tranformReports.ts +595 -0
  70. package/db/types.ts +55 -0
  71. package/endpoints/api_worker.tsx +1854 -0
  72. package/endpoints/site_do_worker.ts +11 -0
  73. package/index.d.ts +63 -0
  74. package/index.ts +83 -0
  75. package/lib/auth.ts +279 -0
  76. package/lib/geojson/world_countries.json +45307 -0
  77. package/lib/random_name.ts +41 -0
  78. package/lib/sendMail.ts +252 -0
  79. package/package.json +142 -0
  80. package/public/favicon.ico +0 -0
  81. package/public/images/android-chrome-192x192.png +0 -0
  82. package/public/images/android-chrome-512x512.png +0 -0
  83. package/public/images/apple-touch-icon.png +0 -0
  84. package/public/images/favicon-16x16.png +0 -0
  85. package/public/images/favicon-32x32.png +0 -0
  86. package/public/images/lytx_dark_dashboard.png +0 -0
  87. package/public/images/lytx_light_dashboard.png +0 -0
  88. package/public/images/safari-pinned-tab.svg +4 -0
  89. package/public/logo.png +0 -0
  90. package/public/site.webmanifest +26 -0
  91. package/public/sw.js +107 -0
  92. package/src/Document.tsx +86 -0
  93. package/src/api/ai_api.ts +1156 -0
  94. package/src/api/authMiddleware.ts +45 -0
  95. package/src/api/auth_api.ts +465 -0
  96. package/src/api/event_labels_api.ts +193 -0
  97. package/src/api/events_api.ts +210 -0
  98. package/src/api/queueWorker.ts +303 -0
  99. package/src/api/reports_api.ts +278 -0
  100. package/src/api/seed_api.ts +288 -0
  101. package/src/api/sites_api.ts +904 -0
  102. package/src/api/tag_api.ts +458 -0
  103. package/src/api/tag_api_v2.ts +289 -0
  104. package/src/api/team_api.ts +456 -0
  105. package/src/app/Dashboard.tsx +1339 -0
  106. package/src/app/Events.tsx +974 -0
  107. package/src/app/Explore.tsx +312 -0
  108. package/src/app/Layout.tsx +58 -0
  109. package/src/app/Settings.tsx +1302 -0
  110. package/src/app/components/DashboardCard.tsx +118 -0
  111. package/src/app/components/EditableCell.tsx +123 -0
  112. package/src/app/components/EventForm.tsx +93 -0
  113. package/src/app/components/MarketingFooter.tsx +49 -0
  114. package/src/app/components/MarketingNav.tsx +150 -0
  115. package/src/app/components/Nav.tsx +755 -0
  116. package/src/app/components/NewSiteSetup.tsx +298 -0
  117. package/src/app/components/SQLEditor.tsx +740 -0
  118. package/src/app/components/SiteSelector.tsx +126 -0
  119. package/src/app/components/SiteTag.tsx +42 -0
  120. package/src/app/components/SiteTagInstallCard.tsx +241 -0
  121. package/src/app/components/WorldMapCard.tsx +337 -0
  122. package/src/app/components/charts/ChartComponents.tsx +1481 -0
  123. package/src/app/components/charts/EventFunnel.tsx +45 -0
  124. package/src/app/components/charts/EventSummary.tsx +194 -0
  125. package/src/app/components/charts/SankeyFlows.tsx +72 -0
  126. package/src/app/components/marketing/CheckIcon.tsx +16 -0
  127. package/src/app/components/marketing/MarketingLayout.tsx +23 -0
  128. package/src/app/components/marketing/SectionHeading.tsx +35 -0
  129. package/src/app/components/reports/AskAiWorkspace.tsx +371 -0
  130. package/src/app/components/reports/CreateReportStarter.tsx +74 -0
  131. package/src/app/components/reports/DashboardRouteFiltersContext.tsx +14 -0
  132. package/src/app/components/reports/DashboardToolbar.tsx +154 -0
  133. package/src/app/components/reports/DashboardWorkspaceLayout.tsx +63 -0
  134. package/src/app/components/reports/DashboardWorkspaceShell.tsx +118 -0
  135. package/src/app/components/reports/ReportBuilderWorkspace.tsx +76 -0
  136. package/src/app/components/reports/custom/CustomReportBuilderPage.tsx +1667 -0
  137. package/src/app/components/reports/custom/ReportWidgetChart.tsx +297 -0
  138. package/src/app/components/reports/custom/buildWidgetSql.ts +151 -0
  139. package/src/app/components/reports/custom/chartPalettes.ts +18 -0
  140. package/src/app/components/reports/custom/types.ts +50 -0
  141. package/src/app/components/reports/reportBuilderMenuItems.ts +17 -0
  142. package/src/app/components/reports/useDashboardToolbarControls.tsx +235 -0
  143. package/src/app/components/ui/AlertBanner.tsx +101 -0
  144. package/src/app/components/ui/Button.tsx +55 -0
  145. package/src/app/components/ui/Card.tsx +80 -0
  146. package/src/app/components/ui/Input.tsx +72 -0
  147. package/src/app/components/ui/Link.tsx +23 -0
  148. package/src/app/components/ui/ReportBuilderMenu.tsx +246 -0
  149. package/src/app/components/ui/ThemeToggle.tsx +54 -0
  150. package/src/app/constants.ts +6 -0
  151. package/src/app/headers.ts +33 -0
  152. package/src/app/providers/AuthProvider.tsx +189 -0
  153. package/src/app/providers/ClientProviders.tsx +18 -0
  154. package/src/app/providers/QueryProvider.tsx +23 -0
  155. package/src/app/providers/ThemeProvider.tsx +88 -0
  156. package/src/app/utils/chartThemes.ts +146 -0
  157. package/src/app/utils/keybinds.ts +96 -0
  158. package/src/app/utils/media.tsx +24 -0
  159. package/src/client.tsx +114 -0
  160. package/src/config/createLytxAppConfig.ts +252 -0
  161. package/src/config/resourceNames.ts +88 -0
  162. package/src/db/index.ts +67 -0
  163. package/src/index.css +285 -0
  164. package/src/lib/featureFlags.ts +69 -0
  165. package/src/pages/GetStarted.tsx +290 -0
  166. package/src/pages/Home.tsx +268 -0
  167. package/src/pages/Login.tsx +283 -0
  168. package/src/pages/PrivacyPolicy.tsx +120 -0
  169. package/src/pages/Signup.tsx +267 -0
  170. package/src/pages/TermsOfService.tsx +126 -0
  171. package/src/pages/VerifyEmail.tsx +56 -0
  172. package/src/session/durableObject.ts +7 -0
  173. package/src/session/siteSchema.ts +86 -0
  174. package/src/session/types.ts +36 -0
  175. package/src/templates/README.md +80 -0
  176. package/src/templates/cleanFunctions.js +44 -0
  177. package/src/templates/embedFunctions.js +52 -0
  178. package/src/templates/lytx-shared.ts +662 -0
  179. package/src/templates/lytxpixel-core.ts +144 -0
  180. package/src/templates/lytxpixel.ts +267 -0
  181. package/src/templates/lytxpixelBrowser.js +634 -0
  182. package/src/templates/lytxpixelBrowser.mjs +634 -0
  183. package/src/templates/parseData.js +12 -0
  184. package/src/templates/script.ts +31 -0
  185. package/src/templates/template.tsx +50 -0
  186. package/src/templates/test.js +3 -0
  187. package/src/templates/trackWebEvents.ts +177 -0
  188. package/src/templates/vendors/clickcease.ts +8 -0
  189. package/src/templates/vendors/google.ts +174 -0
  190. package/src/templates/vendors/linkedin.ts +23 -0
  191. package/src/templates/vendors/meta.ts +56 -0
  192. package/src/templates/vendors/quantcast.ts +22 -0
  193. package/src/templates/vendors/simplfi.ts +7 -0
  194. package/src/types/app-context.ts +16 -0
  195. package/src/utilities/dashboardParams.ts +188 -0
  196. package/src/utilities/dashboardQueries.ts +537 -0
  197. package/src/utilities/dashboardTransforms.ts +167 -0
  198. package/src/utilities/dataValidation.ts +414 -0
  199. package/src/utilities/detector.ts +73 -0
  200. package/src/utilities/encrypt.ts +103 -0
  201. package/src/utilities/index.ts +13 -0
  202. package/src/utilities/parser.ts +117 -0
  203. package/src/utilities/performanceMonitoring.ts +570 -0
  204. package/src/utilities/route_interuptors.ts +24 -0
  205. package/src/worker.tsx +675 -0
  206. package/tsconfig.json +78 -0
  207. package/types/env.d.ts +16 -0
  208. package/types/rw.d.ts +7 -0
  209. package/types/shims.d.ts +53 -0
  210. package/types/vite.d.ts +19 -0
  211. package/vite/vite-plugin-pixel-bundle.ts +126 -0
  212. package/vite.config.ts +53 -0
  213. package/worker-configuration.d.ts +8401 -0
@@ -0,0 +1,628 @@
1
+ #!/usr/bin/env bun
2
+ import { createId } from "@paralleldrive/cuid2";
3
+ import { execSync } from "child_process";
4
+ import { writeFileSync, unlinkSync } from "fs";
5
+ import { join } from "path";
6
+ import { drizzle } from "drizzle-orm/postgres-js";
7
+ import postgres from "postgres";
8
+ import { siteEvents as pgSiteEvents } from "@db/postgres/schema";
9
+ import { eq } from "drizzle-orm";
10
+
11
+ // Parse CLI arguments
12
+ const args = process.argv.slice(2);
13
+ const getArg = (flag: string, defaultValue?: string): string => {
14
+ const index = args.indexOf(flag);
15
+ if (index === -1) {
16
+ if (defaultValue !== undefined) return defaultValue;
17
+ throw new Error(`Missing required argument: ${flag}`);
18
+ }
19
+ const value = args[index + 1];
20
+ if (!value || value.startsWith("-")) {
21
+ throw new Error(`Invalid value for ${flag}`);
22
+ }
23
+ return value;
24
+ };
25
+
26
+ const hasFlag = (flag: string): boolean => args.includes(flag);
27
+
28
+ // Check for help flag first
29
+ if (hasFlag("--help") || hasFlag("-h")) {
30
+ console.log(`
31
+ Usage: bun run cli/import-events.ts [options]
32
+
33
+ Options:
34
+ -t, --team-id <id> Team ID for the events (required)
35
+ -s, --site-id <id> Site ID for the events (required)
36
+ -d, --database <name> Database name (default: "lytx_core_db")
37
+ --local Use local database (default: true)
38
+ --remote Use remote database (default: false)
39
+ --from-db <url> Import from PostgreSQL database (provide connection string)
40
+ --from-site-id <id> Source site ID to import from (when using --from-db)
41
+ -h, --help Show this help message
42
+
43
+ Event Data Sources:
44
+ 1. JSON via stdin (default):
45
+ The script expects JSON data piped to stdin with the following structure:
46
+ [
47
+ {
48
+ "event": "page_view",
49
+ "client_page_url": "/home",
50
+ "page_url": "https://example.com/home",
51
+ "referer": "https://google.com",
52
+ "browser": "Chrome 120.0.0",
53
+ "operating_system": "Windows 11",
54
+ "device_type": "desktop",
55
+ "country": "US",
56
+ "region": "California",
57
+ "city": "San Francisco",
58
+ "postal": "94102",
59
+ "screen_width": 1920,
60
+ "screen_height": 1080,
61
+ "rid": "visitor_id_123",
62
+ "custom_data": {"key": "value"},
63
+ "bot_data": {"is_bot": false},
64
+ "query_params": {"utm_source": "google"},
65
+ "created_at": 1640995200
66
+ },
67
+ ...
68
+ ]
69
+
70
+ Required fields:
71
+ - event: Event type (string)
72
+
73
+ Optional fields:
74
+ - client_page_url: Client page URL (string)
75
+ - page_url: Full page URL (string)
76
+ - referer: Referrer URL (string)
77
+ - browser: Browser info (string)
78
+ - operating_system: OS info (string)
79
+ - device_type: Device type (string)
80
+ - country: Country code (string)
81
+ - region: Region/state (string)
82
+ - city: City name (string)
83
+ - postal: Postal code (string)
84
+ - screen_width: Screen width (number)
85
+ - screen_height: Screen height (number)
86
+ - rid: Visitor ID (string, auto-generated if not provided)
87
+ - custom_data: Custom data object (object)
88
+ - bot_data: Bot detection data (object)
89
+ - query_params: Query parameters (object)
90
+ - created_at: Unix timestamp in seconds (number, defaults to current time)
91
+
92
+ 2. PostgreSQL database:
93
+ Use --from-db with a connection string to import from another database
94
+
95
+ Examples:
96
+ # Import from JSON stdin
97
+ echo '[{"event":"page_view","client_page_url":"/"}]' | bun run cli/import-events.ts --team-id 1 --site-id 5
98
+ cat events.json | bun run cli/import-events.ts --team-id 1 --site-id 5 --remote
99
+ bun run cli/import-events.ts --team-id 1 --site-id 5 --local < events.json
100
+
101
+ # Import from PostgreSQL database
102
+ bun run cli/import-events.ts --team-id 1 --site-id 5 --from-db "postgresql://user:pass@host:5432/db" --from-site-id 10
103
+ bun run cli/import-events.ts --team-id 1 --site-id 5 --from-db "$DATABASE_URL" --from-site-id 10 --remote
104
+ `);
105
+ process.exit(0);
106
+ }
107
+
108
+ // CLI argument parsing
109
+ const getTeamIdArg = () => {
110
+ try {
111
+ return parseInt(getArg("--team-id"));
112
+ } catch {
113
+ return parseInt(getArg("-t"));
114
+ }
115
+ };
116
+
117
+ const getSiteIdArg = () => {
118
+ try {
119
+ return parseInt(getArg("--site-id"));
120
+ } catch {
121
+ return parseInt(getArg("-s"));
122
+ }
123
+ };
124
+
125
+ const getDatabaseArg = () => {
126
+ try {
127
+ return getArg("--database");
128
+ } catch {
129
+ try {
130
+ return getArg("-d");
131
+ } catch {
132
+ return "lytx_core_db";
133
+ }
134
+ }
135
+ };
136
+
137
+ const getFromDbArg = () => {
138
+ try {
139
+ return getArg("--from-db");
140
+ } catch {
141
+ return null;
142
+ }
143
+ };
144
+
145
+ const getFromSiteIdArg = () => {
146
+ try {
147
+ return parseInt(getArg("--from-site-id"));
148
+ } catch {
149
+ return null;
150
+ }
151
+ };
152
+
153
+ const teamId = getTeamIdArg();
154
+ const siteId = getSiteIdArg();
155
+ const database = getDatabaseArg();
156
+ const fromDb = getFromDbArg();
157
+ const fromSiteId = getFromSiteIdArg();
158
+ const isRemote = hasFlag("--remote");
159
+ const isLocal = hasFlag("--local") || !isRemote;
160
+
161
+ // Helper function to execute SQL via wrangler
162
+ function executeSQL(sql: string, description: string) {
163
+ console.log(`šŸ“ ${description}...`);
164
+
165
+ const tempFile = join(process.cwd(), `temp_${Date.now()}.sql`);
166
+ writeFileSync(tempFile, sql);
167
+
168
+ try {
169
+ const command = `bunx wrangler d1 execute ${database} --file ${tempFile} ${isLocal ? "--local" : "--remote"} --yes`;
170
+ const result = execSync(command, { encoding: "utf8", stdio: "pipe" });
171
+ console.log(`āœ… ${description} completed`);
172
+ return result;
173
+ } catch (error: any) {
174
+ console.error(`āŒ Error during ${description}:`, error.message);
175
+ throw error;
176
+ } finally {
177
+ try {
178
+ unlinkSync(tempFile);
179
+ } catch (e) {
180
+ // Ignore cleanup errors
181
+ }
182
+ }
183
+ }
184
+
185
+ // Helper function to validate site exists and get tag_id
186
+ async function validateSiteAndGetTagId(
187
+ teamId: number,
188
+ siteId: number,
189
+ ): Promise<string> {
190
+ const validateSiteSQL = `SELECT site_id, tag_id, name, domain FROM sites WHERE site_id = ${siteId} AND team_id = ${teamId};`;
191
+ const tempFile = join(process.cwd(), `temp_query_${Date.now()}.sql`);
192
+ writeFileSync(tempFile, validateSiteSQL);
193
+
194
+ try {
195
+ const command = `bunx wrangler d1 execute ${database} --file ${tempFile} ${isLocal ? "--local" : "--remote"} --json --yes`;
196
+ const result = execSync(command, { encoding: "utf8", stdio: "pipe" });
197
+
198
+ // Extract JSON from wrangler output (skip progress indicators)
199
+ const jsonStart = result.indexOf("[");
200
+ const jsonEnd = result.lastIndexOf("]") + 1;
201
+
202
+ if (jsonStart === -1 || jsonEnd === 0) {
203
+ console.error("āŒ No JSON found in wrangler output:");
204
+ console.error("Raw output:", result);
205
+ throw new Error(
206
+ `Wrangler did not return valid JSON. Check database connection and permissions.`,
207
+ );
208
+ }
209
+
210
+ const jsonString = result.substring(jsonStart, jsonEnd);
211
+
212
+ let jsonResult;
213
+ try {
214
+ jsonResult = JSON.parse(jsonString);
215
+ } catch (parseError) {
216
+ console.error("āŒ Failed to parse extracted JSON:");
217
+ console.error("Extracted JSON:", jsonString);
218
+ throw new Error(`Invalid JSON format from wrangler.`, { cause: parseError });
219
+ }
220
+
221
+ if (!jsonResult || !jsonResult[0] || !jsonResult[0].results) {
222
+ throw new Error(
223
+ `Unexpected response format from wrangler: ${JSON.stringify(jsonResult)}`,
224
+ );
225
+ }
226
+
227
+ if (!jsonResult || !jsonResult[0] || !jsonResult[0].results) {
228
+ throw new Error(
229
+ `Unexpected response format from wrangler: ${JSON.stringify(jsonResult)}`,
230
+ );
231
+ }
232
+
233
+ if (jsonResult[0].results.length === 0) {
234
+ throw new Error(
235
+ `Site ID ${siteId} not found or doesn't belong to team ${teamId}`,
236
+ );
237
+ }
238
+
239
+ const site = jsonResult[0].results[0];
240
+ console.log(
241
+ `āœ… Found site: ${site.name} (${site.domain}) - Tag ID: ${site.tag_id}`,
242
+ );
243
+ return site.tag_id;
244
+ } catch (error: any) {
245
+ console.error("āŒ Error validating site:", error.message);
246
+ throw error;
247
+ } finally {
248
+ try {
249
+ unlinkSync(tempFile);
250
+ } catch (e) {
251
+ // Ignore cleanup errors
252
+ }
253
+ }
254
+ }
255
+
256
+ // Helper function to read from PostgreSQL database
257
+ async function readFromDatabase(
258
+ connectionString: string,
259
+ sourceSiteId: number,
260
+ ): Promise<EventInput[]> {
261
+ console.log("šŸ“– Reading events from PostgreSQL database...");
262
+
263
+ const sql = postgres(connectionString);
264
+ const db = drizzle(sql);
265
+
266
+ try {
267
+ const events = await db
268
+ .select({
269
+ event: pgSiteEvents.event,
270
+ client_page_url: pgSiteEvents.client_page_url,
271
+ page_url: pgSiteEvents.page_url,
272
+ referer: pgSiteEvents.referer,
273
+ browser: pgSiteEvents.browser,
274
+ operating_system: pgSiteEvents.operating_system,
275
+ device_type: pgSiteEvents.device_type,
276
+ country: pgSiteEvents.country,
277
+ region: pgSiteEvents.region,
278
+ city: pgSiteEvents.city,
279
+ postal: pgSiteEvents.postal,
280
+ screen_width: pgSiteEvents.screen_width,
281
+ screen_height: pgSiteEvents.screen_height,
282
+ rid: pgSiteEvents.rid,
283
+ custom_data: pgSiteEvents.custom_data,
284
+ bot_data: pgSiteEvents.bot_data,
285
+ query_params: pgSiteEvents.query_params,
286
+ created_at: pgSiteEvents.created_at,
287
+ })
288
+ .from(pgSiteEvents)
289
+ .where(eq(pgSiteEvents.site_id, sourceSiteId));
290
+
291
+ console.log(`āœ… Found ${events.length} events in source database`);
292
+
293
+ return events.map((event) => ({
294
+ event: event.event || "page_view",
295
+ client_page_url: event.client_page_url || undefined,
296
+ page_url: event.page_url || undefined,
297
+ referer: event.referer || undefined,
298
+ browser: event.browser || undefined,
299
+ operating_system: event.operating_system || undefined,
300
+ device_type: event.device_type || undefined,
301
+ country: event.country || undefined,
302
+ region: event.region || undefined,
303
+ city: event.city || undefined,
304
+ postal: event.postal || undefined,
305
+ screen_width: event.screen_width || undefined,
306
+ screen_height: event.screen_height || undefined,
307
+ rid: event.rid || undefined,
308
+ custom_data: event.custom_data || undefined,
309
+ bot_data: event.bot_data || undefined,
310
+ query_params: event.query_params || undefined,
311
+ created_at: event.created_at
312
+ ? Math.floor(new Date(event.created_at).getTime() / 1000)
313
+ : Math.floor(Date.now() / 1000),
314
+ }));
315
+ } catch (error: any) {
316
+ throw new Error(`Failed to read from database: ${error.message}`, { cause: error });
317
+ } finally {
318
+ await sql.end();
319
+ }
320
+ }
321
+
322
+ // Helper function to read stdin
323
+ function readStdin(): Promise<string> {
324
+ return new Promise((resolve, reject) => {
325
+ let data = "";
326
+
327
+ process.stdin.setEncoding("utf8");
328
+
329
+ process.stdin.on("readable", () => {
330
+ let chunk;
331
+ while (null !== (chunk = process.stdin.read())) {
332
+ data += chunk;
333
+ }
334
+ });
335
+
336
+ process.stdin.on("end", () => {
337
+ resolve(data.trim());
338
+ });
339
+
340
+ process.stdin.on("error", reject);
341
+ });
342
+ }
343
+
344
+ // Event interface based on schema
345
+ interface EventInput {
346
+ event: string;
347
+ client_page_url?: string;
348
+ page_url?: string;
349
+ referer?: string;
350
+ browser?: string;
351
+ operating_system?: string;
352
+ device_type?: string;
353
+ country?: string;
354
+ region?: string;
355
+ city?: string;
356
+ postal?: string;
357
+ screen_width?: number;
358
+ screen_height?: number;
359
+ rid?: string;
360
+ custom_data?: object;
361
+ bot_data?: object;
362
+ query_params?: object;
363
+ created_at?: number;
364
+ }
365
+
366
+ // Validate event data
367
+ function validateEventData(events: any[]): EventInput[] {
368
+ if (!Array.isArray(events)) {
369
+ throw new Error("Input must be an array of event objects");
370
+ }
371
+
372
+ return events.map((event, index) => {
373
+ if (typeof event !== "object" || event === null) {
374
+ throw new Error(`Event at index ${index} must be an object`);
375
+ }
376
+
377
+ if (!event.event || typeof event.event !== "string") {
378
+ throw new Error(
379
+ `Event at index ${index} must have a valid 'event' field (string)`,
380
+ );
381
+ }
382
+
383
+ // Validate optional string fields
384
+ const stringFields = [
385
+ "client_page_url",
386
+ "page_url",
387
+ "referer",
388
+ "browser",
389
+ "operating_system",
390
+ "device_type",
391
+ "country",
392
+ "region",
393
+ "city",
394
+ "postal",
395
+ "rid",
396
+ ];
397
+ for (const field of stringFields) {
398
+ if (event[field] !== undefined && typeof event[field] !== "string") {
399
+ throw new Error(`Event at index ${index}: '${field}' must be a string`);
400
+ }
401
+ }
402
+
403
+ // Validate optional number fields
404
+ const numberFields = ["screen_width", "screen_height", "created_at"];
405
+ for (const field of numberFields) {
406
+ if (event[field] !== undefined && typeof event[field] !== "number") {
407
+ throw new Error(`Event at index ${index}: '${field}' must be a number`);
408
+ }
409
+ }
410
+
411
+ // Validate optional object fields
412
+ const objectFields = ["custom_data", "bot_data", "query_params"];
413
+ for (const field of objectFields) {
414
+ if (
415
+ event[field] !== undefined &&
416
+ (typeof event[field] !== "object" || event[field] === null)
417
+ ) {
418
+ throw new Error(
419
+ `Event at index ${index}: '${field}' must be an object`,
420
+ );
421
+ }
422
+ }
423
+
424
+ return {
425
+ event: event.event.trim(),
426
+ client_page_url: event.client_page_url?.trim(),
427
+ page_url: event.page_url?.trim(),
428
+ referer: event.referer?.trim(),
429
+ browser: event.browser?.trim(),
430
+ operating_system: event.operating_system?.trim(),
431
+ device_type: event.device_type?.trim(),
432
+ country: event.country?.trim(),
433
+ region: event.region?.trim(),
434
+ city: event.city?.trim(),
435
+ postal: event.postal?.trim(),
436
+ screen_width: event.screen_width,
437
+ screen_height: event.screen_height,
438
+ rid: event.rid?.trim() || createId(), // Generate if not provided
439
+ custom_data: event.custom_data,
440
+ bot_data: event.bot_data,
441
+ query_params: event.query_params,
442
+ created_at: event.created_at || Math.floor(Date.now() / 1000), // Default to current time
443
+ };
444
+ });
445
+ }
446
+
447
+ // Helper function to escape SQL strings and handle nulls
448
+ function sqlValue(value: any): string {
449
+ if (value === undefined || value === null) {
450
+ return "NULL";
451
+ }
452
+ if (typeof value === "string") {
453
+ return `'${value.replace(/'/g, "''")}'`;
454
+ }
455
+ if (typeof value === "object") {
456
+ return `'${JSON.stringify(value).replace(/'/g, "''")}'`;
457
+ }
458
+ return String(value);
459
+ }
460
+
461
+ async function importEvents() {
462
+ try {
463
+ console.log("šŸš€ Starting event import...");
464
+ console.log(`šŸ“Š Target: ${database} (${isLocal ? "local" : "remote"})`);
465
+ console.log(`šŸ¢ Team ID: ${teamId}`);
466
+ console.log(`🌐 Site ID: ${siteId}`);
467
+
468
+ // Validate site exists and get tag_id
469
+ const tagId = await validateSiteAndGetTagId(teamId, siteId);
470
+
471
+ // Get event data from either database or stdin
472
+ let events: EventInput[];
473
+
474
+ if (fromDb) {
475
+ if (!fromSiteId) {
476
+ throw new Error("--from-site-id is required when using --from-db");
477
+ }
478
+ console.log(
479
+ `šŸ“Š Source: PostgreSQL database (Source Site ID: ${fromSiteId})`,
480
+ );
481
+ events = await readFromDatabase(fromDb, fromSiteId);
482
+ } else {
483
+ // Read JSON data from stdin
484
+ console.log("šŸ“– Reading event data from stdin...");
485
+ const stdinData = await readStdin();
486
+
487
+ if (!stdinData) {
488
+ throw new Error(
489
+ "No data provided via stdin. Please pipe JSON data to this command.",
490
+ );
491
+ }
492
+
493
+ // Parse JSON
494
+ let eventsData: any;
495
+ try {
496
+ eventsData = JSON.parse(stdinData);
497
+ } catch (error: any) {
498
+ throw new Error(`Invalid JSON format: ${error.message}`, { cause: error });
499
+ }
500
+
501
+ // Validate event data
502
+ events = validateEventData(eventsData);
503
+ }
504
+ console.log(`šŸ“‹ Found ${events.length} events to import`);
505
+
506
+ if (events.length === 0) {
507
+ console.log("ā„¹ļø No events to import");
508
+ return;
509
+ }
510
+
511
+ // Import events in batches
512
+ const batchSize = 50;
513
+ let totalImported = 0;
514
+
515
+ for (let i = 0; i < events.length; i += batchSize) {
516
+ const batch = events.slice(i, i + batchSize);
517
+ const batchNumber = Math.floor(i / batchSize) + 1;
518
+ const totalBatches = Math.ceil(events.length / batchSize);
519
+
520
+ console.log(
521
+ `\nšŸ“¦ Processing batch ${batchNumber}/${totalBatches} (${batch.length} events)`,
522
+ );
523
+
524
+ // Build batch SQL
525
+ const eventValues = batch
526
+ .map((event) => {
527
+ const updatedAt = event.created_at; // Use same timestamp for updated_at
528
+
529
+ return `(
530
+ ${sqlValue(tagId)},
531
+ ${siteId},
532
+ ${teamId},
533
+ ${sqlValue(event.bot_data)},
534
+ ${sqlValue(event.browser)},
535
+ ${sqlValue(event.city)},
536
+ ${sqlValue(event.client_page_url)},
537
+ ${sqlValue(event.country)},
538
+ ${event.created_at},
539
+ ${updatedAt},
540
+ ${sqlValue(event.custom_data)},
541
+ ${sqlValue(event.device_type)},
542
+ ${sqlValue(event.event)},
543
+ ${sqlValue(event.operating_system)},
544
+ ${sqlValue(event.page_url)},
545
+ ${sqlValue(event.postal)},
546
+ ${sqlValue(event.query_params)},
547
+ ${sqlValue(event.referer)},
548
+ ${sqlValue(event.region)},
549
+ ${sqlValue(event.rid)},
550
+ ${event.screen_height || "NULL"},
551
+ ${event.screen_width || "NULL"}
552
+ )`;
553
+ })
554
+ .join(",\n");
555
+
556
+ const batchSQL = `
557
+ INSERT INTO site_events (
558
+ tag_id, site_id, team_id, bot_data, browser, city, client_page_url, country,
559
+ created_at, updated_at, custom_data, device_type, event, operating_system,
560
+ page_url, postal, query_params, referer, region, rid, screen_height, screen_width
561
+ )
562
+ VALUES ${eventValues};
563
+ `;
564
+
565
+ executeSQL(batchSQL, `Importing batch ${batchNumber}/${totalBatches}`);
566
+ totalImported += batch.length;
567
+ }
568
+
569
+ console.log("\nāœ… Event import complete!");
570
+ console.log(`
571
+ šŸ“Š Summary:
572
+ Events imported: ${totalImported}
573
+ Site ID: ${siteId}
574
+ Tag ID: ${tagId}
575
+ Team ID: ${teamId}
576
+ Database: ${database} (${isLocal ? "local" : "remote"})
577
+ `);
578
+
579
+ console.log(`
580
+ šŸš€ Next steps:
581
+ 1. Start the dev server: bun run dev
582
+ 2. Login and check the analytics dashboard
583
+ 3. Verify the events appear for the imported site
584
+ 4. Use the data for testing and analysis
585
+ `);
586
+ } catch (error) {
587
+ console.error("āŒ Error importing events:", error);
588
+ process.exit(1);
589
+ }
590
+ }
591
+
592
+ // Validate required arguments
593
+ if (!teamId || isNaN(teamId)) {
594
+ console.error("āŒ Error: --team-id is required and must be a number");
595
+ console.log("Use --help for usage information");
596
+ process.exit(1);
597
+ }
598
+
599
+ if (!siteId || isNaN(siteId)) {
600
+ console.error("āŒ Error: --site-id is required and must be a number");
601
+ console.log("Use --help for usage information");
602
+ process.exit(1);
603
+ }
604
+
605
+ // Validate data source
606
+ if (fromDb) {
607
+ if (!fromSiteId || isNaN(fromSiteId)) {
608
+ console.error(
609
+ "āŒ Error: --from-site-id is required and must be a number when using --from-db",
610
+ );
611
+ console.log("Use --help for usage information");
612
+ process.exit(1);
613
+ }
614
+ } else {
615
+ // Check if stdin has data (when not run interactively)
616
+ if (process.stdin.isTTY) {
617
+ console.error(
618
+ "āŒ Error: No data provided via stdin and --from-db not specified",
619
+ );
620
+ console.log(
621
+ "Please pipe JSON data to this command or use --from-db option. Use --help for examples.",
622
+ );
623
+ process.exit(1);
624
+ }
625
+ }
626
+
627
+ // Run the import
628
+ importEvents();