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,595 @@
1
+ import { DasboardDataResult, Pagination } from "@db/types";
2
+
3
+ export interface NivoBarChartData {
4
+ data: Record<string, any>[];
5
+ keys: string[];
6
+ indexBy: string;
7
+ axisBottom?: any;
8
+ axisLeft?: any;
9
+ legends?: any[];
10
+ options: { chart: { type: "bar" } }; // Mandatory type
11
+ }
12
+
13
+ export interface NivoPieChartData {
14
+ data: { id: string | number; value: number }[];
15
+ legends?: any[];
16
+ options: { chart: { type: "pie" } }; // Mandatory type
17
+ }
18
+
19
+ export interface NivoLineChartData {
20
+ data: {
21
+ id: string | number;
22
+ data: { x: string | number; y: string | number }[];
23
+ }[];
24
+ legends?: any[]; // Optional
25
+ options: { chart: { type: "line" } }; // Mandatory type
26
+ axisBottom?: any; // Optional, can be customized per chart
27
+ axisLeft?: any; // Optional
28
+ // Add other line-specific Nivo props if they need to be dynamic from data source
29
+ }
30
+
31
+ export type NivoChartData =
32
+ | NivoBarChartData
33
+ | NivoPieChartData
34
+ | NivoLineChartData;
35
+
36
+ export type ScoreCardLabels =
37
+ | "Uniques"
38
+ | "Total Page Views"
39
+ | "Bounce Rate"
40
+ | "Conversion Rate"
41
+ | "Revenue"
42
+ | "Avg Session Duration"
43
+ | "avg_time_on_page"
44
+ | "pages_per_session"
45
+ | "new_users";
46
+ export interface ScorecardProps {
47
+ title: ScoreCardLabels;
48
+ value: string;
49
+ change: string;
50
+ changeType: "positive" | "negative" | "neutral";
51
+ changeLabel: string;
52
+ }
53
+
54
+ export interface ChartComponentProps {
55
+ chartId: string;
56
+ chartData: NivoChartData | null | undefined;
57
+ title: string;
58
+ isLoading: boolean;
59
+ type: "bar" | "pie" | "line";
60
+ height?: string | number;
61
+ onItemClick?: (id: string) => void;
62
+ }
63
+
64
+ // Props for TableComponent
65
+ export interface TableComponentProps {
66
+ tableId: string;
67
+ tableData:
68
+ | {
69
+ headers: string[];
70
+ rows: (string | number)[][];
71
+ title?: string;
72
+ }
73
+ | null
74
+ | undefined;
75
+ title?: string;
76
+ }
77
+
78
+ // Helper function to serialize data for client components
79
+ function serializeForClient<T>(obj: T): T {
80
+ return JSON.parse(
81
+ JSON.stringify(obj, (_key, value) => {
82
+ if (value instanceof Date) {
83
+ return value.toISOString();
84
+ }
85
+ if (value === undefined) {
86
+ return null;
87
+ }
88
+ return value;
89
+ }),
90
+ );
91
+ }
92
+ function scoreCardCounts(data: DasboardDataResult): Array<ScorecardProps> {
93
+ // Scorecards are derived from raw event rows for a site/date range.
94
+ // `rid` is treated as a "session id"; we use distinct `rid` values as "unique visitors".
95
+ let uniqueCount = 0;
96
+
97
+ // Total page views = number of `page_view` events.
98
+ const totalPageViewsCount = data.filter((d) => d.event == "page_view").length;
99
+
100
+ // Unique visitors = count of distinct `rid` values.
101
+ if (data.length > 0) {
102
+ const uniqueMap = new Map<string, number>();
103
+ for (const item of data) {
104
+ if (!item.rid) continue;
105
+ const check = uniqueMap.get(item.rid);
106
+ uniqueMap.set(item.rid, check ? check + 1 : 1);
107
+ }
108
+ const ridsArray = Array.from(uniqueMap);
109
+ uniqueCount = ridsArray.length;
110
+ }
111
+
112
+ const uniques: ScorecardProps = {
113
+ title: "Uniques",
114
+ value: `${uniqueCount.toLocaleString()}`,
115
+ change: "",
116
+ changeType: "neutral",
117
+ changeLabel: "",
118
+ };
119
+ const totalPageViews: ScorecardProps = {
120
+ title: "Total Page Views",
121
+ value: `${totalPageViewsCount.toLocaleString()}`,
122
+ change: "",
123
+ changeType: "neutral",
124
+ changeLabel: "",
125
+ };
126
+ // Bounce rate = % of sessions (`rid`) with exactly one page view.
127
+ const sessionPageViews = new Map<string, number>();
128
+ data
129
+ .filter((d) => d.event === "page_view" && d.rid)
130
+ .forEach((d) => {
131
+ const current = sessionPageViews.get(d.rid!) || 0;
132
+ sessionPageViews.set(d.rid!, current + 1);
133
+ });
134
+ const singlePageSessions = Array.from(sessionPageViews.values()).filter(
135
+ (count) => count === 1,
136
+ ).length;
137
+ const bounceRatePercent =
138
+ uniqueCount > 0
139
+ ? ((singlePageSessions / uniqueCount) * 100).toFixed(1)
140
+ : "0";
141
+
142
+ // Conversion rate = % of sessions (`rid`) that include a conversion-like event.
143
+ const conversionEvents = data.filter(
144
+ (d) => d.event === "conversion" || d.event === "purchase",
145
+ ).length;
146
+ const conversionRatePercent =
147
+ uniqueCount > 0
148
+ ? ((conversionEvents / uniqueCount) * 100).toFixed(2)
149
+ : "0.00";
150
+
151
+ // Calculate total revenue - placeholder since no value field exists
152
+ const totalRevenue = 0; // Would need a value field in the data structure
153
+
154
+ // Average session duration (simplified): per `rid`, take (max(createdAt)-min(createdAt)).
155
+ const sessionTimes = new Map<string, { first: Date; last: Date }>();
156
+ data
157
+ .filter((d) => d.rid && d.createdAt)
158
+ .forEach((d) => {
159
+ const timestamp = d.createdAt!;
160
+ const existing = sessionTimes.get(d.rid!);
161
+ if (!existing) {
162
+ sessionTimes.set(d.rid!, { first: timestamp, last: timestamp });
163
+ } else {
164
+ if (timestamp < existing.first) existing.first = timestamp;
165
+ if (timestamp > existing.last) existing.last = timestamp;
166
+ }
167
+ });
168
+
169
+ const sessionDurations = Array.from(sessionTimes.values()).map(
170
+ ({ first, last }) => (last.getTime() - first.getTime()) / 1000,
171
+ ); // in seconds
172
+ const avgDuration =
173
+ sessionDurations.length > 0
174
+ ? sessionDurations.reduce((sum, duration) => sum + duration, 0) /
175
+ sessionDurations.length
176
+ : 0;
177
+ const avgDurationFormatted =
178
+ avgDuration > 60
179
+ ? `${Math.floor(avgDuration / 60)}m ${Math.floor(avgDuration % 60)}s`
180
+ : `${Math.floor(avgDuration)}s`;
181
+
182
+ const bounceRate: ScorecardProps = {
183
+ title: "Bounce Rate",
184
+ value: `${bounceRatePercent}%`,
185
+ change: "",
186
+ changeType: "neutral",
187
+ changeLabel: "",
188
+ };
189
+ const conversionRate: ScorecardProps = {
190
+ title: "Conversion Rate",
191
+ value: `${conversionRatePercent}%`,
192
+ change: "",
193
+ changeType: "neutral",
194
+ changeLabel: "",
195
+ };
196
+ const revenue: ScorecardProps = {
197
+ title: "Revenue",
198
+ value: `$${totalRevenue.toLocaleString()}`,
199
+ change: "",
200
+ changeType: "neutral",
201
+ changeLabel: "",
202
+ };
203
+ const avgSessionDuration: ScorecardProps = {
204
+ title: "Avg Session Duration",
205
+ value: avgDurationFormatted,
206
+ change: "",
207
+ changeType: "neutral",
208
+ changeLabel: "",
209
+ };
210
+ // const pagesPerSession: ScorecardProps = {
211
+ // title: "Pages per Session",
212
+ // value: "12,345",
213
+ // change: "",
214
+ // changeType: "neutral",
215
+ // changeLabel: "",
216
+ // };
217
+ // const newUsers: ScorecardProps = {
218
+ // title: "New Users",
219
+ // value: "12,345",
220
+ // change: "",
221
+ // changeType: "neutral",
222
+ // changeLabel: "",
223
+ // };
224
+
225
+ return [
226
+ uniques,
227
+ totalPageViews,
228
+ bounceRate,
229
+ conversionRate,
230
+ revenue,
231
+ avgSessionDuration,
232
+ // avgTimeOnPage,
233
+ // pagesPerSession,
234
+ // newUsers,
235
+ ];
236
+ }
237
+
238
+ export function transformToChartData(data: DasboardDataResult) {
239
+ // Group the data by date
240
+ const dateCountMap = new Map<string, number>();
241
+ const eventCountMap = new Map<string, number>();
242
+ const refererCountMap = new Map<string, number>();
243
+ const debvCountMap = new Map<string, number>();
244
+ const topPagesMap = new Map<string, number>();
245
+ const browserMap = new Map<string, number>();
246
+ const osMap = new Map<string, number>();
247
+
248
+ const cityMap = new Map<string, { count: number; country: string }>();
249
+
250
+ // Array<{ id: string, value: number }
251
+
252
+ data.forEach((item) => {
253
+ const currentCount = eventCountMap.get(item.event!) || 0;
254
+ eventCountMap.set(item.event!, currentCount + 1);
255
+ const currentCity = cityMap.get(item.city!);
256
+ if (currentCity) {
257
+ currentCity.count++;
258
+ } else {
259
+ cityMap.set(item.city!, { count: 1, country: item.country! });
260
+ }
261
+
262
+ const currentReferer =
263
+ refererCountMap.get(cleanReferer(item.referer!)) || 0;
264
+ refererCountMap.set(cleanReferer(item.referer!), currentReferer + 1);
265
+
266
+ const deviceType = item.device_type || "Unknown";
267
+ const currentDebv = debvCountMap.get(deviceType) || 0;
268
+ debvCountMap.set(deviceType, currentDebv + 1);
269
+
270
+ const currentBrowser = browserMap.get(item.browser!) || 0;
271
+ browserMap.set(item.browser!, currentBrowser + 1);
272
+
273
+ // Aggregate operating system data
274
+ if (item.operating_system) {
275
+ const currentOs = osMap.get(item.operating_system) || 0;
276
+ osMap.set(item.operating_system, currentOs + 1);
277
+ }
278
+
279
+ const currentTopPage = topPagesMap.get(item.client_page_url!) || 0;
280
+ topPagesMap.set(item.client_page_url!, currentTopPage + 1);
281
+
282
+ if (item.event === "page_view") {
283
+ // Format the date (extract just the YYYY-MM-DD part)
284
+ const dateStr = item.createdAt!.toISOString().split("T")[0];
285
+
286
+ // Increment the count for this date
287
+ const currentCount = dateCountMap.get(dateStr) || 0;
288
+ dateCountMap.set(dateStr, currentCount + 1);
289
+ }
290
+ });
291
+
292
+ // Convert the map to the required output format
293
+ const result = Array.from(dateCountMap.entries()).map(([date, count]) => ({
294
+ x: date,
295
+ y: count,
296
+ }));
297
+
298
+ // Sort by date
299
+ result.sort((a, b) => a.x.localeCompare(b.x));
300
+
301
+ const transformedData = {
302
+ pageViews: result,
303
+ scoreCards: scoreCardCounts(data),
304
+ events: Array.from(eventCountMap.entries()),
305
+ devices: Array.from(debvCountMap.entries()),
306
+ browsers: Array.from(browserMap.entries())
307
+ .toSorted((a, b) => b[1] - a[1])
308
+ .map((a) => {
309
+ return { id: a[0], value: a[1] };
310
+ }),
311
+ operatingSystems: Array.from(osMap.entries())
312
+ .toSorted((a, b) => b[1] - a[1])
313
+ .map((a) => {
314
+ return { id: a[0], value: a[1] };
315
+ }),
316
+ cities: Array.from(cityMap.entries()).toSorted(
317
+ (a, b) => b[1].count - a[1].count,
318
+ ),
319
+ topPages: Array.from(topPagesMap.entries())
320
+ .toSorted((a, b) => a[1] - b[1])
321
+ .map((a) => {
322
+ return { id: a[0], value: a[1] };
323
+ }),
324
+ referers: Array.from(refererCountMap.entries())
325
+ .toSorted((a, b) => b[1] - a[1])
326
+ .map((a) => {
327
+ return { id: a[0], value: a[1] };
328
+ }),
329
+ };
330
+
331
+ // Ensure all data is serializable for client components
332
+ return serializeForClient(transformedData);
333
+ }
334
+
335
+ export function getPageViewsData(
336
+ data?: Array<{ x: string; y: number }>,
337
+ ): NivoLineChartData {
338
+ const points = (data || []).map((item) => ({
339
+ x: item.x,
340
+ y: item.y,
341
+ }));
342
+
343
+ return {
344
+ options: { chart: { type: "line" as const } },
345
+ data: [
346
+ {
347
+ id: "Page Views",
348
+ data: points,
349
+ },
350
+ ],
351
+ };
352
+ }
353
+
354
+ // function getDateTickValues(values: string[]): string[] | undefined {
355
+ // const total = values.length;
356
+ // if (total <= 10) return values;
357
+ //
358
+ // const targetTicks = total <= 30 ? 8 : total <= 90 ? 10 : 12;
359
+ // const step = Math.max(1, Math.ceil(total / targetTicks));
360
+ // const ticks = values.filter((_, index) => index % step === 0);
361
+ // const last = values[values.length - 1];
362
+ //
363
+ // if (ticks[ticks.length - 1] !== last) {
364
+ // ticks.push(last);
365
+ // }
366
+ //
367
+ // return ticks;
368
+ // }
369
+
370
+ // function formatDateTick(value: string | number): string {
371
+ // const raw = String(value);
372
+ //
373
+ // if (/^\d{4}-\d{2}-\d{2}$/.test(raw)) {
374
+ // const parts = raw.split("-");
375
+ // return `${parts[1]}/${parts[2]}`;
376
+ // }
377
+ //
378
+ // if (/^\d{4}-\d{2}$/.test(raw)) {
379
+ // const parts = raw.split("-");
380
+ // return `${parts[1]}/${parts[0]}`;
381
+ // }
382
+ //
383
+ // return raw;
384
+ // }
385
+
386
+ export function getEventTypesData(
387
+ data?: Array<[string, number]>,
388
+ ): TableComponentProps["tableData"] {
389
+ const defaultData = [
390
+ ["page_view", 2500],
391
+ ["add_to_cart", 300],
392
+ ["checkout_start", 150],
393
+ ["video_play", 500],
394
+ ["download_pdf", 120],
395
+ ];
396
+ return {
397
+ headers: ["Event Name", "Count"],
398
+ rows: data || defaultData,
399
+ title: "Event Types",
400
+ };
401
+ }
402
+
403
+ export function getDeviceGeoData(data?: {
404
+ deviceData?: Array<[string, number]>;
405
+ geoData?: Array<[string, string, number]>;
406
+ }) {
407
+ let defaultDeviceData = [
408
+ { id: "Desktop", value: 65 },
409
+ { id: "Mobile", value: 25 },
410
+ { id: "Tablet", value: 10 },
411
+ ];
412
+ if (data) {
413
+ if (data.deviceData) {
414
+ defaultDeviceData = data.deviceData.map(([id, value]) => ({ id, value }));
415
+ }
416
+ }
417
+
418
+ const defaultGeoData = [["Canada", "Toronto", 400]];
419
+
420
+ return {
421
+ deviceTypes: {
422
+ options: { chart: { type: "pie" as const } },
423
+ data: defaultDeviceData,
424
+ },
425
+ geoData: {
426
+ headers: ["Country", "City", "Views"],
427
+ rows: data?.geoData || defaultGeoData,
428
+ title: "Top Geo Locations",
429
+ },
430
+ };
431
+ }
432
+
433
+ export function getTopSourcesData(
434
+ data?: Array<{ name: string; visitors: number }>,
435
+ ) {
436
+ const defaultData = [
437
+ { name: "Google", visitors: 5200, icon: "google.svg" },
438
+ { name: "Direct", visitors: 2100, icon: "direct.svg" },
439
+ { name: "Facebook", visitors: 1500, icon: "facebook.svg" },
440
+ { name: "Twitter", visitors: 900, icon: "twitter.svg" },
441
+ ];
442
+ return data || defaultData;
443
+ }
444
+
445
+ export function getTopPagesData(
446
+ data?: Array<{ id: string; value: number }>,
447
+ ): NivoBarChartData {
448
+ const defaultData = [
449
+ { id: "/home", value: 3050 },
450
+ { id: "/products", value: 2200 },
451
+ { id: "/about-us", value: 1800 },
452
+ { id: "/blog/article-1", value: 1200 },
453
+ { id: "/contact", value: 950 },
454
+ ];
455
+ return {
456
+ options: { chart: { type: "bar" as const } },
457
+ data: data || defaultData,
458
+ keys: ["value"],
459
+ indexBy: "id",
460
+ };
461
+ }
462
+
463
+ export function getDeviceData(
464
+ data?: Array<{ name: string; visitors: number; percentage: string }>,
465
+ ) {
466
+ const defaultData = [
467
+ { name: "Chrome", visitors: 4500, percentage: "60%", icon: "chrome.svg" },
468
+ { name: "Safari", visitors: 1500, percentage: "20%", icon: "safari.svg" },
469
+ { name: "Firefox", visitors: 900, percentage: "12%", icon: "firefox.svg" },
470
+ { name: "Edge", visitors: 600, percentage: "8%", icon: "edge.svg" },
471
+ ];
472
+ return data || defaultData;
473
+ }
474
+
475
+ export function getGoalConversionData(
476
+ data?: (string | number)[][],
477
+ ): TableComponentProps["tableData"] {
478
+ const defaultData = [
479
+ ["Account Signup", 1500, 1800, "83.33"],
480
+ ["Newsletter Subscription", 800, 950, "84.21"],
481
+ ["Demo Request", 300, 320, "93.75"],
482
+ ["Contact Form Submission", 450, 470, "95.74"],
483
+ ["Software Download", 600, 680, "88.24"],
484
+ ];
485
+ return {
486
+ title: "Goal Conversions",
487
+ headers: ["Goal", "Uniques", "Total", "CR (%)"],
488
+ rows: data || defaultData,
489
+ };
490
+ }
491
+
492
+ function cleanReferer(referer: string) {
493
+ if (!referer) return "Direct";
494
+ if (referer === "") return "Direct";
495
+ return referer.replace(/https?:\/\//, "").replace(/\/.*/, "");
496
+ }
497
+
498
+ export function getLocationsMapData(): Array<{ id: string; value: number }> {
499
+ return [{ id: "CAN", value: 300 }];
500
+ }
501
+
502
+ export function getReferrersData(
503
+ data?: Array<{ id: string; value: number }>,
504
+ ): NivoPieChartData {
505
+ const defaultData = [
506
+ { id: "Google", value: 44 },
507
+ { id: "Direct", value: 55 },
508
+ { id: "Facebook", value: 13 },
509
+ { id: "Twitter", value: 43 },
510
+ { id: "LinkedIn", value: 22 },
511
+ ];
512
+ return {
513
+ options: { chart: { type: "pie" as const } },
514
+ data: data || defaultData,
515
+ };
516
+ }
517
+ export type DashboardResponseData = {
518
+ noSiteRecordsExist: boolean;
519
+ ScoreCards: Array<ScorecardProps>;
520
+ PageViewsData: NivoLineChartData;
521
+ EventTypesData: ReturnType<typeof getEventTypesData>;
522
+ DeviceGeoData: ReturnType<typeof getDeviceGeoData>;
523
+ ReferrersData: NivoPieChartData;
524
+ TopPagesData: NivoBarChartData;
525
+ TopSourcesData: ReturnType<typeof getTopSourcesData>;
526
+ BrowserData: ReturnType<typeof getDeviceData>;
527
+ OSData: ReturnType<typeof getDeviceData>;
528
+ Countries?: Array<{ id: string; value: number }>;
529
+ CountryUniques?: Array<{ id: string; value: number }>;
530
+ Pagination: Pagination;
531
+ Regions?: Array<{ id: string; value: number }>;
532
+ EventSummary?: {
533
+ summary: Array<{ event: string | null; count: number; firstSeen: string | null; lastSeen: string | null }>;
534
+ pagination: { offset: number; limit: number; total: number; hasMore: boolean };
535
+ totalEvents: number;
536
+ totalEventTypes: number;
537
+ siteId: number | null;
538
+ dateRange: { start?: string; end?: string };
539
+ } | null;
540
+ };
541
+
542
+ export type EventTypeDistributionItem = {
543
+ id: string;
544
+ label: string;
545
+ value: number;
546
+ };
547
+
548
+ export const prettifyEventName = (
549
+ name: string,
550
+ labelsMap?: Map<string, string>,
551
+ ): string => {
552
+ const custom = labelsMap?.get(name);
553
+ if (custom) return custom;
554
+
555
+ if (name.startsWith("$ac_")) {
556
+ const parts = name.split("_");
557
+ const text = parts[2] || "unnamed";
558
+ const id = parts[3] || null;
559
+ return id ? `${text}_${id}` : text;
560
+ }
561
+
562
+ return name
563
+ .replace(/_/g, " ")
564
+ .replace(/\b\w/g, (char) => char.toUpperCase());
565
+ };
566
+
567
+ export const getEventTypesDistribution = (
568
+ rows: NonNullable<TableComponentProps["tableData"]>["rows"] | undefined,
569
+ labelsMap?: Map<string, string>,
570
+ ): EventTypeDistributionItem[] => {
571
+ const safeRows = rows || [];
572
+
573
+ const filtered = safeRows
574
+ .filter((row) => String(row?.[0] ?? "").toLowerCase() !== "page_view")
575
+ .map((row, index) => {
576
+ const rawName = String(row?.[0] ?? `Step ${index + 1}`);
577
+ const label = prettifyEventName(rawName, labelsMap);
578
+ const value = Number(row?.[1]) || 0;
579
+ return { id: label, label, value };
580
+ })
581
+ .filter((item) => item.value > 0)
582
+ .toSorted((a, b) => b.value - a.value)
583
+ .slice(0, 5);
584
+
585
+ const total = filtered.reduce((sum, item) => sum + item.value, 0);
586
+ return filtered.map((item) => ({
587
+ ...item,
588
+ value: total > 0 ? Math.round((item.value / total) * 100) : 0,
589
+ }));
590
+ };
591
+
592
+
593
+ export type DeviceGeoData = ReturnType<typeof getDeviceGeoData>;
594
+ export type TopSourcesData = ReturnType<typeof getTopSourcesData>;
595
+ export type BrowserData = ReturnType<typeof getDeviceData>;
package/db/types.ts ADDED
@@ -0,0 +1,55 @@
1
+ import type { getDashboardData } from "@db/postgres/sites";
2
+ import type { DBAdapter, EventSelect } from "@db/d1/schema";
3
+ import type { d1_client } from "@db/d1/client";
4
+ import type { pg_client } from "@db/postgres/client";
5
+ import type { DashboardOptions } from "@db/durable/types";
6
+
7
+
8
+ export type D1Client = typeof d1_client;
9
+ export type PGClient = ReturnType<typeof pg_client>;
10
+
11
+ //TODO: move implementation over
12
+ type SingleStoreClient = any;
13
+
14
+ export type AdapterToClient = {
15
+ "sqlite": D1Client;
16
+ "postgres": PGClient;
17
+ //WARNING: This is not a real client
18
+ "singlestore": SingleStoreClient;
19
+ "analytics_engine": SingleStoreClient;
20
+ };
21
+
22
+ export type UserRole = "viewer" | "editor" | "admin";
23
+
24
+
25
+ export type Pagination = {
26
+ limit: number;
27
+ offset: number;
28
+ total: number;
29
+ hasMore: boolean;
30
+ }
31
+
32
+ /**
33
+ * Client utilities for communicating with Site Durable Objects
34
+ *
35
+ * These functions handle the communication between the main worker
36
+ * and the site-specific durable objects using RPC calls instead of fetch.
37
+ */
38
+ export interface DashboardDataResult {
39
+ //TODO: make this typed
40
+ events: Array<Partial<EventSelect>>;
41
+ error: boolean;
42
+ pagination: Pagination;
43
+ site_id: number;
44
+ site_uuid: string;
45
+ }
46
+
47
+ export interface AdapterResult<T extends DBAdapter> {
48
+ adapter: T;
49
+ client: AdapterToClient[T] | null;
50
+ noSiteRecordsExist: boolean;
51
+ query: DashboardDataResult | null;
52
+
53
+ }
54
+ export type DasboardDataResult = Awaited<ReturnType<typeof getDashboardData>["query"]>;
55
+ export type { DBAdapter, DashboardOptions };