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,564 @@
1
+ #!/usr/bin/env bun
2
+ import { execSync } from "child_process";
3
+ import { writeFileSync, unlinkSync } from "fs";
4
+ import { join } from "path";
5
+ import { drizzle } from "drizzle-orm/postgres-js";
6
+ import postgres from "postgres";
7
+ import { siteEvents as pgSiteEvents } from "@db/postgres/schema";
8
+ import { eq } from "drizzle-orm";
9
+
10
+ // Parse CLI arguments
11
+ const args = process.argv.slice(2);
12
+ const getArg = (flag: string, defaultValue?: string): string => {
13
+ const index = args.indexOf(flag);
14
+ if (index === -1) {
15
+ if (defaultValue !== undefined) return defaultValue;
16
+ throw new Error(`Missing required argument: ${flag}`);
17
+ }
18
+ const value = args[index + 1];
19
+ if (!value || value.startsWith("-")) {
20
+ throw new Error(`Invalid value for ${flag}`);
21
+ }
22
+ return value;
23
+ };
24
+
25
+ const hasFlag = (flag: string): boolean => args.includes(flag);
26
+
27
+ // Check for help flag first
28
+ if (hasFlag("--help") || hasFlag("-h")) {
29
+ console.log(`
30
+ Usage: bun run cli/migrate-to-durable-objects.ts [options]
31
+
32
+ Description:
33
+ Migrate existing siteEvents data from D1/Postgres databases to per-site durable objects.
34
+ This script coordinates migration by calling a temporary migration worker that has access
35
+ to the Cloudflare Workers environment and durable objects.
36
+
37
+ Prerequisites:
38
+ 1. Deploy or run the migration worker:
39
+ - Local: wrangler dev --config cli/wrangler-migration.jsonc
40
+ - Remote: wrangler deploy --config cli/wrangler-migration.jsonc
41
+ 2. Update the worker URL in this script if needed
42
+
43
+ Options:
44
+ -s, --site-id <id> Migrate specific site ID (required unless --all-sites)
45
+ -t, --team-id <id> Migrate all sites for team ID (use with --all-sites)
46
+ -d, --database <name> D1 database name (default: "lytx_core_db")
47
+ --local Use local database (default: true)
48
+ --remote Use remote database (default: false)
49
+ --from-postgres <url> Migrate from PostgreSQL database (provide connection string)
50
+ --all-sites Migrate all sites for the specified team
51
+ --batch-size <size> Events per batch (default: 50)
52
+ --dry-run Show what would be migrated without actually migrating
53
+ --verify Verify migration by comparing record counts
54
+ --cleanup Remove original data after successful migration (DANGEROUS)
55
+ --force Skip confirmation prompts
56
+ -h, --help Show this help message
57
+
58
+ Migration Sources:
59
+ 1. D1 Database (default):
60
+ Migrates siteEvents from the local D1 database to durable objects
61
+
62
+ 2. PostgreSQL Database:
63
+ Use --from-postgres with connection string to migrate from Postgres
64
+
65
+ Examples:
66
+ # Migrate specific site from D1 (local)
67
+ bun run cli/migrate-to-durable-objects.ts --site-id 123 --local
68
+
69
+ # Migrate all sites for a team from D1 (remote)
70
+ bun run cli/migrate-to-durable-objects.ts --team-id 5 --all-sites --remote
71
+
72
+ # Migrate from PostgreSQL database
73
+ bun run cli/migrate-to-durable-objects.ts --site-id 123 --from-postgres "postgresql://user:pass@host:5432/db"
74
+
75
+ # Dry run to see what would be migrated
76
+ bun run cli/migrate-to-durable-objects.ts --site-id 123 --dry-run
77
+
78
+ # Migrate with verification and cleanup
79
+ bun run cli/migrate-to-durable-objects.ts --site-id 123 --verify --cleanup --force
80
+
81
+ Safety Features:
82
+ - Batch processing to avoid memory issues
83
+ - Data validation before migration
84
+ - Verification option to compare record counts
85
+ - Dry run mode to preview migration
86
+ - Rollback capability (keeps original data by default)
87
+ `);
88
+ process.exit(0);
89
+ }
90
+
91
+ // CLI argument parsing
92
+ const getSiteIdArg = () => {
93
+ try {
94
+ return parseInt(getArg("--site-id"));
95
+ } catch {
96
+ try {
97
+ return parseInt(getArg("-s"));
98
+ } catch {
99
+ return null;
100
+ }
101
+ }
102
+ };
103
+
104
+ const getTeamIdArg = () => {
105
+ try {
106
+ return parseInt(getArg("--team-id"));
107
+ } catch {
108
+ try {
109
+ return parseInt(getArg("-t"));
110
+ } catch {
111
+ return null;
112
+ }
113
+ }
114
+ };
115
+
116
+ const getDatabaseArg = () => {
117
+ try {
118
+ return getArg("--database");
119
+ } catch {
120
+ try {
121
+ return getArg("-d");
122
+ } catch {
123
+ return "lytx_core_db";
124
+ }
125
+ }
126
+ };
127
+
128
+ const getFromPostgresArg = () => {
129
+ try {
130
+ return getArg("--from-postgres");
131
+ } catch {
132
+ return null;
133
+ }
134
+ };
135
+
136
+ const getBatchSizeArg = () => {
137
+ try {
138
+ return parseInt(getArg("--batch-size"));
139
+ } catch {
140
+ return 50;
141
+ }
142
+ };
143
+
144
+ const siteId = getSiteIdArg();
145
+ const teamId = getTeamIdArg();
146
+ const database = getDatabaseArg();
147
+ const fromPostgres = getFromPostgresArg();
148
+ const batchSize = getBatchSizeArg();
149
+ const isRemote = hasFlag("--remote");
150
+ const isLocal = hasFlag("--local") || !isRemote;
151
+ const allSites = hasFlag("--all-sites");
152
+ const dryRun = hasFlag("--dry-run");
153
+ const verify = hasFlag("--verify");
154
+ const cleanup = hasFlag("--cleanup");
155
+ const force = hasFlag("--force");
156
+
157
+ // Validation
158
+ if (!siteId && !allSites) {
159
+ console.error("โŒ Error: Either --site-id or --all-sites is required");
160
+ process.exit(1);
161
+ }
162
+
163
+ if (allSites && !teamId) {
164
+ console.error("โŒ Error: --team-id is required when using --all-sites");
165
+ process.exit(1);
166
+ }
167
+
168
+ if (cleanup && !force) {
169
+ console.error("โŒ Error: --cleanup requires --force flag for safety");
170
+ process.exit(1);
171
+ }
172
+
173
+ // Event interface for migration
174
+ interface MigrationEvent {
175
+ id?: number;
176
+ team_id?: number;
177
+ site_id?: number;
178
+ bot_data?: object;
179
+ browser?: string;
180
+ city?: string;
181
+ client_page_url?: string;
182
+ country?: string;
183
+ created_at?: Date;
184
+ updated_at?: Date;
185
+ custom_data?: object;
186
+ device_type?: string;
187
+ event: string;
188
+ operating_system?: string;
189
+ page_url?: string;
190
+ postal?: string;
191
+ query_params?: object;
192
+ referer?: string;
193
+ region?: string;
194
+ rid?: string;
195
+ screen_height?: number;
196
+ screen_width?: number;
197
+ tag_id: string;
198
+ }
199
+
200
+ // Helper function to execute SQL via wrangler
201
+ function executeSQL(sql: string, description: string) {
202
+ console.log(`๐Ÿ“ ${description}...`);
203
+
204
+ const tempFile = join(process.cwd(), `temp_${Date.now()}.sql`);
205
+ writeFileSync(tempFile, sql);
206
+
207
+ try {
208
+ const command = `bunx wrangler d1 execute ${database} --file ${tempFile} ${isLocal ? "--local" : "--remote"} --yes`;
209
+ const result = execSync(command, { encoding: "utf8", stdio: "pipe" });
210
+ console.log(`โœ… ${description} completed`);
211
+ return result;
212
+ } catch (error: any) {
213
+ console.error(`โŒ Error during ${description}:`, error.message);
214
+ throw error;
215
+ } finally {
216
+ try {
217
+ unlinkSync(tempFile);
218
+ } catch (e) {
219
+ // Ignore cleanup errors
220
+ }
221
+ }
222
+ }
223
+
224
+ // Helper function to query D1 database
225
+ async function queryD1(sql: string): Promise<any[]> {
226
+ const tempFile = join(process.cwd(), `temp_query_${Date.now()}.sql`);
227
+ writeFileSync(tempFile, sql);
228
+
229
+ try {
230
+ const command = `bunx wrangler d1 execute ${database} --file ${tempFile} ${isLocal ? "--local" : "--remote"} --json --yes`;
231
+ const result = execSync(command, { encoding: "utf8", stdio: "pipe" });
232
+
233
+ // Extract JSON from wrangler output
234
+ const jsonStart = result.indexOf("[");
235
+ const jsonEnd = result.lastIndexOf("]") + 1;
236
+
237
+ if (jsonStart === -1 || jsonEnd === 0) {
238
+ throw new Error("No JSON found in wrangler output");
239
+ }
240
+
241
+ const jsonString = result.substring(jsonStart, jsonEnd);
242
+ const jsonResult = JSON.parse(jsonString);
243
+
244
+ if (!jsonResult || !jsonResult[0] || !jsonResult[0].results) {
245
+ throw new Error(`Unexpected response format from wrangler`);
246
+ }
247
+
248
+ return jsonResult[0].results;
249
+ } catch (error: any) {
250
+ console.error("โŒ Error querying D1:", error.message);
251
+ throw error;
252
+ } finally {
253
+ try {
254
+ unlinkSync(tempFile);
255
+ } catch (e) {
256
+ // Ignore cleanup errors
257
+ }
258
+ }
259
+ }
260
+
261
+ // Get sites to migrate
262
+ async function getSitesToMigrate(): Promise<Array<{ site_id: number; tag_id: string; name?: string }>> {
263
+ if (siteId) {
264
+ // Single site migration
265
+ const sites = await queryD1(`SELECT site_id, tag_id, name FROM sites WHERE site_id = ${siteId};`);
266
+ if (sites.length === 0) {
267
+ throw new Error(`Site ID ${siteId} not found`);
268
+ }
269
+ return sites;
270
+ } else if (allSites && teamId) {
271
+ // All sites for team migration
272
+ const sites = await queryD1(`SELECT site_id, tag_id, name FROM sites WHERE team_id = ${teamId};`);
273
+ if (sites.length === 0) {
274
+ throw new Error(`No sites found for team ID ${teamId}`);
275
+ }
276
+ return sites;
277
+ }
278
+ return [];
279
+ }
280
+
281
+ // Read events from D1 database
282
+ async function readEventsFromD1(siteId: number): Promise<MigrationEvent[]> {
283
+ console.log(`๐Ÿ“– Reading events from D1 for site ${siteId}...`);
284
+
285
+ const events = await queryD1(`
286
+ SELECT * FROM site_events
287
+ WHERE site_id = ${siteId}
288
+ ORDER BY created_at ASC
289
+ `);
290
+
291
+ console.log(`โœ… Found ${events.length} events in D1 for site ${siteId}`);
292
+ return events;
293
+ }
294
+
295
+ // Read events from PostgreSQL database
296
+ async function readEventsFromPostgres(siteId: number, connectionString: string): Promise<MigrationEvent[]> {
297
+ console.log(`๐Ÿ“– Reading events from PostgreSQL for site ${siteId}...`);
298
+
299
+ const sql = postgres(connectionString);
300
+ const db = drizzle(sql);
301
+
302
+ try {
303
+ const events = await db
304
+ .select()
305
+ .from(pgSiteEvents)
306
+ .where(eq(pgSiteEvents.site_id, siteId))
307
+ .orderBy(pgSiteEvents.created_at);
308
+
309
+ console.log(`โœ… Found ${events.length} events in PostgreSQL for site ${siteId}`);
310
+
311
+ await sql.end();
312
+ return events as MigrationEvent[];
313
+ } catch (error: any) {
314
+ await sql.end();
315
+ throw new Error(`Failed to read from PostgreSQL: ${error.message}`, { cause: error });
316
+ }
317
+ }
318
+
319
+ // Transform events for durable object (remove team_id, site_id)
320
+ function transformEventsForDurableObject(events: MigrationEvent[]): any[] {
321
+ return events.map(event => ({
322
+ bot_data: event.bot_data,
323
+ browser: event.browser,
324
+ city: event.city,
325
+ client_page_url: event.client_page_url,
326
+ country: event.country,
327
+ createdAt: event.created_at || new Date(),
328
+ updatedAt: event.updated_at || new Date(),
329
+ custom_data: event.custom_data,
330
+ device_type: event.device_type,
331
+ event: event.event,
332
+ operating_system: event.operating_system,
333
+ page_url: event.page_url,
334
+ postal: event.postal,
335
+ query_params: event.query_params,
336
+ referer: event.referer,
337
+ region: event.region,
338
+ rid: event.rid,
339
+ screen_height: event.screen_height,
340
+ screen_width: event.screen_width,
341
+ tag_id: event.tag_id,
342
+ }));
343
+ }
344
+
345
+ // Write events to durable object using the migration worker
346
+ async function writeEventsToDurableObject(siteId: number, events: any[]): Promise<void> {
347
+ console.log(`๐Ÿ“ Writing ${events.length} events to durable object Site-${siteId}...`);
348
+
349
+ try {
350
+ // Use the migration worker endpoint
351
+ const workerUrl = isLocal
352
+ ? 'http://localhost:8787' // Local migration worker
353
+ : 'https://migration.lytx.workers.dev'; // Deployed migration worker
354
+
355
+ // Call the migration worker's migrate-site endpoint
356
+ const response = await fetch(`${workerUrl}/migrate-site`, {
357
+ method: 'POST',
358
+ headers: {
359
+ 'Content-Type': 'application/json'
360
+ },
361
+ body: JSON.stringify({
362
+ siteId,
363
+ batchSize: Math.min(events.length, batchSize),
364
+ dryRun: false,
365
+ verify: false
366
+ })
367
+ });
368
+
369
+ if (!response.ok) {
370
+ const errorText = await response.text();
371
+ throw new Error(`HTTP ${response.status}: ${errorText}`);
372
+ }
373
+
374
+ const result = await response.json() as any;
375
+
376
+ if (!result.migration?.success) {
377
+ const errors = result.migration?.errors?.join(', ') || 'Unknown error';
378
+ throw new Error(`Migration failed: ${errors}`);
379
+ }
380
+
381
+ console.log(`โœ… Successfully migrated ${result.migration.migratedEvents} events to durable object Site-${siteId}`);
382
+
383
+ } catch (error: any) {
384
+ console.error(`โŒ Failed to migrate to durable object Site-${siteId}:`, error.message);
385
+ throw error;
386
+ }
387
+ }
388
+
389
+ // Verify migration using the migration worker
390
+ async function verifyMigration(siteId: number, originalCount: number): Promise<boolean> {
391
+ console.log(`๐Ÿ” Verifying migration for site ${siteId}...`);
392
+
393
+ try {
394
+ // Use the migration worker endpoint
395
+ const workerUrl = isLocal
396
+ ? 'http://localhost:8787' // Local migration worker
397
+ : 'https://migration.lytx.workers.dev'; // Deployed migration worker
398
+
399
+ // Call the migration worker's verify endpoint
400
+ const response = await fetch(`${workerUrl}/verify-site/${siteId}?expectedCount=${originalCount}`, {
401
+ method: 'GET',
402
+ headers: {
403
+ 'Content-Type': 'application/json'
404
+ }
405
+ });
406
+
407
+ if (!response.ok) {
408
+ const errorText = await response.text();
409
+ throw new Error(`HTTP ${response.status}: ${errorText}`);
410
+ }
411
+
412
+ const result = await response.json() as { success: boolean; actualCount: number; expectedCount: number };
413
+
414
+ if (result.success) {
415
+ console.log(`โœ… Verification passed: ${result.actualCount} events in durable object (expected: ${result.expectedCount})`);
416
+ return true;
417
+ } else {
418
+ console.error(`โŒ Verification failed: Expected ${result.expectedCount}, found ${result.actualCount}`);
419
+ return false;
420
+ }
421
+
422
+ } catch (error: any) {
423
+ console.error(`โŒ Verification failed for site ${siteId}:`, error.message);
424
+ return false;
425
+ }
426
+ }
427
+
428
+ // Clean up original data
429
+ async function cleanupOriginalData(siteId: number): Promise<void> {
430
+ if (!cleanup) return;
431
+
432
+ console.log(`๐Ÿงน Cleaning up original data for site ${siteId}...`);
433
+
434
+ if (fromPostgres) {
435
+ // TODO: Delete from PostgreSQL
436
+ console.log(`Would delete PostgreSQL data for site ${siteId}`);
437
+ } else {
438
+ // Delete from D1
439
+ executeSQL(`DELETE FROM site_events WHERE site_id = ${siteId};`, `Cleaning up D1 data for site ${siteId}`);
440
+ }
441
+ }
442
+
443
+ // Main migration function
444
+ async function migrateSite(site: { site_id: number; tag_id: string; name?: string }): Promise<void> {
445
+ const { site_id, tag_id, name } = site;
446
+
447
+ console.log(`\n๐Ÿš€ Starting migration for site ${site_id} (${name || 'Unnamed'}) - Tag: ${tag_id}`);
448
+
449
+ try {
450
+ // Read events from source
451
+ let events: MigrationEvent[];
452
+ if (fromPostgres) {
453
+ events = await readEventsFromPostgres(site_id, fromPostgres);
454
+ } else {
455
+ events = await readEventsFromD1(site_id);
456
+ }
457
+
458
+ if (events.length === 0) {
459
+ console.log(`โ„น๏ธ No events found for site ${site_id}, skipping...`);
460
+ return;
461
+ }
462
+
463
+ // Transform events for durable object
464
+ const transformedEvents = transformEventsForDurableObject(events);
465
+
466
+ if (dryRun) {
467
+ console.log(`๐Ÿ” DRY RUN: Would migrate ${transformedEvents.length} events for site ${site_id}`);
468
+ console.log(` Sample event:`, JSON.stringify(transformedEvents[0], null, 2));
469
+ return;
470
+ }
471
+
472
+ // Process in batches
473
+ for (let i = 0; i < transformedEvents.length; i += batchSize) {
474
+ const batch = transformedEvents.slice(i, i + batchSize);
475
+ const batchNumber = Math.floor(i / batchSize) + 1;
476
+ const totalBatches = Math.ceil(transformedEvents.length / batchSize);
477
+
478
+ console.log(`๐Ÿ“ฆ Processing batch ${batchNumber}/${totalBatches} (${batch.length} events)`);
479
+ await writeEventsToDurableObject(site_id, batch);
480
+ }
481
+
482
+ // Verify migration if requested
483
+ if (verify) {
484
+ const verified = await verifyMigration(site_id, events.length);
485
+ if (!verified) {
486
+ throw new Error(`Migration verification failed for site ${site_id}`);
487
+ }
488
+ }
489
+
490
+ // Cleanup original data if requested
491
+ await cleanupOriginalData(site_id);
492
+
493
+ console.log(`โœ… Migration completed successfully for site ${site_id}`);
494
+
495
+ } catch (error) {
496
+ console.error(`โŒ Migration failed for site ${site_id}:`, error);
497
+ throw error;
498
+ }
499
+ }
500
+
501
+ // Main execution
502
+ async function main() {
503
+ try {
504
+ console.log("๐Ÿš€ Starting durable objects migration...");
505
+ console.log(`๐Ÿ“Š Source: ${fromPostgres ? 'PostgreSQL' : 'D1'} (${isLocal ? 'local' : 'remote'})`);
506
+ console.log(`๐Ÿ“ฆ Batch size: ${batchSize}`);
507
+ console.log(`๐Ÿ” Mode: ${dryRun ? 'DRY RUN' : 'LIVE MIGRATION'}`);
508
+
509
+ // Get sites to migrate
510
+ const sites = await getSitesToMigrate();
511
+ console.log(`๐Ÿ“‹ Found ${sites.length} site(s) to migrate`);
512
+
513
+ // Confirmation prompt (unless forced)
514
+ if (!force && !dryRun) {
515
+ console.log(`\nโš ๏ธ About to migrate ${sites.length} site(s). This will:`);
516
+ console.log(` - Read events from ${fromPostgres ? 'PostgreSQL' : 'D1'}`);
517
+ console.log(` - Write events to durable objects`);
518
+ if (verify) console.log(` - Verify migration by comparing counts`);
519
+ if (cleanup) console.log(` - DELETE original data after migration`);
520
+ console.log(`\nPress Ctrl+C to cancel, or any key to continue...`);
521
+
522
+ // Wait for user input
523
+ process.stdin.setRawMode(true);
524
+ process.stdin.resume();
525
+ await new Promise(resolve => process.stdin.once('data', resolve));
526
+ process.stdin.setRawMode(false);
527
+ process.stdin.pause();
528
+ }
529
+
530
+ // Migrate each site
531
+ let successCount = 0;
532
+ let failureCount = 0;
533
+
534
+ for (const site of sites) {
535
+ try {
536
+ await migrateSite(site);
537
+ successCount++;
538
+ } catch (error) {
539
+ console.error(`Failed to migrate site ${site.site_id}:`, error);
540
+ failureCount++;
541
+ }
542
+ }
543
+
544
+ // Summary
545
+ console.log(`\n๐Ÿ“Š Migration Summary:`);
546
+ console.log(` โœ… Successful: ${successCount}`);
547
+ console.log(` โŒ Failed: ${failureCount}`);
548
+ console.log(` ๐Ÿ“‹ Total: ${sites.length}`);
549
+
550
+ if (failureCount > 0) {
551
+ console.log(`\nโš ๏ธ Some migrations failed. Check the logs above for details.`);
552
+ process.exit(1);
553
+ } else {
554
+ console.log(`\n๐ŸŽ‰ All migrations completed successfully!`);
555
+ }
556
+
557
+ } catch (error) {
558
+ console.error("โŒ Migration failed:", error);
559
+ process.exit(1);
560
+ }
561
+ }
562
+
563
+ // Run the migration
564
+ main();