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,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();
|