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.
- package/.env.example +37 -0
- package/README.md +486 -0
- package/alchemy.run.ts +155 -0
- package/cli/bootstrap-admin.ts +284 -0
- package/cli/deploy-staging.ts +692 -0
- package/cli/import-events.ts +628 -0
- package/cli/import-sites.ts +518 -0
- package/cli/index.ts +609 -0
- package/cli/init-db.ts +269 -0
- package/cli/migrate-to-durable-objects.ts +564 -0
- package/cli/migration-worker.ts +300 -0
- package/cli/performance-test.ts +588 -0
- package/cli/pg/client.ts +4 -0
- package/cli/pg/new-site.ts +153 -0
- package/cli/rollback-durable-objects.ts +622 -0
- package/cli/seed-data.ts +459 -0
- package/cli/setup.js +18 -0
- package/cli/setup.ts +463 -0
- package/cli/validate-migration.ts +200 -0
- package/cli/wrangler-migration.jsonc +28 -0
- package/db/adapter.ts +166 -0
- package/db/analytics_engine/client.ts +0 -0
- package/db/analytics_engine/sites.ts +0 -0
- package/db/client.ts +16 -0
- package/db/d1/client.ts +8 -0
- package/db/d1/drizzle.config.ts +35 -0
- package/db/d1/migrations/0000_true_maelstrom.sql +165 -0
- package/db/d1/migrations/0001_wonderful_bloodaxe.sql +12 -0
- package/db/d1/migrations/0002_late_frightful_four.sql +1 -0
- package/db/d1/migrations/0003_cuddly_obadiah_stane.sql +16 -0
- package/db/d1/migrations/0004_mute_stardust.sql +1 -0
- package/db/d1/migrations/0005_awesome_silvermane.sql +3 -0
- package/db/d1/migrations/0006_volatile_shriek.sql +2 -0
- package/db/d1/migrations/0007_superb_lila_cheney.sql +1 -0
- package/db/d1/migrations/0008_bitter_longshot.sql +17 -0
- package/db/d1/migrations/0009_wonderful_madame_masque.sql +28 -0
- package/db/d1/migrations/meta/0000_snapshot.json +1112 -0
- package/db/d1/migrations/meta/0001_snapshot.json +1187 -0
- package/db/d1/migrations/meta/0002_snapshot.json +1194 -0
- package/db/d1/migrations/meta/0003_snapshot.json +1296 -0
- package/db/d1/migrations/meta/0004_snapshot.json +1303 -0
- package/db/d1/migrations/meta/0005_snapshot.json +1325 -0
- package/db/d1/migrations/meta/0006_snapshot.json +1339 -0
- package/db/d1/migrations/meta/0007_snapshot.json +1347 -0
- package/db/d1/migrations/meta/0008_snapshot.json +1464 -0
- package/db/d1/migrations/meta/0009_snapshot.json +1648 -0
- package/db/d1/migrations/meta/_journal.json +76 -0
- package/db/d1/schema.ts +407 -0
- package/db/d1/sites.ts +374 -0
- package/db/d1/teamAiUsage.ts +101 -0
- package/db/d1/teams.ts +127 -0
- package/db/durable/drizzle.config.ts +8 -0
- package/db/durable/durableObjectClient.ts +480 -0
- package/db/durable/events.ts +100 -0
- package/db/durable/migrations/0000_fair_bucky.sql +38 -0
- package/db/durable/migrations/meta/0000_snapshot.json +278 -0
- package/db/durable/migrations/meta/_journal.json +13 -0
- package/db/durable/migrations/migrations.js +10 -0
- package/db/durable/schema.ts +5 -0
- package/db/durable/siteDurableObject.ts +1352 -0
- package/db/durable/types.ts +53 -0
- package/db/postgres/client.ts +13 -0
- package/db/postgres/drizzle.config.ts +12 -0
- package/db/postgres/migrations/0000_brainy_sprite.sql +116 -0
- package/db/postgres/migrations/meta/0000_snapshot.json +681 -0
- package/db/postgres/migrations/meta/_journal.json +13 -0
- package/db/postgres/schema.ts +145 -0
- package/db/postgres/sites.ts +118 -0
- package/db/tranformReports.ts +595 -0
- package/db/types.ts +55 -0
- package/endpoints/api_worker.tsx +1854 -0
- package/endpoints/site_do_worker.ts +11 -0
- package/index.d.ts +63 -0
- package/index.ts +83 -0
- package/lib/auth.ts +279 -0
- package/lib/geojson/world_countries.json +45307 -0
- package/lib/random_name.ts +41 -0
- package/lib/sendMail.ts +252 -0
- package/package.json +142 -0
- package/public/favicon.ico +0 -0
- package/public/images/android-chrome-192x192.png +0 -0
- package/public/images/android-chrome-512x512.png +0 -0
- package/public/images/apple-touch-icon.png +0 -0
- package/public/images/favicon-16x16.png +0 -0
- package/public/images/favicon-32x32.png +0 -0
- package/public/images/lytx_dark_dashboard.png +0 -0
- package/public/images/lytx_light_dashboard.png +0 -0
- package/public/images/safari-pinned-tab.svg +4 -0
- package/public/logo.png +0 -0
- package/public/site.webmanifest +26 -0
- package/public/sw.js +107 -0
- package/src/Document.tsx +86 -0
- package/src/api/ai_api.ts +1156 -0
- package/src/api/authMiddleware.ts +45 -0
- package/src/api/auth_api.ts +465 -0
- package/src/api/event_labels_api.ts +193 -0
- package/src/api/events_api.ts +210 -0
- package/src/api/queueWorker.ts +303 -0
- package/src/api/reports_api.ts +278 -0
- package/src/api/seed_api.ts +288 -0
- package/src/api/sites_api.ts +904 -0
- package/src/api/tag_api.ts +458 -0
- package/src/api/tag_api_v2.ts +289 -0
- package/src/api/team_api.ts +456 -0
- package/src/app/Dashboard.tsx +1339 -0
- package/src/app/Events.tsx +974 -0
- package/src/app/Explore.tsx +312 -0
- package/src/app/Layout.tsx +58 -0
- package/src/app/Settings.tsx +1302 -0
- package/src/app/components/DashboardCard.tsx +118 -0
- package/src/app/components/EditableCell.tsx +123 -0
- package/src/app/components/EventForm.tsx +93 -0
- package/src/app/components/MarketingFooter.tsx +49 -0
- package/src/app/components/MarketingNav.tsx +150 -0
- package/src/app/components/Nav.tsx +755 -0
- package/src/app/components/NewSiteSetup.tsx +298 -0
- package/src/app/components/SQLEditor.tsx +740 -0
- package/src/app/components/SiteSelector.tsx +126 -0
- package/src/app/components/SiteTag.tsx +42 -0
- package/src/app/components/SiteTagInstallCard.tsx +241 -0
- package/src/app/components/WorldMapCard.tsx +337 -0
- package/src/app/components/charts/ChartComponents.tsx +1481 -0
- package/src/app/components/charts/EventFunnel.tsx +45 -0
- package/src/app/components/charts/EventSummary.tsx +194 -0
- package/src/app/components/charts/SankeyFlows.tsx +72 -0
- package/src/app/components/marketing/CheckIcon.tsx +16 -0
- package/src/app/components/marketing/MarketingLayout.tsx +23 -0
- package/src/app/components/marketing/SectionHeading.tsx +35 -0
- package/src/app/components/reports/AskAiWorkspace.tsx +371 -0
- package/src/app/components/reports/CreateReportStarter.tsx +74 -0
- package/src/app/components/reports/DashboardRouteFiltersContext.tsx +14 -0
- package/src/app/components/reports/DashboardToolbar.tsx +154 -0
- package/src/app/components/reports/DashboardWorkspaceLayout.tsx +63 -0
- package/src/app/components/reports/DashboardWorkspaceShell.tsx +118 -0
- package/src/app/components/reports/ReportBuilderWorkspace.tsx +76 -0
- package/src/app/components/reports/custom/CustomReportBuilderPage.tsx +1667 -0
- package/src/app/components/reports/custom/ReportWidgetChart.tsx +297 -0
- package/src/app/components/reports/custom/buildWidgetSql.ts +151 -0
- package/src/app/components/reports/custom/chartPalettes.ts +18 -0
- package/src/app/components/reports/custom/types.ts +50 -0
- package/src/app/components/reports/reportBuilderMenuItems.ts +17 -0
- package/src/app/components/reports/useDashboardToolbarControls.tsx +235 -0
- package/src/app/components/ui/AlertBanner.tsx +101 -0
- package/src/app/components/ui/Button.tsx +55 -0
- package/src/app/components/ui/Card.tsx +80 -0
- package/src/app/components/ui/Input.tsx +72 -0
- package/src/app/components/ui/Link.tsx +23 -0
- package/src/app/components/ui/ReportBuilderMenu.tsx +246 -0
- package/src/app/components/ui/ThemeToggle.tsx +54 -0
- package/src/app/constants.ts +6 -0
- package/src/app/headers.ts +33 -0
- package/src/app/providers/AuthProvider.tsx +189 -0
- package/src/app/providers/ClientProviders.tsx +18 -0
- package/src/app/providers/QueryProvider.tsx +23 -0
- package/src/app/providers/ThemeProvider.tsx +88 -0
- package/src/app/utils/chartThemes.ts +146 -0
- package/src/app/utils/keybinds.ts +96 -0
- package/src/app/utils/media.tsx +24 -0
- package/src/client.tsx +114 -0
- package/src/config/createLytxAppConfig.ts +252 -0
- package/src/config/resourceNames.ts +88 -0
- package/src/db/index.ts +67 -0
- package/src/index.css +285 -0
- package/src/lib/featureFlags.ts +69 -0
- package/src/pages/GetStarted.tsx +290 -0
- package/src/pages/Home.tsx +268 -0
- package/src/pages/Login.tsx +283 -0
- package/src/pages/PrivacyPolicy.tsx +120 -0
- package/src/pages/Signup.tsx +267 -0
- package/src/pages/TermsOfService.tsx +126 -0
- package/src/pages/VerifyEmail.tsx +56 -0
- package/src/session/durableObject.ts +7 -0
- package/src/session/siteSchema.ts +86 -0
- package/src/session/types.ts +36 -0
- package/src/templates/README.md +80 -0
- package/src/templates/cleanFunctions.js +44 -0
- package/src/templates/embedFunctions.js +52 -0
- package/src/templates/lytx-shared.ts +662 -0
- package/src/templates/lytxpixel-core.ts +144 -0
- package/src/templates/lytxpixel.ts +267 -0
- package/src/templates/lytxpixelBrowser.js +634 -0
- package/src/templates/lytxpixelBrowser.mjs +634 -0
- package/src/templates/parseData.js +12 -0
- package/src/templates/script.ts +31 -0
- package/src/templates/template.tsx +50 -0
- package/src/templates/test.js +3 -0
- package/src/templates/trackWebEvents.ts +177 -0
- package/src/templates/vendors/clickcease.ts +8 -0
- package/src/templates/vendors/google.ts +174 -0
- package/src/templates/vendors/linkedin.ts +23 -0
- package/src/templates/vendors/meta.ts +56 -0
- package/src/templates/vendors/quantcast.ts +22 -0
- package/src/templates/vendors/simplfi.ts +7 -0
- package/src/types/app-context.ts +16 -0
- package/src/utilities/dashboardParams.ts +188 -0
- package/src/utilities/dashboardQueries.ts +537 -0
- package/src/utilities/dashboardTransforms.ts +167 -0
- package/src/utilities/dataValidation.ts +414 -0
- package/src/utilities/detector.ts +73 -0
- package/src/utilities/encrypt.ts +103 -0
- package/src/utilities/index.ts +13 -0
- package/src/utilities/parser.ts +117 -0
- package/src/utilities/performanceMonitoring.ts +570 -0
- package/src/utilities/route_interuptors.ts +24 -0
- package/src/worker.tsx +675 -0
- package/tsconfig.json +78 -0
- package/types/env.d.ts +16 -0
- package/types/rw.d.ts +7 -0
- package/types/shims.d.ts +53 -0
- package/types/vite.d.ts +19 -0
- package/vite/vite-plugin-pixel-bundle.ts +126 -0
- package/vite.config.ts +53 -0
- 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();
|