lytx 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (213) hide show
  1. package/.env.example +37 -0
  2. package/README.md +486 -0
  3. package/alchemy.run.ts +155 -0
  4. package/cli/bootstrap-admin.ts +284 -0
  5. package/cli/deploy-staging.ts +692 -0
  6. package/cli/import-events.ts +628 -0
  7. package/cli/import-sites.ts +518 -0
  8. package/cli/index.ts +609 -0
  9. package/cli/init-db.ts +269 -0
  10. package/cli/migrate-to-durable-objects.ts +564 -0
  11. package/cli/migration-worker.ts +300 -0
  12. package/cli/performance-test.ts +588 -0
  13. package/cli/pg/client.ts +4 -0
  14. package/cli/pg/new-site.ts +153 -0
  15. package/cli/rollback-durable-objects.ts +622 -0
  16. package/cli/seed-data.ts +459 -0
  17. package/cli/setup.js +18 -0
  18. package/cli/setup.ts +463 -0
  19. package/cli/validate-migration.ts +200 -0
  20. package/cli/wrangler-migration.jsonc +28 -0
  21. package/db/adapter.ts +166 -0
  22. package/db/analytics_engine/client.ts +0 -0
  23. package/db/analytics_engine/sites.ts +0 -0
  24. package/db/client.ts +16 -0
  25. package/db/d1/client.ts +8 -0
  26. package/db/d1/drizzle.config.ts +35 -0
  27. package/db/d1/migrations/0000_true_maelstrom.sql +165 -0
  28. package/db/d1/migrations/0001_wonderful_bloodaxe.sql +12 -0
  29. package/db/d1/migrations/0002_late_frightful_four.sql +1 -0
  30. package/db/d1/migrations/0003_cuddly_obadiah_stane.sql +16 -0
  31. package/db/d1/migrations/0004_mute_stardust.sql +1 -0
  32. package/db/d1/migrations/0005_awesome_silvermane.sql +3 -0
  33. package/db/d1/migrations/0006_volatile_shriek.sql +2 -0
  34. package/db/d1/migrations/0007_superb_lila_cheney.sql +1 -0
  35. package/db/d1/migrations/0008_bitter_longshot.sql +17 -0
  36. package/db/d1/migrations/0009_wonderful_madame_masque.sql +28 -0
  37. package/db/d1/migrations/meta/0000_snapshot.json +1112 -0
  38. package/db/d1/migrations/meta/0001_snapshot.json +1187 -0
  39. package/db/d1/migrations/meta/0002_snapshot.json +1194 -0
  40. package/db/d1/migrations/meta/0003_snapshot.json +1296 -0
  41. package/db/d1/migrations/meta/0004_snapshot.json +1303 -0
  42. package/db/d1/migrations/meta/0005_snapshot.json +1325 -0
  43. package/db/d1/migrations/meta/0006_snapshot.json +1339 -0
  44. package/db/d1/migrations/meta/0007_snapshot.json +1347 -0
  45. package/db/d1/migrations/meta/0008_snapshot.json +1464 -0
  46. package/db/d1/migrations/meta/0009_snapshot.json +1648 -0
  47. package/db/d1/migrations/meta/_journal.json +76 -0
  48. package/db/d1/schema.ts +407 -0
  49. package/db/d1/sites.ts +374 -0
  50. package/db/d1/teamAiUsage.ts +101 -0
  51. package/db/d1/teams.ts +127 -0
  52. package/db/durable/drizzle.config.ts +8 -0
  53. package/db/durable/durableObjectClient.ts +480 -0
  54. package/db/durable/events.ts +100 -0
  55. package/db/durable/migrations/0000_fair_bucky.sql +38 -0
  56. package/db/durable/migrations/meta/0000_snapshot.json +278 -0
  57. package/db/durable/migrations/meta/_journal.json +13 -0
  58. package/db/durable/migrations/migrations.js +10 -0
  59. package/db/durable/schema.ts +5 -0
  60. package/db/durable/siteDurableObject.ts +1352 -0
  61. package/db/durable/types.ts +53 -0
  62. package/db/postgres/client.ts +13 -0
  63. package/db/postgres/drizzle.config.ts +12 -0
  64. package/db/postgres/migrations/0000_brainy_sprite.sql +116 -0
  65. package/db/postgres/migrations/meta/0000_snapshot.json +681 -0
  66. package/db/postgres/migrations/meta/_journal.json +13 -0
  67. package/db/postgres/schema.ts +145 -0
  68. package/db/postgres/sites.ts +118 -0
  69. package/db/tranformReports.ts +595 -0
  70. package/db/types.ts +55 -0
  71. package/endpoints/api_worker.tsx +1854 -0
  72. package/endpoints/site_do_worker.ts +11 -0
  73. package/index.d.ts +63 -0
  74. package/index.ts +83 -0
  75. package/lib/auth.ts +279 -0
  76. package/lib/geojson/world_countries.json +45307 -0
  77. package/lib/random_name.ts +41 -0
  78. package/lib/sendMail.ts +252 -0
  79. package/package.json +142 -0
  80. package/public/favicon.ico +0 -0
  81. package/public/images/android-chrome-192x192.png +0 -0
  82. package/public/images/android-chrome-512x512.png +0 -0
  83. package/public/images/apple-touch-icon.png +0 -0
  84. package/public/images/favicon-16x16.png +0 -0
  85. package/public/images/favicon-32x32.png +0 -0
  86. package/public/images/lytx_dark_dashboard.png +0 -0
  87. package/public/images/lytx_light_dashboard.png +0 -0
  88. package/public/images/safari-pinned-tab.svg +4 -0
  89. package/public/logo.png +0 -0
  90. package/public/site.webmanifest +26 -0
  91. package/public/sw.js +107 -0
  92. package/src/Document.tsx +86 -0
  93. package/src/api/ai_api.ts +1156 -0
  94. package/src/api/authMiddleware.ts +45 -0
  95. package/src/api/auth_api.ts +465 -0
  96. package/src/api/event_labels_api.ts +193 -0
  97. package/src/api/events_api.ts +210 -0
  98. package/src/api/queueWorker.ts +303 -0
  99. package/src/api/reports_api.ts +278 -0
  100. package/src/api/seed_api.ts +288 -0
  101. package/src/api/sites_api.ts +904 -0
  102. package/src/api/tag_api.ts +458 -0
  103. package/src/api/tag_api_v2.ts +289 -0
  104. package/src/api/team_api.ts +456 -0
  105. package/src/app/Dashboard.tsx +1339 -0
  106. package/src/app/Events.tsx +974 -0
  107. package/src/app/Explore.tsx +312 -0
  108. package/src/app/Layout.tsx +58 -0
  109. package/src/app/Settings.tsx +1302 -0
  110. package/src/app/components/DashboardCard.tsx +118 -0
  111. package/src/app/components/EditableCell.tsx +123 -0
  112. package/src/app/components/EventForm.tsx +93 -0
  113. package/src/app/components/MarketingFooter.tsx +49 -0
  114. package/src/app/components/MarketingNav.tsx +150 -0
  115. package/src/app/components/Nav.tsx +755 -0
  116. package/src/app/components/NewSiteSetup.tsx +298 -0
  117. package/src/app/components/SQLEditor.tsx +740 -0
  118. package/src/app/components/SiteSelector.tsx +126 -0
  119. package/src/app/components/SiteTag.tsx +42 -0
  120. package/src/app/components/SiteTagInstallCard.tsx +241 -0
  121. package/src/app/components/WorldMapCard.tsx +337 -0
  122. package/src/app/components/charts/ChartComponents.tsx +1481 -0
  123. package/src/app/components/charts/EventFunnel.tsx +45 -0
  124. package/src/app/components/charts/EventSummary.tsx +194 -0
  125. package/src/app/components/charts/SankeyFlows.tsx +72 -0
  126. package/src/app/components/marketing/CheckIcon.tsx +16 -0
  127. package/src/app/components/marketing/MarketingLayout.tsx +23 -0
  128. package/src/app/components/marketing/SectionHeading.tsx +35 -0
  129. package/src/app/components/reports/AskAiWorkspace.tsx +371 -0
  130. package/src/app/components/reports/CreateReportStarter.tsx +74 -0
  131. package/src/app/components/reports/DashboardRouteFiltersContext.tsx +14 -0
  132. package/src/app/components/reports/DashboardToolbar.tsx +154 -0
  133. package/src/app/components/reports/DashboardWorkspaceLayout.tsx +63 -0
  134. package/src/app/components/reports/DashboardWorkspaceShell.tsx +118 -0
  135. package/src/app/components/reports/ReportBuilderWorkspace.tsx +76 -0
  136. package/src/app/components/reports/custom/CustomReportBuilderPage.tsx +1667 -0
  137. package/src/app/components/reports/custom/ReportWidgetChart.tsx +297 -0
  138. package/src/app/components/reports/custom/buildWidgetSql.ts +151 -0
  139. package/src/app/components/reports/custom/chartPalettes.ts +18 -0
  140. package/src/app/components/reports/custom/types.ts +50 -0
  141. package/src/app/components/reports/reportBuilderMenuItems.ts +17 -0
  142. package/src/app/components/reports/useDashboardToolbarControls.tsx +235 -0
  143. package/src/app/components/ui/AlertBanner.tsx +101 -0
  144. package/src/app/components/ui/Button.tsx +55 -0
  145. package/src/app/components/ui/Card.tsx +80 -0
  146. package/src/app/components/ui/Input.tsx +72 -0
  147. package/src/app/components/ui/Link.tsx +23 -0
  148. package/src/app/components/ui/ReportBuilderMenu.tsx +246 -0
  149. package/src/app/components/ui/ThemeToggle.tsx +54 -0
  150. package/src/app/constants.ts +6 -0
  151. package/src/app/headers.ts +33 -0
  152. package/src/app/providers/AuthProvider.tsx +189 -0
  153. package/src/app/providers/ClientProviders.tsx +18 -0
  154. package/src/app/providers/QueryProvider.tsx +23 -0
  155. package/src/app/providers/ThemeProvider.tsx +88 -0
  156. package/src/app/utils/chartThemes.ts +146 -0
  157. package/src/app/utils/keybinds.ts +96 -0
  158. package/src/app/utils/media.tsx +24 -0
  159. package/src/client.tsx +114 -0
  160. package/src/config/createLytxAppConfig.ts +252 -0
  161. package/src/config/resourceNames.ts +88 -0
  162. package/src/db/index.ts +67 -0
  163. package/src/index.css +285 -0
  164. package/src/lib/featureFlags.ts +69 -0
  165. package/src/pages/GetStarted.tsx +290 -0
  166. package/src/pages/Home.tsx +268 -0
  167. package/src/pages/Login.tsx +283 -0
  168. package/src/pages/PrivacyPolicy.tsx +120 -0
  169. package/src/pages/Signup.tsx +267 -0
  170. package/src/pages/TermsOfService.tsx +126 -0
  171. package/src/pages/VerifyEmail.tsx +56 -0
  172. package/src/session/durableObject.ts +7 -0
  173. package/src/session/siteSchema.ts +86 -0
  174. package/src/session/types.ts +36 -0
  175. package/src/templates/README.md +80 -0
  176. package/src/templates/cleanFunctions.js +44 -0
  177. package/src/templates/embedFunctions.js +52 -0
  178. package/src/templates/lytx-shared.ts +662 -0
  179. package/src/templates/lytxpixel-core.ts +144 -0
  180. package/src/templates/lytxpixel.ts +267 -0
  181. package/src/templates/lytxpixelBrowser.js +634 -0
  182. package/src/templates/lytxpixelBrowser.mjs +634 -0
  183. package/src/templates/parseData.js +12 -0
  184. package/src/templates/script.ts +31 -0
  185. package/src/templates/template.tsx +50 -0
  186. package/src/templates/test.js +3 -0
  187. package/src/templates/trackWebEvents.ts +177 -0
  188. package/src/templates/vendors/clickcease.ts +8 -0
  189. package/src/templates/vendors/google.ts +174 -0
  190. package/src/templates/vendors/linkedin.ts +23 -0
  191. package/src/templates/vendors/meta.ts +56 -0
  192. package/src/templates/vendors/quantcast.ts +22 -0
  193. package/src/templates/vendors/simplfi.ts +7 -0
  194. package/src/types/app-context.ts +16 -0
  195. package/src/utilities/dashboardParams.ts +188 -0
  196. package/src/utilities/dashboardQueries.ts +537 -0
  197. package/src/utilities/dashboardTransforms.ts +167 -0
  198. package/src/utilities/dataValidation.ts +414 -0
  199. package/src/utilities/detector.ts +73 -0
  200. package/src/utilities/encrypt.ts +103 -0
  201. package/src/utilities/index.ts +13 -0
  202. package/src/utilities/parser.ts +117 -0
  203. package/src/utilities/performanceMonitoring.ts +570 -0
  204. package/src/utilities/route_interuptors.ts +24 -0
  205. package/src/worker.tsx +675 -0
  206. package/tsconfig.json +78 -0
  207. package/types/env.d.ts +16 -0
  208. package/types/rw.d.ts +7 -0
  209. package/types/shims.d.ts +53 -0
  210. package/types/vite.d.ts +19 -0
  211. package/vite/vite-plugin-pixel-bundle.ts +126 -0
  212. package/vite.config.ts +53 -0
  213. package/worker-configuration.d.ts +8401 -0
@@ -0,0 +1,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`);