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,904 @@
|
|
|
1
|
+
import { route } from "rwsdk/router";
|
|
2
|
+
import type { RequestInfo } from "rwsdk/worker";
|
|
3
|
+
import type { AppContext } from "@/types/app-context";
|
|
4
|
+
import { checkIfTeamSetupSites } from "@/utilities/route_interuptors";
|
|
5
|
+
import { getSiteFromContext } from "@/api/authMiddleware";
|
|
6
|
+
import { IS_DEV } from "rwsdk/constants";
|
|
7
|
+
// import { auth } from "@lib/auth";
|
|
8
|
+
import {
|
|
9
|
+
DashboardResponseData,
|
|
10
|
+
getDeviceData,
|
|
11
|
+
getDeviceGeoData,
|
|
12
|
+
getEventTypesData,
|
|
13
|
+
getPageViewsData,
|
|
14
|
+
getReferrersData,
|
|
15
|
+
getTopPagesData,
|
|
16
|
+
getTopSourcesData,
|
|
17
|
+
} from "@db/tranformReports";
|
|
18
|
+
import { DashboardOptions } from "@db/types";
|
|
19
|
+
import { getDashboardAggregatesFromDurableObject, getDurableDatabaseStub, getEventSummaryFromDurableObject } from "@db/durable/durableObjectClient";
|
|
20
|
+
import {
|
|
21
|
+
isDateOnly,
|
|
22
|
+
isValidTimeZone,
|
|
23
|
+
parseDateParam,
|
|
24
|
+
parseSiteIdParam,
|
|
25
|
+
} from "@/utilities/dashboardParams";
|
|
26
|
+
|
|
27
|
+
const DASHBOARD_CACHE_TTL_SECONDS = IS_DEV ? 5 : 30;
|
|
28
|
+
const DASHBOARD_CACHE_STALE_SECONDS = 30;
|
|
29
|
+
|
|
30
|
+
type EventSummaryTypeFilter = "all" | "autocapture" | "event_capture" | "page_view";
|
|
31
|
+
type EventSummaryActionFilter = "all" | "click" | "submit" | "change" | "rule";
|
|
32
|
+
type EventSummarySortBy = "count" | "first_seen" | "last_seen";
|
|
33
|
+
type EventSummarySortDirection = "asc" | "desc";
|
|
34
|
+
|
|
35
|
+
function formatDuration(seconds: number): string {
|
|
36
|
+
if (!Number.isFinite(seconds) || seconds <= 0) return "0s";
|
|
37
|
+
if (seconds >= 60) {
|
|
38
|
+
const minutes = Math.floor(seconds / 60);
|
|
39
|
+
const remainingSeconds = Math.floor(seconds % 60);
|
|
40
|
+
return `${minutes}m ${remainingSeconds}s`;
|
|
41
|
+
}
|
|
42
|
+
return `${Math.floor(seconds)}s`;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function cleanRefererValue(referer: string): string {
|
|
46
|
+
if (!referer) return "Direct";
|
|
47
|
+
if (referer === "null") return "Direct";
|
|
48
|
+
return referer.replace(/https?:\/\//, "").replace(/\/.*/, "") || "Direct";
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function shouldBypassDashboardCache(request: Request): boolean {
|
|
52
|
+
const cacheControl = request.headers.get("Cache-Control")?.toLowerCase() ?? "";
|
|
53
|
+
return (
|
|
54
|
+
cacheControl.includes("no-cache") ||
|
|
55
|
+
cacheControl.includes("no-store") ||
|
|
56
|
+
cacheControl.includes("max-age=0")
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
async function sha256Hex(input: string): Promise<string> {
|
|
61
|
+
const bytes = new TextEncoder().encode(input);
|
|
62
|
+
const digest = await crypto.subtle.digest("SHA-256", bytes);
|
|
63
|
+
return Array.from(new Uint8Array(digest))
|
|
64
|
+
.map((b) => b.toString(16).padStart(2, "0"))
|
|
65
|
+
.join("");
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* GET /api/world_countries
|
|
70
|
+
*
|
|
71
|
+
* This is the world countries API endpoint
|
|
72
|
+
*/
|
|
73
|
+
export const world_countries = route(
|
|
74
|
+
"/world_countries",
|
|
75
|
+
async ({ request }: RequestInfo<any, AppContext>) => {
|
|
76
|
+
if (request.method != "POST") {
|
|
77
|
+
return new Response("Not Found.", { status: 404 });
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const include = (await request.json()) as { include: string[] };
|
|
81
|
+
const world_countries = await import("@lib/geojson/world_countries.json");
|
|
82
|
+
|
|
83
|
+
if (include.include.length > 0) {
|
|
84
|
+
const filtered = world_countries.features.filter((feature) => {
|
|
85
|
+
return include.include.includes(feature.properties.name);
|
|
86
|
+
});
|
|
87
|
+
return new Response(JSON.stringify(filtered));
|
|
88
|
+
}
|
|
89
|
+
//consider r2 bucket?
|
|
90
|
+
return new Response(JSON.stringify(world_countries));
|
|
91
|
+
},
|
|
92
|
+
);
|
|
93
|
+
|
|
94
|
+
export const getCurrentVisitorsRoute = route(
|
|
95
|
+
"/dashboard/current-visitors",
|
|
96
|
+
[
|
|
97
|
+
checkIfTeamSetupSites,
|
|
98
|
+
async ({ request, ctx }: RequestInfo<any, AppContext>) => {
|
|
99
|
+
const requestId = crypto.randomUUID();
|
|
100
|
+
if (request.method !== "GET") {
|
|
101
|
+
return new Response(
|
|
102
|
+
JSON.stringify({ error: "Method not allowed", requestId }),
|
|
103
|
+
{
|
|
104
|
+
status: 405,
|
|
105
|
+
headers: { "Content-Type": "application/json" },
|
|
106
|
+
},
|
|
107
|
+
);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const url = new URL(request.url);
|
|
111
|
+
const site_id = url.searchParams.get("site_id");
|
|
112
|
+
const windowSecondsParam = url.searchParams.get("windowSeconds");
|
|
113
|
+
|
|
114
|
+
const windowSeconds = windowSecondsParam
|
|
115
|
+
? Math.max(1, parseInt(windowSecondsParam, 10))
|
|
116
|
+
: 60 * 5;
|
|
117
|
+
|
|
118
|
+
if (!site_id) {
|
|
119
|
+
return new Response(
|
|
120
|
+
JSON.stringify({ error: "site_id is required", requestId }),
|
|
121
|
+
{
|
|
122
|
+
status: 400,
|
|
123
|
+
headers: { "Content-Type": "application/json" },
|
|
124
|
+
},
|
|
125
|
+
);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const siteIdValue = parseInt(site_id, 10);
|
|
129
|
+
if (Number.isNaN(siteIdValue)) {
|
|
130
|
+
return new Response(
|
|
131
|
+
JSON.stringify({ error: "site_id must be a valid number", requestId }),
|
|
132
|
+
{
|
|
133
|
+
status: 400,
|
|
134
|
+
headers: { "Content-Type": "application/json" },
|
|
135
|
+
},
|
|
136
|
+
);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
const siteDetails = getSiteFromContext(ctx, siteIdValue);
|
|
140
|
+
if (!siteDetails?.uuid) {
|
|
141
|
+
return new Response(
|
|
142
|
+
JSON.stringify({ error: "Site not found", requestId }),
|
|
143
|
+
{
|
|
144
|
+
status: 404,
|
|
145
|
+
headers: { "Content-Type": "application/json" },
|
|
146
|
+
},
|
|
147
|
+
);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
try {
|
|
151
|
+
const stub = await getDurableDatabaseStub(siteDetails.uuid, siteIdValue);
|
|
152
|
+
const result = await stub.getCurrentVisitors({ windowSeconds });
|
|
153
|
+
|
|
154
|
+
return new Response(JSON.stringify(result), {
|
|
155
|
+
headers: { "Content-Type": "application/json" },
|
|
156
|
+
});
|
|
157
|
+
} catch (error) {
|
|
158
|
+
console.error("Current visitors API error:", { requestId, error });
|
|
159
|
+
return new Response(
|
|
160
|
+
JSON.stringify({ error: "Internal server error", requestId }),
|
|
161
|
+
{
|
|
162
|
+
status: 500,
|
|
163
|
+
headers: { "Content-Type": "application/json" },
|
|
164
|
+
},
|
|
165
|
+
);
|
|
166
|
+
}
|
|
167
|
+
},
|
|
168
|
+
],
|
|
169
|
+
);
|
|
170
|
+
|
|
171
|
+
type DashboardDataCoreInput = {
|
|
172
|
+
ctx: AppContext;
|
|
173
|
+
requestId: string;
|
|
174
|
+
siteIdValue: number;
|
|
175
|
+
dateStartValue: Date | null;
|
|
176
|
+
dateEndValue: Date | null;
|
|
177
|
+
rawDateEnd: unknown;
|
|
178
|
+
normalizedTimezone: string | null;
|
|
179
|
+
normalizedDeviceType: string | null;
|
|
180
|
+
normalizedCountry: string | null;
|
|
181
|
+
normalizedSource: string | null;
|
|
182
|
+
normalizedPageUrl: string | null;
|
|
183
|
+
normalizedCity: string | null;
|
|
184
|
+
normalizedRegion: string | null;
|
|
185
|
+
normalizedEventName: string | null;
|
|
186
|
+
normalizedEventSummaryLimit: number;
|
|
187
|
+
normalizedEventSummaryOffset: number;
|
|
188
|
+
eventSummarySearch: string;
|
|
189
|
+
normalizedEventSummaryType: EventSummaryTypeFilter;
|
|
190
|
+
normalizedEventSummaryAction: EventSummaryActionFilter;
|
|
191
|
+
normalizedEventSummarySortBy: EventSummarySortBy;
|
|
192
|
+
normalizedEventSummarySortDirection: EventSummarySortDirection;
|
|
193
|
+
};
|
|
194
|
+
|
|
195
|
+
type DashboardDataCoreResult =
|
|
196
|
+
| { ok: true; data: DashboardResponseData }
|
|
197
|
+
| { ok: false; status: number; error: string };
|
|
198
|
+
|
|
199
|
+
export async function getDashboardDataCore(input: DashboardDataCoreInput): Promise<DashboardDataCoreResult> {
|
|
200
|
+
const {
|
|
201
|
+
ctx,
|
|
202
|
+
siteIdValue,
|
|
203
|
+
dateStartValue,
|
|
204
|
+
dateEndValue,
|
|
205
|
+
rawDateEnd,
|
|
206
|
+
normalizedTimezone,
|
|
207
|
+
normalizedDeviceType,
|
|
208
|
+
normalizedCountry,
|
|
209
|
+
normalizedSource,
|
|
210
|
+
normalizedPageUrl,
|
|
211
|
+
normalizedCity,
|
|
212
|
+
normalizedRegion,
|
|
213
|
+
normalizedEventName,
|
|
214
|
+
normalizedEventSummaryLimit,
|
|
215
|
+
normalizedEventSummaryOffset,
|
|
216
|
+
eventSummarySearch,
|
|
217
|
+
normalizedEventSummaryType,
|
|
218
|
+
normalizedEventSummaryAction,
|
|
219
|
+
normalizedEventSummarySortBy,
|
|
220
|
+
normalizedEventSummarySortDirection,
|
|
221
|
+
} = input;
|
|
222
|
+
|
|
223
|
+
if (IS_DEV) console.log("🔥🔥🔥 site_id", siteIdValue);
|
|
224
|
+
|
|
225
|
+
const dashboardOptions: DashboardOptions = {
|
|
226
|
+
site_id: siteIdValue,
|
|
227
|
+
site_uuid: "",
|
|
228
|
+
team_id: ctx.team.id,
|
|
229
|
+
};
|
|
230
|
+
|
|
231
|
+
const endDateIsDateOnly = isDateOnly(rawDateEnd);
|
|
232
|
+
const endDateIsExact = !endDateIsDateOnly || !!normalizedTimezone;
|
|
233
|
+
if (dateStartValue || dateEndValue) {
|
|
234
|
+
dashboardOptions.date = {
|
|
235
|
+
start: dateStartValue ?? undefined,
|
|
236
|
+
end: dateEndValue ?? undefined,
|
|
237
|
+
endIsExact: endDateIsExact,
|
|
238
|
+
};
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
const siteDetails = getSiteFromContext(ctx, siteIdValue);
|
|
242
|
+
if (!siteDetails || !siteDetails.uuid) {
|
|
243
|
+
return { ok: false, status: 404, error: "Site not found" };
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
let db_adapter = ctx.db_adapter;
|
|
247
|
+
dashboardOptions.site_uuid = siteDetails.uuid;
|
|
248
|
+
if (siteDetails.site_db_adapter != ctx.db_adapter) {
|
|
249
|
+
db_adapter = siteDetails.site_db_adapter;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
if (db_adapter != "sqlite") {
|
|
253
|
+
if (siteDetails.external_id > 0) {
|
|
254
|
+
dashboardOptions.site_id = siteDetails.external_id;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
const externalTeamId = ctx.team.external_id ?? 0;
|
|
258
|
+
if (externalTeamId > 0) {
|
|
259
|
+
dashboardOptions.team_id = externalTeamId;
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
const eventSummary = await getEventSummaryFromDurableObject({
|
|
264
|
+
...dashboardOptions,
|
|
265
|
+
date: {
|
|
266
|
+
start: dateStartValue ?? undefined,
|
|
267
|
+
end: dateEndValue ?? undefined,
|
|
268
|
+
endIsExact: endDateIsExact,
|
|
269
|
+
},
|
|
270
|
+
limit: normalizedEventSummaryLimit,
|
|
271
|
+
offset: normalizedEventSummaryOffset,
|
|
272
|
+
search: eventSummarySearch,
|
|
273
|
+
type: normalizedEventSummaryType,
|
|
274
|
+
action: normalizedEventSummaryAction,
|
|
275
|
+
sortBy: normalizedEventSummarySortBy,
|
|
276
|
+
sortDirection: normalizedEventSummarySortDirection,
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
const dashboardAggregates = await getDashboardAggregatesFromDurableObject({
|
|
280
|
+
...dashboardOptions,
|
|
281
|
+
date: {
|
|
282
|
+
start: dateStartValue ?? undefined,
|
|
283
|
+
end: dateEndValue ?? undefined,
|
|
284
|
+
endIsExact: endDateIsExact,
|
|
285
|
+
},
|
|
286
|
+
timezone: normalizedTimezone ?? undefined,
|
|
287
|
+
country: normalizedCountry ?? undefined,
|
|
288
|
+
deviceType: normalizedDeviceType ?? undefined,
|
|
289
|
+
source: normalizedSource ?? undefined,
|
|
290
|
+
pageUrl: normalizedPageUrl ?? undefined,
|
|
291
|
+
city: normalizedCity ?? undefined,
|
|
292
|
+
region: normalizedRegion ?? undefined,
|
|
293
|
+
event: normalizedEventName ?? undefined,
|
|
294
|
+
});
|
|
295
|
+
|
|
296
|
+
if (!dashboardAggregates) {
|
|
297
|
+
return { ok: false, status: 404, error: "No data found" };
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
const pageViewsData = getPageViewsData(dashboardAggregates.pageViews);
|
|
301
|
+
|
|
302
|
+
const referers = Array.from(
|
|
303
|
+
dashboardAggregates.referers.reduce((acc, item) => {
|
|
304
|
+
const normalizedReferer = cleanRefererValue(item.id);
|
|
305
|
+
const current = acc.get(normalizedReferer) ?? 0;
|
|
306
|
+
acc.set(normalizedReferer, current + item.value);
|
|
307
|
+
return acc;
|
|
308
|
+
}, new Map<string, number>()),
|
|
309
|
+
)
|
|
310
|
+
.toSorted((a, b) => b[1] - a[1])
|
|
311
|
+
.map(([id, value]) => ({ id, value }));
|
|
312
|
+
|
|
313
|
+
const topSourcesData = getTopSourcesData(
|
|
314
|
+
referers
|
|
315
|
+
.slice(0, 10)
|
|
316
|
+
.map((a) => ({ name: a.id, visitors: a.value })),
|
|
317
|
+
);
|
|
318
|
+
|
|
319
|
+
const referrersData = getReferrersData(referers.slice(0, 10));
|
|
320
|
+
|
|
321
|
+
const eventTypesData = getEventTypesData(dashboardAggregates.events);
|
|
322
|
+
|
|
323
|
+
const geoData = dashboardAggregates.cities
|
|
324
|
+
.map(([city, details]) => [details.country, city, details.count]) as Array<[string, string, number]>;
|
|
325
|
+
|
|
326
|
+
const deviceGeoData = getDeviceGeoData({
|
|
327
|
+
geoData,
|
|
328
|
+
deviceData: dashboardAggregates.devices,
|
|
329
|
+
});
|
|
330
|
+
|
|
331
|
+
const topPagesData = getTopPagesData(dashboardAggregates.topPages.slice(0, 10));
|
|
332
|
+
|
|
333
|
+
const browserData = getDeviceData(
|
|
334
|
+
dashboardAggregates.browsers.map((a) => ({
|
|
335
|
+
name: a.id,
|
|
336
|
+
visitors: a.value,
|
|
337
|
+
percentage: "",
|
|
338
|
+
})),
|
|
339
|
+
);
|
|
340
|
+
|
|
341
|
+
const osData = getDeviceData(
|
|
342
|
+
dashboardAggregates.operatingSystems.map((a) => ({
|
|
343
|
+
name: a.id,
|
|
344
|
+
visitors: a.value,
|
|
345
|
+
percentage: "",
|
|
346
|
+
})),
|
|
347
|
+
);
|
|
348
|
+
|
|
349
|
+
const scoreCards = [
|
|
350
|
+
{
|
|
351
|
+
title: "Uniques",
|
|
352
|
+
value: `${dashboardAggregates.scoreCards.uniqueVisitors.toLocaleString()}`,
|
|
353
|
+
change: "",
|
|
354
|
+
changeType: "neutral",
|
|
355
|
+
changeLabel: "",
|
|
356
|
+
},
|
|
357
|
+
{
|
|
358
|
+
title: "Total Page Views",
|
|
359
|
+
value: `${dashboardAggregates.scoreCards.totalPageViews.toLocaleString()}`,
|
|
360
|
+
change: "",
|
|
361
|
+
changeType: "neutral",
|
|
362
|
+
changeLabel: "",
|
|
363
|
+
},
|
|
364
|
+
{
|
|
365
|
+
title: "Bounce Rate",
|
|
366
|
+
value: `${dashboardAggregates.scoreCards.bounceRatePercent.toFixed(1)}%`,
|
|
367
|
+
change: "",
|
|
368
|
+
changeType: "neutral",
|
|
369
|
+
changeLabel: "",
|
|
370
|
+
},
|
|
371
|
+
{
|
|
372
|
+
title: "Conversion Rate",
|
|
373
|
+
value: `${dashboardAggregates.scoreCards.conversionRatePercent.toFixed(2)}%`,
|
|
374
|
+
change: "",
|
|
375
|
+
changeType: "neutral",
|
|
376
|
+
changeLabel: "",
|
|
377
|
+
},
|
|
378
|
+
{
|
|
379
|
+
title: "Total Events",
|
|
380
|
+
value: `${dashboardAggregates.scoreCards.nonPageViewEvents.toLocaleString()}`,
|
|
381
|
+
change: "",
|
|
382
|
+
changeType: "neutral",
|
|
383
|
+
changeLabel: "",
|
|
384
|
+
},
|
|
385
|
+
{
|
|
386
|
+
title: "Avg Session Duration",
|
|
387
|
+
value: formatDuration(dashboardAggregates.scoreCards.avgSessionDurationSeconds),
|
|
388
|
+
change: "",
|
|
389
|
+
changeType: "neutral",
|
|
390
|
+
changeLabel: "",
|
|
391
|
+
},
|
|
392
|
+
] as DashboardResponseData["ScoreCards"];
|
|
393
|
+
|
|
394
|
+
return {
|
|
395
|
+
ok: true,
|
|
396
|
+
data: {
|
|
397
|
+
noSiteRecordsExist: dashboardAggregates.totalAllTime === 0,
|
|
398
|
+
PageViewsData: pageViewsData,
|
|
399
|
+
EventTypesData: eventTypesData,
|
|
400
|
+
DeviceGeoData: deviceGeoData,
|
|
401
|
+
ReferrersData: referrersData,
|
|
402
|
+
TopPagesData: topPagesData,
|
|
403
|
+
TopSourcesData: topSourcesData,
|
|
404
|
+
ScoreCards: scoreCards,
|
|
405
|
+
BrowserData: browserData.slice(0, 10),
|
|
406
|
+
OSData: osData.slice(0, 10),
|
|
407
|
+
Countries: dashboardAggregates.countries,
|
|
408
|
+
CountryUniques: dashboardAggregates.countryUniques,
|
|
409
|
+
Pagination: dashboardAggregates.pagination,
|
|
410
|
+
Regions: dashboardAggregates.regions,
|
|
411
|
+
EventSummary: eventSummary,
|
|
412
|
+
},
|
|
413
|
+
};
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
export const getDashboardDataRoute = route(
|
|
417
|
+
"/dashboard/data",
|
|
418
|
+
[
|
|
419
|
+
checkIfTeamSetupSites,
|
|
420
|
+
async ({ request, ctx, cf }: RequestInfo<any, AppContext>) => {
|
|
421
|
+
const requestId = crypto.randomUUID();
|
|
422
|
+
if (request.method !== "POST") {
|
|
423
|
+
return new Response(
|
|
424
|
+
JSON.stringify({ error: "Method not allowed", requestId }),
|
|
425
|
+
{
|
|
426
|
+
status: 405,
|
|
427
|
+
headers: { "Content-Type": "application/json" },
|
|
428
|
+
},
|
|
429
|
+
);
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
const bypassCache = shouldBypassDashboardCache(request);
|
|
433
|
+
|
|
434
|
+
try {
|
|
435
|
+
const url = new URL(request.url);
|
|
436
|
+
|
|
437
|
+
let params: any = {};
|
|
438
|
+
try {
|
|
439
|
+
params = await request.json();
|
|
440
|
+
} catch {
|
|
441
|
+
return new Response(
|
|
442
|
+
JSON.stringify({ error: "Invalid JSON body", requestId }),
|
|
443
|
+
{
|
|
444
|
+
status: 400,
|
|
445
|
+
headers: { "Content-Type": "application/json" },
|
|
446
|
+
},
|
|
447
|
+
);
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
const {
|
|
451
|
+
site_id,
|
|
452
|
+
date_start,
|
|
453
|
+
date_end,
|
|
454
|
+
timezone,
|
|
455
|
+
device_type,
|
|
456
|
+
country,
|
|
457
|
+
source,
|
|
458
|
+
page_url,
|
|
459
|
+
city,
|
|
460
|
+
region,
|
|
461
|
+
event_name,
|
|
462
|
+
event_summary_offset,
|
|
463
|
+
event_summary_limit,
|
|
464
|
+
event_summary_search,
|
|
465
|
+
event_summary_type,
|
|
466
|
+
event_summary_action,
|
|
467
|
+
event_summary_sort_by,
|
|
468
|
+
event_summary_sort_direction,
|
|
469
|
+
} = params;
|
|
470
|
+
|
|
471
|
+
const requestedTimezone = typeof timezone === "string" ? timezone.trim() : "";
|
|
472
|
+
if (requestedTimezone.length > 0 && !isValidTimeZone(requestedTimezone)) {
|
|
473
|
+
return new Response(
|
|
474
|
+
JSON.stringify({ error: "timezone must be a valid IANA timezone", requestId }),
|
|
475
|
+
{
|
|
476
|
+
status: 400,
|
|
477
|
+
headers: { "Content-Type": "application/json" },
|
|
478
|
+
},
|
|
479
|
+
);
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
const sessionTimezone = (ctx.session as { timezone?: unknown } | undefined)?.timezone;
|
|
483
|
+
const normalizedTimezone = requestedTimezone.length > 0
|
|
484
|
+
? requestedTimezone
|
|
485
|
+
: isValidTimeZone(sessionTimezone)
|
|
486
|
+
? sessionTimezone
|
|
487
|
+
: null;
|
|
488
|
+
|
|
489
|
+
if (site_id === undefined || site_id === null || site_id === "") {
|
|
490
|
+
return new Response(
|
|
491
|
+
JSON.stringify({ error: "site_id is required", requestId }),
|
|
492
|
+
{
|
|
493
|
+
status: 400,
|
|
494
|
+
headers: { "Content-Type": "application/json" },
|
|
495
|
+
},
|
|
496
|
+
);
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
const siteIdValue = parseSiteIdParam(site_id);
|
|
500
|
+
if (siteIdValue === null) {
|
|
501
|
+
return new Response(
|
|
502
|
+
JSON.stringify({ error: "site_id must be a valid number", requestId }),
|
|
503
|
+
{
|
|
504
|
+
status: 400,
|
|
505
|
+
headers: { "Content-Type": "application/json" },
|
|
506
|
+
},
|
|
507
|
+
);
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
const dateStartValue = parseDateParam(date_start, {
|
|
511
|
+
timeZone: normalizedTimezone,
|
|
512
|
+
boundary: "start",
|
|
513
|
+
});
|
|
514
|
+
if (date_start && !dateStartValue) {
|
|
515
|
+
return new Response(
|
|
516
|
+
JSON.stringify({ error: "date_start must be a valid date", requestId }),
|
|
517
|
+
{
|
|
518
|
+
status: 400,
|
|
519
|
+
headers: { "Content-Type": "application/json" },
|
|
520
|
+
},
|
|
521
|
+
);
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
const dateEndValue = parseDateParam(date_end, {
|
|
525
|
+
timeZone: normalizedTimezone,
|
|
526
|
+
boundary: "end",
|
|
527
|
+
});
|
|
528
|
+
if (date_end && !dateEndValue) {
|
|
529
|
+
return new Response(
|
|
530
|
+
JSON.stringify({ error: "date_end must be a valid date", requestId }),
|
|
531
|
+
{
|
|
532
|
+
status: 400,
|
|
533
|
+
headers: { "Content-Type": "application/json" },
|
|
534
|
+
},
|
|
535
|
+
);
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
const normalizedDeviceType =
|
|
539
|
+
typeof device_type === "string" ? device_type.toLowerCase() : null;
|
|
540
|
+
const normalizedCountry =
|
|
541
|
+
typeof country === "string" ? country.toUpperCase() : null;
|
|
542
|
+
const normalizedSource = typeof source === "string" ? source.toLowerCase() : null;
|
|
543
|
+
const normalizedPageUrl = typeof page_url === "string" && page_url.length > 0 ? page_url : null;
|
|
544
|
+
const normalizedCity = typeof city === "string" && city.length > 0 ? city : null;
|
|
545
|
+
const normalizedRegion = typeof region === "string" && region.length > 0 ? region : null;
|
|
546
|
+
const normalizedEventName = typeof event_name === "string" && event_name.length > 0 ? event_name : null;
|
|
547
|
+
const eventSummaryOffset =
|
|
548
|
+
typeof event_summary_offset === "number"
|
|
549
|
+
? event_summary_offset
|
|
550
|
+
: Number(event_summary_offset ?? 0);
|
|
551
|
+
const eventSummaryLimit =
|
|
552
|
+
typeof event_summary_limit === "number"
|
|
553
|
+
? event_summary_limit
|
|
554
|
+
: Number(event_summary_limit ?? 50);
|
|
555
|
+
const eventSummarySearch =
|
|
556
|
+
typeof event_summary_search === "string"
|
|
557
|
+
? event_summary_search.trim()
|
|
558
|
+
: "";
|
|
559
|
+
const normalizedEventSummaryType: EventSummaryTypeFilter =
|
|
560
|
+
event_summary_type === "autocapture"
|
|
561
|
+
|| event_summary_type === "event_capture"
|
|
562
|
+
|| event_summary_type === "page_view"
|
|
563
|
+
? event_summary_type
|
|
564
|
+
: "all";
|
|
565
|
+
const normalizedEventSummaryAction: EventSummaryActionFilter =
|
|
566
|
+
event_summary_action === "click"
|
|
567
|
+
|| event_summary_action === "submit"
|
|
568
|
+
|| event_summary_action === "change"
|
|
569
|
+
|| event_summary_action === "rule"
|
|
570
|
+
? event_summary_action
|
|
571
|
+
: "all";
|
|
572
|
+
const normalizedEventSummarySortBy: EventSummarySortBy =
|
|
573
|
+
event_summary_sort_by === "first_seen"
|
|
574
|
+
|| event_summary_sort_by === "last_seen"
|
|
575
|
+
? event_summary_sort_by
|
|
576
|
+
: "count";
|
|
577
|
+
const normalizedEventSummarySortDirection: EventSummarySortDirection =
|
|
578
|
+
event_summary_sort_direction === "asc"
|
|
579
|
+
? "asc"
|
|
580
|
+
: "desc";
|
|
581
|
+
const normalizedEventSummaryOffset = Number.isFinite(eventSummaryOffset)
|
|
582
|
+
? Math.max(0, eventSummaryOffset)
|
|
583
|
+
: 0;
|
|
584
|
+
const normalizedEventSummaryLimit = Number.isFinite(eventSummaryLimit)
|
|
585
|
+
? Math.min(Math.max(1, eventSummaryLimit), 100)
|
|
586
|
+
: 50;
|
|
587
|
+
|
|
588
|
+
const cache = (caches as CacheStorage & { default: Cache }).default;
|
|
589
|
+
|
|
590
|
+
const cacheKeyPayload = {
|
|
591
|
+
teamId: ctx.team.id,
|
|
592
|
+
teamExternalId: ctx.team.external_id ?? null,
|
|
593
|
+
siteId: siteIdValue,
|
|
594
|
+
dateStart: dateStartValue ? dateStartValue.toISOString() : null,
|
|
595
|
+
dateEnd: dateEndValue ? dateEndValue.toISOString() : null,
|
|
596
|
+
deviceType: normalizedDeviceType,
|
|
597
|
+
country: normalizedCountry,
|
|
598
|
+
source: normalizedSource,
|
|
599
|
+
pageUrl: normalizedPageUrl,
|
|
600
|
+
city: normalizedCity,
|
|
601
|
+
region: normalizedRegion,
|
|
602
|
+
eventName: normalizedEventName,
|
|
603
|
+
eventSummaryOffset: normalizedEventSummaryOffset,
|
|
604
|
+
eventSummaryLimit: normalizedEventSummaryLimit,
|
|
605
|
+
eventSummarySearch,
|
|
606
|
+
eventSummaryType: normalizedEventSummaryType,
|
|
607
|
+
eventSummaryAction: normalizedEventSummaryAction,
|
|
608
|
+
eventSummarySortBy: normalizedEventSummarySortBy,
|
|
609
|
+
eventSummarySortDirection: normalizedEventSummarySortDirection,
|
|
610
|
+
timezone: normalizedTimezone,
|
|
611
|
+
dbAdapter: ctx.db_adapter,
|
|
612
|
+
};
|
|
613
|
+
|
|
614
|
+
const cacheKeyHash = await sha256Hex(JSON.stringify(cacheKeyPayload));
|
|
615
|
+
const cacheKeyUrl = new URL(url.toString());
|
|
616
|
+
cacheKeyUrl.searchParams.set("cache", cacheKeyHash);
|
|
617
|
+
|
|
618
|
+
// Cache API does not support POST keys directly.
|
|
619
|
+
const cacheKey = new Request(cacheKeyUrl.toString(), { method: "GET" });
|
|
620
|
+
|
|
621
|
+
if (!bypassCache) {
|
|
622
|
+
const cached = await cache.match(cacheKey);
|
|
623
|
+
if (cached) {
|
|
624
|
+
const cachedResponse = new Response(cached.body, cached);
|
|
625
|
+
cachedResponse.headers.set("X-Cache", "HIT");
|
|
626
|
+
return cachedResponse;
|
|
627
|
+
}
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
const dashboardDataResult = await getDashboardDataCore({
|
|
631
|
+
ctx,
|
|
632
|
+
requestId,
|
|
633
|
+
siteIdValue,
|
|
634
|
+
dateStartValue,
|
|
635
|
+
dateEndValue,
|
|
636
|
+
rawDateEnd: date_end,
|
|
637
|
+
normalizedTimezone,
|
|
638
|
+
normalizedDeviceType,
|
|
639
|
+
normalizedCountry,
|
|
640
|
+
normalizedSource,
|
|
641
|
+
normalizedPageUrl,
|
|
642
|
+
normalizedCity,
|
|
643
|
+
normalizedRegion,
|
|
644
|
+
normalizedEventName,
|
|
645
|
+
normalizedEventSummaryLimit,
|
|
646
|
+
normalizedEventSummaryOffset,
|
|
647
|
+
eventSummarySearch,
|
|
648
|
+
normalizedEventSummaryType,
|
|
649
|
+
normalizedEventSummaryAction,
|
|
650
|
+
normalizedEventSummarySortBy,
|
|
651
|
+
normalizedEventSummarySortDirection,
|
|
652
|
+
});
|
|
653
|
+
|
|
654
|
+
if (!dashboardDataResult.ok) {
|
|
655
|
+
return new Response(
|
|
656
|
+
JSON.stringify({ error: dashboardDataResult.error, requestId }),
|
|
657
|
+
{
|
|
658
|
+
status: dashboardDataResult.status,
|
|
659
|
+
headers: { "Content-Type": "application/json" },
|
|
660
|
+
},
|
|
661
|
+
);
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
const responseData = dashboardDataResult.data;
|
|
665
|
+
|
|
666
|
+
const headers = new Headers({ "Content-Type": "application/json" });
|
|
667
|
+
headers.set(
|
|
668
|
+
"Cache-Control",
|
|
669
|
+
`max-age=0, s-maxage=${DASHBOARD_CACHE_TTL_SECONDS}, stale-while-revalidate=${DASHBOARD_CACHE_STALE_SECONDS}`,
|
|
670
|
+
);
|
|
671
|
+
headers.set("X-Cache", bypassCache ? "BYPASS" : "MISS");
|
|
672
|
+
|
|
673
|
+
const response = new Response(JSON.stringify(responseData), { headers });
|
|
674
|
+
|
|
675
|
+
if (!bypassCache) {
|
|
676
|
+
cf.waitUntil(cache.put(cacheKey, response.clone()));
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
return response;
|
|
680
|
+
} catch (error) {
|
|
681
|
+
console.error("Dashboard request error:", { requestId, error });
|
|
682
|
+
return new Response(
|
|
683
|
+
JSON.stringify({ error: "Internal server error", requestId }),
|
|
684
|
+
{
|
|
685
|
+
status: 500,
|
|
686
|
+
headers: { "Content-Type": "application/json" },
|
|
687
|
+
},
|
|
688
|
+
);
|
|
689
|
+
}
|
|
690
|
+
},
|
|
691
|
+
],
|
|
692
|
+
);
|
|
693
|
+
|
|
694
|
+
export const siteEventsSqlRoute = route(
|
|
695
|
+
"/site-events/query",
|
|
696
|
+
[
|
|
697
|
+
checkIfTeamSetupSites,
|
|
698
|
+
async ({ request, ctx }: RequestInfo<any, AppContext>) => {
|
|
699
|
+
const requestId = crypto.randomUUID();
|
|
700
|
+
if (request.method !== "POST") {
|
|
701
|
+
return new Response(
|
|
702
|
+
JSON.stringify({ error: "Method not allowed", requestId }),
|
|
703
|
+
{
|
|
704
|
+
status: 405,
|
|
705
|
+
headers: { "Content-Type": "application/json" },
|
|
706
|
+
},
|
|
707
|
+
);
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
let body: { site_id?: unknown; query?: unknown; limit?: unknown } = {};
|
|
711
|
+
try {
|
|
712
|
+
body = await request.json();
|
|
713
|
+
} catch {
|
|
714
|
+
return new Response(
|
|
715
|
+
JSON.stringify({ error: "Invalid JSON body", requestId }),
|
|
716
|
+
{
|
|
717
|
+
status: 400,
|
|
718
|
+
headers: { "Content-Type": "application/json" },
|
|
719
|
+
},
|
|
720
|
+
);
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
if (body.site_id === undefined || body.site_id === null || body.site_id === "") {
|
|
724
|
+
return new Response(
|
|
725
|
+
JSON.stringify({ error: "site_id is required", requestId }),
|
|
726
|
+
{
|
|
727
|
+
status: 400,
|
|
728
|
+
headers: { "Content-Type": "application/json" },
|
|
729
|
+
},
|
|
730
|
+
);
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
const siteIdValue = parseSiteIdParam(body.site_id);
|
|
734
|
+
if (siteIdValue === null) {
|
|
735
|
+
return new Response(
|
|
736
|
+
JSON.stringify({ error: "site_id must be a valid number", requestId }),
|
|
737
|
+
{
|
|
738
|
+
status: 400,
|
|
739
|
+
headers: { "Content-Type": "application/json" },
|
|
740
|
+
},
|
|
741
|
+
);
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
const query = typeof body.query === "string" ? body.query.trim() : "";
|
|
745
|
+
if (!query) {
|
|
746
|
+
return new Response(
|
|
747
|
+
JSON.stringify({ error: "query is required", requestId }),
|
|
748
|
+
{
|
|
749
|
+
status: 400,
|
|
750
|
+
headers: { "Content-Type": "application/json" },
|
|
751
|
+
},
|
|
752
|
+
);
|
|
753
|
+
}
|
|
754
|
+
|
|
755
|
+
const siteDetails = getSiteFromContext(ctx, siteIdValue);
|
|
756
|
+
if (!siteDetails?.uuid) {
|
|
757
|
+
return new Response(
|
|
758
|
+
JSON.stringify({ error: "Site not found", requestId }),
|
|
759
|
+
{
|
|
760
|
+
status: 404,
|
|
761
|
+
headers: { "Content-Type": "application/json" },
|
|
762
|
+
},
|
|
763
|
+
);
|
|
764
|
+
}
|
|
765
|
+
|
|
766
|
+
const limit = typeof body.limit === "number" && Number.isFinite(body.limit)
|
|
767
|
+
? Math.max(1, Math.floor(body.limit))
|
|
768
|
+
: undefined;
|
|
769
|
+
|
|
770
|
+
try {
|
|
771
|
+
const stub = await getDurableDatabaseStub(siteDetails.uuid, siteIdValue);
|
|
772
|
+
const result = await stub.runSqlQuery(query, limit ? { limit } : undefined);
|
|
773
|
+
|
|
774
|
+
if (!result?.success) {
|
|
775
|
+
return new Response(
|
|
776
|
+
JSON.stringify({ error: result?.error || "Query failed", requestId }),
|
|
777
|
+
{
|
|
778
|
+
status: 400,
|
|
779
|
+
headers: { "Content-Type": "application/json" },
|
|
780
|
+
},
|
|
781
|
+
);
|
|
782
|
+
}
|
|
783
|
+
|
|
784
|
+
return new Response(
|
|
785
|
+
JSON.stringify({
|
|
786
|
+
rows: result.rows || [],
|
|
787
|
+
rowCount: result.rowCount ?? 0,
|
|
788
|
+
limit: result.limit,
|
|
789
|
+
}),
|
|
790
|
+
{
|
|
791
|
+
status: 200,
|
|
792
|
+
headers: { "Content-Type": "application/json" },
|
|
793
|
+
},
|
|
794
|
+
);
|
|
795
|
+
} catch (error) {
|
|
796
|
+
console.error("SQL query error:", { requestId, error });
|
|
797
|
+
return new Response(
|
|
798
|
+
JSON.stringify({ error: "Internal server error", requestId }),
|
|
799
|
+
{
|
|
800
|
+
status: 500,
|
|
801
|
+
headers: { "Content-Type": "application/json" },
|
|
802
|
+
},
|
|
803
|
+
);
|
|
804
|
+
}
|
|
805
|
+
},
|
|
806
|
+
],
|
|
807
|
+
);
|
|
808
|
+
|
|
809
|
+
export const siteEventsSchemaRoute = route(
|
|
810
|
+
"/site-events/schema",
|
|
811
|
+
[
|
|
812
|
+
checkIfTeamSetupSites,
|
|
813
|
+
async ({ request, ctx }: RequestInfo<any, AppContext>) => {
|
|
814
|
+
const requestId = crypto.randomUUID();
|
|
815
|
+
if (request.method !== "GET" && request.method !== "POST") {
|
|
816
|
+
return new Response(
|
|
817
|
+
JSON.stringify({ error: "Method not allowed", requestId }),
|
|
818
|
+
{
|
|
819
|
+
status: 405,
|
|
820
|
+
headers: { "Content-Type": "application/json" },
|
|
821
|
+
},
|
|
822
|
+
);
|
|
823
|
+
}
|
|
824
|
+
|
|
825
|
+
// Get site_id from query params (GET) or body (POST)
|
|
826
|
+
let siteIdValue: number | null = null;
|
|
827
|
+
|
|
828
|
+
if (request.method === "GET") {
|
|
829
|
+
const url = new URL(request.url);
|
|
830
|
+
const siteIdParam = url.searchParams.get("site_id");
|
|
831
|
+
siteIdValue = parseSiteIdParam(siteIdParam);
|
|
832
|
+
} else {
|
|
833
|
+
try {
|
|
834
|
+
const body = await request.json() as { site_id?: unknown };
|
|
835
|
+
siteIdValue = parseSiteIdParam(body.site_id);
|
|
836
|
+
} catch {
|
|
837
|
+
return new Response(
|
|
838
|
+
JSON.stringify({ error: "Invalid JSON body", requestId }),
|
|
839
|
+
{
|
|
840
|
+
status: 400,
|
|
841
|
+
headers: { "Content-Type": "application/json" },
|
|
842
|
+
},
|
|
843
|
+
);
|
|
844
|
+
}
|
|
845
|
+
}
|
|
846
|
+
|
|
847
|
+
if (siteIdValue === null) {
|
|
848
|
+
return new Response(
|
|
849
|
+
JSON.stringify({ error: "site_id is required and must be a valid number", requestId }),
|
|
850
|
+
{
|
|
851
|
+
status: 400,
|
|
852
|
+
headers: { "Content-Type": "application/json" },
|
|
853
|
+
},
|
|
854
|
+
);
|
|
855
|
+
}
|
|
856
|
+
|
|
857
|
+
const siteDetails = getSiteFromContext(ctx, siteIdValue);
|
|
858
|
+
if (!siteDetails?.uuid) {
|
|
859
|
+
return new Response(
|
|
860
|
+
JSON.stringify({ error: "Site not found", requestId }),
|
|
861
|
+
{
|
|
862
|
+
status: 404,
|
|
863
|
+
headers: { "Content-Type": "application/json" },
|
|
864
|
+
},
|
|
865
|
+
);
|
|
866
|
+
}
|
|
867
|
+
|
|
868
|
+
try {
|
|
869
|
+
const stub = await getDurableDatabaseStub(siteDetails.uuid, siteIdValue);
|
|
870
|
+
const result = await stub.getSchema();
|
|
871
|
+
|
|
872
|
+
if (!result?.success) {
|
|
873
|
+
return new Response(
|
|
874
|
+
JSON.stringify({ error: result?.error || "Failed to get schema", requestId }),
|
|
875
|
+
{
|
|
876
|
+
status: 500,
|
|
877
|
+
headers: { "Content-Type": "application/json" },
|
|
878
|
+
},
|
|
879
|
+
);
|
|
880
|
+
}
|
|
881
|
+
|
|
882
|
+
return new Response(
|
|
883
|
+
JSON.stringify({
|
|
884
|
+
tables: result.tables,
|
|
885
|
+
siteId: result.siteId,
|
|
886
|
+
}),
|
|
887
|
+
{
|
|
888
|
+
status: 200,
|
|
889
|
+
headers: { "Content-Type": "application/json" },
|
|
890
|
+
},
|
|
891
|
+
);
|
|
892
|
+
} catch (error) {
|
|
893
|
+
console.error("Schema fetch error:", { requestId, error });
|
|
894
|
+
return new Response(
|
|
895
|
+
JSON.stringify({ error: "Internal server error", requestId }),
|
|
896
|
+
{
|
|
897
|
+
status: 500,
|
|
898
|
+
headers: { "Content-Type": "application/json" },
|
|
899
|
+
},
|
|
900
|
+
);
|
|
901
|
+
}
|
|
902
|
+
},
|
|
903
|
+
],
|
|
904
|
+
);
|