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,480 @@
|
|
|
1
|
+
import type { SiteEventInput, DashboardOptions } from "@db/durable/types";
|
|
2
|
+
import { AdapterResult } from "@db/types";
|
|
3
|
+
import { env } from "cloudflare:workers";
|
|
4
|
+
|
|
5
|
+
export interface DurableObjectStats {
|
|
6
|
+
totalEvents: number;
|
|
7
|
+
eventsByType: Array<{ event: string; count: number }>;
|
|
8
|
+
eventsByCountry: Array<{ country: string | null; count: number }>;
|
|
9
|
+
eventsByDevice: Array<{ device_type: string | null; count: number }>;
|
|
10
|
+
topReferers: Array<{ referer: string | null; count: number }>;
|
|
11
|
+
siteId: number;
|
|
12
|
+
dateRange: {
|
|
13
|
+
start?: string;
|
|
14
|
+
end?: string;
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface DashboardAggregates {
|
|
19
|
+
scoreCards: {
|
|
20
|
+
uniqueVisitors: number;
|
|
21
|
+
totalPageViews: number;
|
|
22
|
+
nonPageViewEvents: number;
|
|
23
|
+
bounceRatePercent: number;
|
|
24
|
+
conversionRatePercent: number;
|
|
25
|
+
avgSessionDurationSeconds: number;
|
|
26
|
+
};
|
|
27
|
+
pageViews: Array<{ x: string; y: number }>;
|
|
28
|
+
events: Array<[string, number]>;
|
|
29
|
+
devices: Array<[string, number]>;
|
|
30
|
+
cities: Array<[string, { count: number; country: string }]>;
|
|
31
|
+
countries: Array<{ id: string; value: number }>;
|
|
32
|
+
countryUniques: Array<{ id: string; value: number }>;
|
|
33
|
+
regions: Array<{ id: string; value: number }>;
|
|
34
|
+
referers: Array<{ id: string; value: number }>;
|
|
35
|
+
topPages: Array<{ id: string; value: number }>;
|
|
36
|
+
browsers: Array<{ id: string; value: number }>;
|
|
37
|
+
operatingSystems: Array<{ id: string; value: number }>;
|
|
38
|
+
pagination: { limit: number; offset: number; total: number; hasMore: boolean };
|
|
39
|
+
totalEvents: number;
|
|
40
|
+
totalAllTime: number;
|
|
41
|
+
siteId: number | null;
|
|
42
|
+
dateRange: { start?: string; end?: string };
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function asNumberTupleRows(value: unknown): Array<[string, number]> {
|
|
46
|
+
if (!Array.isArray(value)) return [];
|
|
47
|
+
return value
|
|
48
|
+
.filter((row): row is [unknown, unknown] => Array.isArray(row) && row.length >= 2)
|
|
49
|
+
.map((row) => [String(row[0]), Number(row[1]) || 0] as [string, number]);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function asCityTupleRows(value: unknown): Array<[string, { count: number; country: string }]> {
|
|
53
|
+
if (!Array.isArray(value)) return [];
|
|
54
|
+
return value
|
|
55
|
+
.filter((row): row is [unknown, unknown] => Array.isArray(row) && row.length >= 2)
|
|
56
|
+
.map((row) => {
|
|
57
|
+
const city = String(row[0]);
|
|
58
|
+
const payload = row[1] as { count?: unknown; country?: unknown };
|
|
59
|
+
return [
|
|
60
|
+
city,
|
|
61
|
+
{
|
|
62
|
+
count: Number(payload?.count) || 0,
|
|
63
|
+
country: typeof payload?.country === "string" ? payload.country : "Unknown",
|
|
64
|
+
},
|
|
65
|
+
] as [string, { count: number; country: string }];
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export async function getDurableDatabaseStub(site_uuid: string, site_id: number) {
|
|
70
|
+
|
|
71
|
+
const doId = env.SITE_DURABLE_OBJECT.idFromName(site_uuid);
|
|
72
|
+
const durableStub = env.SITE_DURABLE_OBJECT.get(doId);
|
|
73
|
+
await durableStub.setSiteInfo(site_id, site_uuid);
|
|
74
|
+
|
|
75
|
+
return durableStub;
|
|
76
|
+
|
|
77
|
+
}
|
|
78
|
+
export async function getDashboardDataFromDurableObject(options: DashboardOptions): Promise<AdapterResult<"sqlite">> {
|
|
79
|
+
try {
|
|
80
|
+
const stub = await getDurableDatabaseStub(options.site_uuid, options.site_id);
|
|
81
|
+
|
|
82
|
+
const data = await stub.getEventsData({
|
|
83
|
+
startDate: options.date?.start,
|
|
84
|
+
endDate: options.date?.end,
|
|
85
|
+
limit: options.events?.limit,
|
|
86
|
+
offset: options.events?.offset,
|
|
87
|
+
});
|
|
88
|
+
if (data.error || !data.events) {
|
|
89
|
+
console.error(`Durable object request failed: ${data.error}`);
|
|
90
|
+
return { query: null, client: null, noSiteRecordsExist: true, adapter: "sqlite" };
|
|
91
|
+
}
|
|
92
|
+
const { totalAllTime: totalAllTimeRaw, ...rest } = data;
|
|
93
|
+
const totalAllTime = typeof totalAllTimeRaw === "number" ? totalAllTimeRaw : null;
|
|
94
|
+
return {
|
|
95
|
+
query: {
|
|
96
|
+
site_id: options.site_id,
|
|
97
|
+
site_uuid: options.site_uuid,
|
|
98
|
+
...rest,
|
|
99
|
+
events: data.events || [],
|
|
100
|
+
pagination: data.pagination || { limit: 0, offset: 0, total: 0, hasMore: false }
|
|
101
|
+
},
|
|
102
|
+
adapter: "sqlite",
|
|
103
|
+
client: null,
|
|
104
|
+
noSiteRecordsExist: totalAllTime !== null ? totalAllTime === 0 : data.events.length === 0
|
|
105
|
+
};
|
|
106
|
+
} catch (error) {
|
|
107
|
+
console.error('Error fetching dashboard data from durable object:', error);
|
|
108
|
+
return { query: null, client: null, noSiteRecordsExist: true, adapter: "sqlite" };
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export async function getStatsFromDurableObject(
|
|
113
|
+
options: DashboardOptions
|
|
114
|
+
): Promise<DurableObjectStats | null> {
|
|
115
|
+
try {
|
|
116
|
+
const stub = await getDurableDatabaseStub(options.site_uuid, options.site_id);
|
|
117
|
+
|
|
118
|
+
const data = await stub.getStats({
|
|
119
|
+
startDate: options.date?.start,
|
|
120
|
+
endDate: options.date?.end
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
if (data.error) {
|
|
124
|
+
console.error(`Durable object stats request failed: ${data.error}`);
|
|
125
|
+
return null;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
return data as DurableObjectStats;
|
|
129
|
+
} catch (error) {
|
|
130
|
+
console.error('Error fetching stats from durable object:', error);
|
|
131
|
+
return null;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
export async function getDashboardAggregatesFromDurableObject(
|
|
136
|
+
options: DashboardOptions & {
|
|
137
|
+
timezone?: string;
|
|
138
|
+
country?: string;
|
|
139
|
+
deviceType?: string;
|
|
140
|
+
source?: string;
|
|
141
|
+
pageUrl?: string;
|
|
142
|
+
city?: string;
|
|
143
|
+
region?: string;
|
|
144
|
+
event?: string;
|
|
145
|
+
},
|
|
146
|
+
): Promise<DashboardAggregates | null> {
|
|
147
|
+
try {
|
|
148
|
+
const stub = await getDurableDatabaseStub(options.site_uuid, options.site_id);
|
|
149
|
+
|
|
150
|
+
const result = await stub.getDashboardAggregates({
|
|
151
|
+
startDate: options.date?.start,
|
|
152
|
+
endDate: options.date?.end,
|
|
153
|
+
endDateIsExact: options.date?.endIsExact,
|
|
154
|
+
timezone: options.timezone,
|
|
155
|
+
country: options.country,
|
|
156
|
+
deviceType: options.deviceType,
|
|
157
|
+
source: options.source,
|
|
158
|
+
pageUrl: options.pageUrl,
|
|
159
|
+
city: options.city,
|
|
160
|
+
region: options.region,
|
|
161
|
+
event: options.event,
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
if (result.error) {
|
|
165
|
+
console.error(`Durable object dashboard aggregates request failed: ${result.error}`);
|
|
166
|
+
return null;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
return {
|
|
170
|
+
...result,
|
|
171
|
+
siteId: result.siteId ?? options.site_id,
|
|
172
|
+
pageViews: result.pageViews ?? [],
|
|
173
|
+
events: asNumberTupleRows(result.events),
|
|
174
|
+
devices: asNumberTupleRows(result.devices),
|
|
175
|
+
cities: asCityTupleRows(result.cities),
|
|
176
|
+
countries: result.countries ?? [],
|
|
177
|
+
countryUniques: result.countryUniques ?? [],
|
|
178
|
+
referers: result.referers ?? [],
|
|
179
|
+
topPages: result.topPages ?? [],
|
|
180
|
+
regions: result.regions ?? [],
|
|
181
|
+
browsers: result.browsers ?? [],
|
|
182
|
+
operatingSystems: result.operatingSystems ?? [],
|
|
183
|
+
pagination: result.pagination ?? {
|
|
184
|
+
limit: 0,
|
|
185
|
+
offset: 0,
|
|
186
|
+
total: 0,
|
|
187
|
+
hasMore: false,
|
|
188
|
+
},
|
|
189
|
+
scoreCards: result.scoreCards ?? {
|
|
190
|
+
uniqueVisitors: 0,
|
|
191
|
+
totalPageViews: 0,
|
|
192
|
+
nonPageViewEvents: 0,
|
|
193
|
+
bounceRatePercent: 0,
|
|
194
|
+
conversionRatePercent: 0,
|
|
195
|
+
avgSessionDurationSeconds: 0,
|
|
196
|
+
},
|
|
197
|
+
totalEvents: result.totalEvents ?? 0,
|
|
198
|
+
totalAllTime: result.totalAllTime ?? 0,
|
|
199
|
+
dateRange: result.dateRange ?? { start: options.date?.start?.toISOString(), end: options.date?.end?.toISOString() },
|
|
200
|
+
};
|
|
201
|
+
} catch (error) {
|
|
202
|
+
console.error("Error fetching dashboard aggregates from durable object:", error);
|
|
203
|
+
return null;
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
export async function countEventsFromDurableObject(options: {
|
|
208
|
+
siteId: number;
|
|
209
|
+
siteUuid: string;
|
|
210
|
+
startDate: Date;
|
|
211
|
+
endDate?: Date;
|
|
212
|
+
}): Promise<number> {
|
|
213
|
+
try {
|
|
214
|
+
const stub = await getDurableDatabaseStub(options.siteUuid, options.siteId);
|
|
215
|
+
const result = await stub.countEventsSince({
|
|
216
|
+
startDate: options.startDate,
|
|
217
|
+
endDate: options.endDate,
|
|
218
|
+
});
|
|
219
|
+
if (result?.error) {
|
|
220
|
+
console.error(`Durable object count failed: ${result.error}`);
|
|
221
|
+
return 0;
|
|
222
|
+
}
|
|
223
|
+
return result?.count ?? 0;
|
|
224
|
+
} catch (error) {
|
|
225
|
+
console.error("Error counting events from durable object:", error);
|
|
226
|
+
return 0;
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
export async function writeToDurableObject(
|
|
231
|
+
siteId: number,
|
|
232
|
+
siteUuid: string,
|
|
233
|
+
events: SiteEventInput[],
|
|
234
|
+
): Promise<{ success: boolean; inserted?: number; error?: string }> {
|
|
235
|
+
try {
|
|
236
|
+
const stub = await getDurableDatabaseStub(siteUuid, siteId);
|
|
237
|
+
|
|
238
|
+
const result = await stub.insertEvents(events);
|
|
239
|
+
|
|
240
|
+
return {
|
|
241
|
+
success: result.success,
|
|
242
|
+
inserted: result.inserted,
|
|
243
|
+
error: result.error
|
|
244
|
+
};
|
|
245
|
+
} catch (error) {
|
|
246
|
+
console.error('Error writing to durable object:', error);
|
|
247
|
+
return {
|
|
248
|
+
success: false,
|
|
249
|
+
error: error instanceof Error ? error.message : 'Unknown error'
|
|
250
|
+
};
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
export async function checkDurableObjectHealth(
|
|
255
|
+
siteId: number,
|
|
256
|
+
siteUuid: string
|
|
257
|
+
): Promise<{ status: string; siteId: number; totalEvents: number; timestamp: string } | null> {
|
|
258
|
+
try {
|
|
259
|
+
const stub = await getDurableDatabaseStub(siteUuid, siteId);
|
|
260
|
+
|
|
261
|
+
const result = await stub.healthCheck();
|
|
262
|
+
|
|
263
|
+
if (result.error) {
|
|
264
|
+
console.error(`Durable object health check failed: ${result.error}`);
|
|
265
|
+
return null;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
return result as { status: string; siteId: number; totalEvents: number; timestamp: string };
|
|
269
|
+
} catch (error) {
|
|
270
|
+
console.error('Error checking durable object health:', error);
|
|
271
|
+
return null;
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
export async function cleanupDurableObjectEvents(
|
|
276
|
+
siteId: number,
|
|
277
|
+
siteUuid: string,
|
|
278
|
+
olderThan: Date
|
|
279
|
+
): Promise<{ success: boolean; deleted?: string; error?: string }> {
|
|
280
|
+
try {
|
|
281
|
+
const stub = await getDurableDatabaseStub(siteUuid, siteId);
|
|
282
|
+
|
|
283
|
+
const result = await stub.deleteEvents({
|
|
284
|
+
olderThan
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
return {
|
|
288
|
+
success: result.success,
|
|
289
|
+
deleted: result.deleted,
|
|
290
|
+
error: result.error
|
|
291
|
+
};
|
|
292
|
+
} catch (error) {
|
|
293
|
+
console.error('Error cleaning up durable object events:', error);
|
|
294
|
+
return {
|
|
295
|
+
success: false,
|
|
296
|
+
error: error instanceof Error ? error.message : 'Unknown error'
|
|
297
|
+
};
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
export async function getSiteInfo(
|
|
302
|
+
siteId: number,
|
|
303
|
+
_env: Env
|
|
304
|
+
): Promise<{ site_id: number; site_db_adapter: string; tag_id: string } | null> {
|
|
305
|
+
try {
|
|
306
|
+
return {
|
|
307
|
+
site_id: siteId,
|
|
308
|
+
site_db_adapter: 'sqlite',
|
|
309
|
+
tag_id: `site-${siteId}-tag`
|
|
310
|
+
};
|
|
311
|
+
} catch (error) {
|
|
312
|
+
console.error('Error getting site info:', error);
|
|
313
|
+
return null;
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
export async function batchWriteToDurableObjects(
|
|
318
|
+
eventsBySite: Map<number, { siteUuid: string; events: SiteEventInput[] }>
|
|
319
|
+
): Promise<Map<number, { success: boolean; inserted?: number; error?: string }>> {
|
|
320
|
+
const results = new Map();
|
|
321
|
+
|
|
322
|
+
const promises = Array.from(eventsBySite.entries()).map(async ([siteId, { siteUuid, events }]) => {
|
|
323
|
+
const result = await writeToDurableObject(siteId, siteUuid, events);
|
|
324
|
+
return [siteId, result] as const;
|
|
325
|
+
});
|
|
326
|
+
|
|
327
|
+
const settledResults = await Promise.allSettled(promises);
|
|
328
|
+
|
|
329
|
+
settledResults.forEach((result, index) => {
|
|
330
|
+
const siteId = Array.from(eventsBySite.keys())[index];
|
|
331
|
+
if (result.status === 'fulfilled') {
|
|
332
|
+
results.set(result.value[0], result.value[1]);
|
|
333
|
+
} else {
|
|
334
|
+
results.set(siteId, {
|
|
335
|
+
success: false,
|
|
336
|
+
error: `Batch write failed: ${result.reason}`
|
|
337
|
+
});
|
|
338
|
+
}
|
|
339
|
+
});
|
|
340
|
+
|
|
341
|
+
return results;
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
export async function getTimeSeriesFromDurableObject(
|
|
345
|
+
options: DashboardOptions & {
|
|
346
|
+
granularity?: 'hour' | 'day' | 'week' | 'month';
|
|
347
|
+
byEvent?: boolean;
|
|
348
|
+
}
|
|
349
|
+
): Promise<{
|
|
350
|
+
data: Array<{ date: string; count: number; event?: string }>;
|
|
351
|
+
granularity: string;
|
|
352
|
+
byEvent: boolean;
|
|
353
|
+
siteId: number;
|
|
354
|
+
dateRange: { start?: string; end?: string };
|
|
355
|
+
} | null> {
|
|
356
|
+
try {
|
|
357
|
+
const stub = await getDurableDatabaseStub(options.site_uuid, options.site_id);
|
|
358
|
+
|
|
359
|
+
const result = await stub.getTimeSeries({
|
|
360
|
+
startDate: options.date?.start,
|
|
361
|
+
endDate: options.date?.end,
|
|
362
|
+
granularity: options.granularity,
|
|
363
|
+
byEvent: options.byEvent
|
|
364
|
+
});
|
|
365
|
+
|
|
366
|
+
if (result.error) {
|
|
367
|
+
console.error(`Durable object time series request failed: ${result.error}`);
|
|
368
|
+
return null;
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
return result as {
|
|
372
|
+
data: Array<{ date: string; count: number; event?: string }>;
|
|
373
|
+
granularity: string;
|
|
374
|
+
byEvent: boolean;
|
|
375
|
+
siteId: number;
|
|
376
|
+
dateRange: { start?: string; end?: string };
|
|
377
|
+
};
|
|
378
|
+
} catch (error) {
|
|
379
|
+
console.error('Error fetching time series from durable object:', error);
|
|
380
|
+
return null;
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
export async function getMetricsFromDurableObject(
|
|
385
|
+
options: DashboardOptions & {
|
|
386
|
+
metricType: 'events' | 'countries' | 'devices' | 'referers' | 'pages';
|
|
387
|
+
limit?: number;
|
|
388
|
+
}
|
|
389
|
+
): Promise<{
|
|
390
|
+
metricType: string;
|
|
391
|
+
data: Array<{ label: string; count: number }>;
|
|
392
|
+
siteId: number;
|
|
393
|
+
dateRange: { start?: string; end?: string };
|
|
394
|
+
} | null> {
|
|
395
|
+
try {
|
|
396
|
+
const stub = await getDurableDatabaseStub(options.site_uuid, options.site_id);
|
|
397
|
+
|
|
398
|
+
const result = await stub.getMetrics({
|
|
399
|
+
startDate: options.date?.start,
|
|
400
|
+
endDate: options.date?.end,
|
|
401
|
+
metricType: options.metricType,
|
|
402
|
+
limit: options.limit
|
|
403
|
+
});
|
|
404
|
+
|
|
405
|
+
if (result.error) {
|
|
406
|
+
console.error(`Durable object metrics request failed: ${result.error}`);
|
|
407
|
+
return null;
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
return {
|
|
411
|
+
...result,
|
|
412
|
+
siteId: result.siteId ?? options.site_id
|
|
413
|
+
} as { metricType: string; data: Array<{ label: string; count: number }>; siteId: number; dateRange: { start?: string; end?: string } };
|
|
414
|
+
} catch (error) {
|
|
415
|
+
console.error('Error fetching metrics from durable object:', error);
|
|
416
|
+
return null;
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
export async function getEventSummaryFromDurableObject(
|
|
421
|
+
options: DashboardOptions & {
|
|
422
|
+
limit?: number;
|
|
423
|
+
offset?: number;
|
|
424
|
+
search?: string;
|
|
425
|
+
type?: "all" | "autocapture" | "event_capture" | "page_view";
|
|
426
|
+
action?: "all" | "click" | "submit" | "change" | "rule";
|
|
427
|
+
sortBy?: "count" | "first_seen" | "last_seen";
|
|
428
|
+
sortDirection?: "asc" | "desc";
|
|
429
|
+
}
|
|
430
|
+
): Promise<{
|
|
431
|
+
summary: Array<{ event: string | null; count: number; firstSeen: string | null; lastSeen: string | null }>;
|
|
432
|
+
pagination: { offset: number; limit: number; total: number; hasMore: boolean };
|
|
433
|
+
totalEvents: number;
|
|
434
|
+
totalEventTypes: number;
|
|
435
|
+
siteId: number | null;
|
|
436
|
+
dateRange: { start?: string; end?: string };
|
|
437
|
+
} | null> {
|
|
438
|
+
try {
|
|
439
|
+
const stub = await getDurableDatabaseStub(options.site_uuid, options.site_id);
|
|
440
|
+
|
|
441
|
+
const result = await stub.getEventSummary({
|
|
442
|
+
startDate: options.date?.start,
|
|
443
|
+
endDate: options.date?.end,
|
|
444
|
+
endDateIsExact: options.date?.endIsExact,
|
|
445
|
+
limit: options.limit,
|
|
446
|
+
offset: options.offset,
|
|
447
|
+
search: options.search,
|
|
448
|
+
type: options.type,
|
|
449
|
+
action: options.action,
|
|
450
|
+
sortBy: options.sortBy,
|
|
451
|
+
sortDirection: options.sortDirection,
|
|
452
|
+
});
|
|
453
|
+
|
|
454
|
+
if (result.error) {
|
|
455
|
+
console.error(`Durable object event summary request failed: ${result.error}`);
|
|
456
|
+
return null;
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
const summaryItems = (result.summary ?? []) as Array<{
|
|
460
|
+
event?: string | null;
|
|
461
|
+
count: number;
|
|
462
|
+
firstSeen?: number | string | Date | null;
|
|
463
|
+
lastSeen?: number | string | Date | null;
|
|
464
|
+
}>;
|
|
465
|
+
|
|
466
|
+
return {
|
|
467
|
+
...result,
|
|
468
|
+
summary: summaryItems.map((item) => ({
|
|
469
|
+
event: item.event ?? null,
|
|
470
|
+
count: item.count,
|
|
471
|
+
firstSeen: item.firstSeen ? new Date(item.firstSeen).toISOString() : null,
|
|
472
|
+
lastSeen: item.lastSeen ? new Date(item.lastSeen).toISOString() : null,
|
|
473
|
+
})),
|
|
474
|
+
siteId: result.siteId ?? options.site_id
|
|
475
|
+
};
|
|
476
|
+
} catch (error) {
|
|
477
|
+
console.error('Error fetching event summary from durable object:', error);
|
|
478
|
+
return null;
|
|
479
|
+
}
|
|
480
|
+
}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import type { Durable_DB, GetEventsOptions } from "@db/durable/types";
|
|
2
|
+
import { eq, and, gte, lte, desc, count } from "drizzle-orm";
|
|
3
|
+
import { siteEvents, type SiteEventInsert, type SiteEventSelect } from "@db/durable/schema";
|
|
4
|
+
|
|
5
|
+
/** Adjusts end date to include the entire day (23:59:59.999 UTC) */
|
|
6
|
+
function getEndOfDay(date: Date): Date {
|
|
7
|
+
const endOfDay = new Date(date);
|
|
8
|
+
endOfDay.setUTCHours(23, 59, 59, 999);
|
|
9
|
+
return endOfDay;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export async function getEvents(db: Durable_DB, options: GetEventsOptions = {}) {
|
|
13
|
+
const { startDate, endDate, eventType, country, deviceType, referer, limit = 100, offset = 0 } = options;
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
// // Build query conditions
|
|
17
|
+
const conditions = [];
|
|
18
|
+
|
|
19
|
+
if (startDate) {
|
|
20
|
+
conditions.push(gte(siteEvents.createdAt, startDate));
|
|
21
|
+
}
|
|
22
|
+
if (endDate) {
|
|
23
|
+
conditions.push(lte(siteEvents.createdAt, getEndOfDay(endDate)));
|
|
24
|
+
}
|
|
25
|
+
if (eventType) {
|
|
26
|
+
conditions.push(eq(siteEvents.event, eventType));
|
|
27
|
+
}
|
|
28
|
+
if (country) {
|
|
29
|
+
conditions.push(eq(siteEvents.country, country));
|
|
30
|
+
}
|
|
31
|
+
if (deviceType) {
|
|
32
|
+
conditions.push(eq(siteEvents.device_type, deviceType));
|
|
33
|
+
}
|
|
34
|
+
if (referer) {
|
|
35
|
+
conditions.push(eq(siteEvents.referer, referer));
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// Execute query
|
|
39
|
+
const whereClause = conditions.length > 0 ? and(...conditions) : undefined;
|
|
40
|
+
const events = await db
|
|
41
|
+
.select({
|
|
42
|
+
id: siteEvents.id,
|
|
43
|
+
event: siteEvents.event,
|
|
44
|
+
createdAt: siteEvents.createdAt,
|
|
45
|
+
updatedAt: siteEvents.updatedAt,
|
|
46
|
+
tag_id: siteEvents.tag_id,
|
|
47
|
+
bot_data: siteEvents.bot_data,
|
|
48
|
+
browser: siteEvents.browser,
|
|
49
|
+
city: siteEvents.city,
|
|
50
|
+
client_page_url: siteEvents.client_page_url,
|
|
51
|
+
country: siteEvents.country,
|
|
52
|
+
custom_data: siteEvents.custom_data,
|
|
53
|
+
device_type: siteEvents.device_type,
|
|
54
|
+
operating_system: siteEvents.operating_system,
|
|
55
|
+
page_url: siteEvents.page_url,
|
|
56
|
+
postal: siteEvents.postal,
|
|
57
|
+
query_params: siteEvents.query_params,
|
|
58
|
+
referer: siteEvents.referer,
|
|
59
|
+
region: siteEvents.region,
|
|
60
|
+
rid: siteEvents.rid,
|
|
61
|
+
site_id: siteEvents.site_id,
|
|
62
|
+
screen_height: siteEvents.screen_height,
|
|
63
|
+
screen_width: siteEvents.screen_width,
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
})
|
|
67
|
+
.from(siteEvents)
|
|
68
|
+
.where(whereClause)
|
|
69
|
+
.orderBy(desc(siteEvents.createdAt))
|
|
70
|
+
.limit(limit)
|
|
71
|
+
.offset(offset);
|
|
72
|
+
|
|
73
|
+
const countResult = await db
|
|
74
|
+
.select({ count: count() })
|
|
75
|
+
.from(siteEvents)
|
|
76
|
+
.where(whereClause)
|
|
77
|
+
// return { error: false, events: events }
|
|
78
|
+
|
|
79
|
+
const totalCount = countResult[0]?.count || 0;
|
|
80
|
+
const totalAllTime = conditions.length > 0
|
|
81
|
+
? (await db.select({ count: count() }).from(siteEvents))[0]?.count || 0
|
|
82
|
+
: totalCount;
|
|
83
|
+
const pagination = {
|
|
84
|
+
offset,
|
|
85
|
+
total: totalCount,
|
|
86
|
+
hasMore: offset + limit < totalCount,
|
|
87
|
+
limit,
|
|
88
|
+
}
|
|
89
|
+
if (!events) return { error: true, events: null, pagination }
|
|
90
|
+
// Get total count for pagination
|
|
91
|
+
return {
|
|
92
|
+
error: false,
|
|
93
|
+
events,
|
|
94
|
+
pagination,
|
|
95
|
+
totalAllTime,
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export type GetEventResult = Awaited<ReturnType<typeof getEvents>>;
|
|
100
|
+
export type events_t = GetEventResult["events"];
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
CREATE TABLE `site_events` (
|
|
2
|
+
`id` integer PRIMARY KEY AUTOINCREMENT NOT NULL,
|
|
3
|
+
`team_id` integer,
|
|
4
|
+
`bot_data` text,
|
|
5
|
+
`browser` text,
|
|
6
|
+
`city` text,
|
|
7
|
+
`client_page_url` text,
|
|
8
|
+
`country` text,
|
|
9
|
+
`created_at` integer NOT NULL,
|
|
10
|
+
`updated_at` integer NOT NULL,
|
|
11
|
+
`custom_data` text,
|
|
12
|
+
`device_type` text,
|
|
13
|
+
`event` text NOT NULL,
|
|
14
|
+
`operating_system` text,
|
|
15
|
+
`page_url` text,
|
|
16
|
+
`postal` text,
|
|
17
|
+
`query_params` text,
|
|
18
|
+
`referer` text,
|
|
19
|
+
`region` text,
|
|
20
|
+
`rid` text,
|
|
21
|
+
`screen_height` integer,
|
|
22
|
+
`screen_width` integer,
|
|
23
|
+
`site_id` integer NOT NULL,
|
|
24
|
+
`tag_id` text NOT NULL
|
|
25
|
+
);
|
|
26
|
+
--> statement-breakpoint
|
|
27
|
+
CREATE INDEX `site_events_team_id_idx` ON `site_events` (`team_id`);--> statement-breakpoint
|
|
28
|
+
CREATE INDEX `site_events_site_id_idx` ON `site_events` (`site_id`);--> statement-breakpoint
|
|
29
|
+
CREATE INDEX `site_events_tag_id_idx` ON `site_events` (`tag_id`);--> statement-breakpoint
|
|
30
|
+
CREATE INDEX `site_events_created_at_idx` ON `site_events` (`created_at`);--> statement-breakpoint
|
|
31
|
+
CREATE INDEX `site_events_team_site_idx` ON `site_events` (`team_id`,`site_id`);--> statement-breakpoint
|
|
32
|
+
CREATE INDEX `site_events_team_tag_idx` ON `site_events` (`team_id`,`tag_id`);--> statement-breakpoint
|
|
33
|
+
CREATE INDEX `site_events_site_created_idx` ON `site_events` (`site_id`,`created_at`);--> statement-breakpoint
|
|
34
|
+
CREATE INDEX `site_events_team_created_idx` ON `site_events` (`team_id`,`created_at`);--> statement-breakpoint
|
|
35
|
+
CREATE INDEX `site_events_country_idx` ON `site_events` (`country`);--> statement-breakpoint
|
|
36
|
+
CREATE INDEX `site_events_device_type_idx` ON `site_events` (`device_type`);--> statement-breakpoint
|
|
37
|
+
CREATE INDEX `site_events_event_idx` ON `site_events` (`event`);--> statement-breakpoint
|
|
38
|
+
CREATE INDEX `site_events_referer_idx` ON `site_events` (`referer`);
|