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
package/src/worker.tsx ADDED
@@ -0,0 +1,675 @@
1
+ import { defineApp } from "rwsdk/worker";
2
+ import { route, render, prefix, layout } from "rwsdk/router";
3
+ import { Document } from "@/Document";
4
+ import { DashboardPage } from "@/app/Dashboard";
5
+ import { EventsPage } from "@/app/Events";
6
+ import { ExplorePage } from "@/app/Explore";
7
+ import { AppLayout } from "@/app/Layout";
8
+ import { eventsApi } from "@api/events_api";
9
+ import { seedApi } from "@api/seed_api";
10
+ import { team_dashboard_endpoints } from "@api/team_api";
11
+ import { world_countries, getCurrentVisitorsRoute, getDashboardDataCore, getDashboardDataRoute, siteEventsSqlRoute, siteEventsSchemaRoute } from "@api/sites_api";
12
+ import { aiChatRoute, aiConfigRoute, aiTagSuggestRoute, getAiConfig } from "@api/ai_api";
13
+ import { resendVerificationEmailRoute, userApiRoutes } from "@api/auth_api";
14
+ import { eventLabelsApi } from "@api/event_labels_api";
15
+ import { reportsApi } from "@api/reports_api";
16
+ import {
17
+ legacyContainerRoute,
18
+ newSiteSetup,
19
+ } from "@api/tag_api";
20
+ import { lytxTag, trackWebEvent } from "@api/tag_api_v2";
21
+ import { authMiddleware, sessionMiddleware } from "@api/authMiddleware";
22
+ import { getAuth, getAuthProviderAvailability, setAuthRuntimeConfig } from "@lib/auth";
23
+ import { Signup } from "@/pages/Signup";
24
+ import { Login } from "@/pages/Login";
25
+ import { VerifyEmail } from "@/pages/VerifyEmail";
26
+ import { SettingsPage } from "@/app/Settings";
27
+ import { NewSiteSetup } from "@/app/components/NewSiteSetup";
28
+ import { DashboardWorkspaceLayout } from "@/app/components/reports/DashboardWorkspaceLayout";
29
+ import { ReportBuilderWorkspace } from "@/app/components/reports/ReportBuilderWorkspace";
30
+ import { CustomReportBuilderPage } from "@/app/components/reports/custom/CustomReportBuilderPage";
31
+ import { checkIfTeamSetupSites, onlyAllowGetPost } from "@utilities/route_interuptors";
32
+ import type { DBAdapter } from "@db/types";
33
+ import { IS_DEV } from "rwsdk/constants";
34
+ import type { AppContext, AppRequestInfo } from "@/types/app-context";
35
+ import { handleQueueMessage } from "@/api/queueWorker";
36
+ import {
37
+ isAiFeatureEnabled,
38
+ isAskAiEnabled,
39
+ isAuthEnabled,
40
+ isDashboardEnabled,
41
+ isEventsEnabled,
42
+ isReportBuilderEnabled,
43
+ isTagScriptEnabled,
44
+ } from "@/lib/featureFlags";
45
+ import { parseCreateLytxAppConfig } from "@/config/createLytxAppConfig";
46
+ import type { CreateLytxAppConfig } from "@/config/createLytxAppConfig";
47
+ import { setEmailFromAddress } from "@lib/sendMail";
48
+ import type { DashboardResponseData } from "@db/tranformReports";
49
+ import { parseDateParam } from "@/utilities/dashboardParams";
50
+ import { getTeamSettings } from "@db/d1/teams";
51
+ import { d1_client } from "@db/d1/client";
52
+ import { user } from "@db/d1/schema";
53
+ import { eq } from "drizzle-orm";
54
+ export { SyncDurableObject } from "@/session/durableObject";
55
+ export { SiteDurableObject } from "@db/durable/siteDurableObject";
56
+
57
+ //TODO: Define things on context and create a middleware function where users can set adapters and override defaults
58
+ export type { AppContext };
59
+ export type { CreateLytxAppConfig } from "@/config/createLytxAppConfig";
60
+
61
+ const DEFAULT_TAG_DB_ADAPTER: DBAdapter = "sqlite";
62
+ const DEFAULT_TAG_SCRIPT_PATH = "/lytx.v2.js";
63
+ const DEFAULT_LEGACY_TAG_SCRIPT_PATH = "/lytx.js";
64
+ const DEFAULT_TRACK_WEB_EVENT_PATH = "/trackWebEvent.v2";
65
+ const DEFAULT_LEGACY_TRACK_WEB_EVENT_PATH = "/trackWebEvent";
66
+
67
+ const normalizeRoutePrefix = (value?: string): string => {
68
+ if (!value) return "";
69
+ const trimmed = value.trim();
70
+ if (trimmed.length === 0 || trimmed === "/") return "";
71
+ return trimmed.endsWith("/") ? trimmed.slice(0, -1) : trimmed;
72
+ };
73
+
74
+ const withRoutePrefix = (prefix: string, routePath: string): string => {
75
+ if (!prefix) return routePath;
76
+ if (routePath === prefix || routePath.startsWith(`${prefix}/`)) return routePath;
77
+ const normalizedPath = routePath.startsWith("/") ? routePath : `/${routePath}`;
78
+ return `${prefix}${normalizedPath}`;
79
+ };
80
+
81
+ type ToolbarSiteOption = {
82
+ site_id: number;
83
+ name: string;
84
+ tag_id: string;
85
+ };
86
+
87
+ const getInitialToolbarState = (ctx: AppContext) => {
88
+ const initialSites: ToolbarSiteOption[] = (ctx.sites ?? []).map((site) => ({
89
+ site_id: site.site_id,
90
+ name: site.name || `Site ${site.site_id}`,
91
+ tag_id: site.tag_id,
92
+ }));
93
+
94
+ const preferredSiteId = ctx.session?.last_site_id ?? null;
95
+ const initialSiteId = initialSites.some((site) => site.site_id === preferredSiteId)
96
+ ? preferredSiteId
97
+ : (initialSites[0]?.site_id ?? null);
98
+
99
+ return {
100
+ initialSites,
101
+ initialSiteId,
102
+ };
103
+ };
104
+
105
+ const resolvePreferredTimeZone = (value: unknown): string => {
106
+ if (typeof value !== "string" || value.trim().length === 0) return "UTC";
107
+ try {
108
+ Intl.DateTimeFormat(undefined, { timeZone: value.trim() });
109
+ return value.trim();
110
+ } catch {
111
+ return "UTC";
112
+ }
113
+ };
114
+
115
+ const resolveUserTimeZoneForServerRender = async (
116
+ ctx: AppContext,
117
+ fallbackTimeZone?: unknown,
118
+ ): Promise<string> => {
119
+ try {
120
+ const dbUser = await d1_client
121
+ .select({ timezone: user.timezone })
122
+ .from(user)
123
+ .where(eq(user.id, ctx.session.user.id))
124
+ .limit(1);
125
+
126
+ if (dbUser[0]?.timezone) {
127
+ return resolvePreferredTimeZone(dbUser[0].timezone);
128
+ }
129
+ } catch (error) {
130
+ if (IS_DEV) {
131
+ console.log("🔥🔥🔥 failed to resolve user timezone for server render", error);
132
+ }
133
+ }
134
+
135
+ const sessionTimeZone = ctx.session?.timezone;
136
+ const candidate =
137
+ typeof sessionTimeZone === "string" && sessionTimeZone.trim().length > 0
138
+ ? sessionTimeZone
139
+ : fallbackTimeZone;
140
+
141
+ return resolvePreferredTimeZone(candidate);
142
+ };
143
+
144
+ const getDateStringInTimeZone = (date: Date, timeZone: string): string => {
145
+ const formatter = new Intl.DateTimeFormat("en-CA", {
146
+ timeZone,
147
+ year: "numeric",
148
+ month: "2-digit",
149
+ day: "2-digit",
150
+ });
151
+
152
+ const parts = formatter.formatToParts(date);
153
+ const year = parts.find((part) => part.type === "year")?.value ?? "1970";
154
+ const month = parts.find((part) => part.type === "month")?.value ?? "01";
155
+ const day = parts.find((part) => part.type === "day")?.value ?? "01";
156
+ return `${year}-${month}-${day}`;
157
+ };
158
+
159
+ const appRoute = <TPath extends string>(
160
+ path: TPath,
161
+ handlers: Parameters<typeof route<TPath, AppRequestInfo>>[1],
162
+ ) => route<TPath, AppRequestInfo>(path, handlers);
163
+ export function createLytxApp(config: CreateLytxAppConfig = {}) {
164
+ const parsed_config = parseCreateLytxAppConfig(config);
165
+ setAuthRuntimeConfig(parsed_config.auth);
166
+ setEmailFromAddress(parsed_config.env?.EMAIL_FROM);
167
+ const authProviders = getAuthProviderAvailability();
168
+ const emailPasswordEnabled = parsed_config.auth?.emailPasswordEnabled ?? true;
169
+ if (!emailPasswordEnabled && !authProviders.google && !authProviders.github) {
170
+ throw new Error("Invalid auth configuration: at least one auth method must be enabled");
171
+ }
172
+ const enableRequestLogging = parsed_config.enableRequestLogging ?? IS_DEV;
173
+ const authEnabled = parsed_config.features?.auth ?? isAuthEnabled();
174
+ const dashboardEnabled = authEnabled && (parsed_config.features?.dashboard ?? isDashboardEnabled());
175
+ const eventsEnabled = parsed_config.features?.events ?? isEventsEnabled();
176
+ const aiEnabled = dashboardEnabled && (parsed_config.features?.ai ?? isAiFeatureEnabled());
177
+ const tagScriptEnabled = parsed_config.features?.tagScript ?? isTagScriptEnabled();
178
+ const tagRouteDbAdapter = parsed_config.db?.dbAdapter ?? parsed_config.dbAdapter ?? DEFAULT_TAG_DB_ADAPTER;
179
+ const tagRouteEventStore = parsed_config.db?.eventStore ?? "durable_objects";
180
+ const tagRouteQueueIngestionEnabled = parsed_config.useQueueIngestion ?? (tagRouteEventStore === "durable_objects");
181
+ const includeLegacyTagRoutes = parsed_config.includeLegacyTagRoutes ?? true;
182
+ const trackingRoutePrefix = normalizeRoutePrefix(parsed_config.trackingRoutePrefix);
183
+ const tagScriptPath = withRoutePrefix(
184
+ trackingRoutePrefix,
185
+ parsed_config.tagRoutes?.scriptPath ?? DEFAULT_TAG_SCRIPT_PATH,
186
+ );
187
+ const legacyTagScriptPath = withRoutePrefix(
188
+ trackingRoutePrefix,
189
+ parsed_config.tagRoutes?.legacyScriptPath ?? DEFAULT_LEGACY_TAG_SCRIPT_PATH,
190
+ );
191
+ const trackWebEventPath = withRoutePrefix(
192
+ trackingRoutePrefix,
193
+ parsed_config.tagRoutes?.eventPath ?? DEFAULT_TRACK_WEB_EVENT_PATH,
194
+ );
195
+ const legacyTrackWebEventPath = withRoutePrefix(
196
+ trackingRoutePrefix,
197
+ parsed_config.tagRoutes?.legacyEventPath ?? DEFAULT_LEGACY_TRACK_WEB_EVENT_PATH,
198
+ );
199
+ const reportBuilderEnabled =
200
+ dashboardEnabled && (parsed_config.features?.reportBuilderEnabled ?? isReportBuilderEnabled());
201
+ const askAiEnabled =
202
+ aiEnabled && (parsed_config.features?.askAiEnabled ?? (reportBuilderEnabled && isAskAiEnabled()));
203
+
204
+ const tagRoutes: Array<
205
+ typeof legacyContainerRoute | ReturnType<typeof lytxTag> | ReturnType<typeof trackWebEvent>
206
+ > = [];
207
+
208
+ if (includeLegacyTagRoutes && tagScriptEnabled) {
209
+ tagRoutes.push(legacyContainerRoute);
210
+ }
211
+
212
+ if (tagScriptEnabled) {
213
+ tagRoutes.push(lytxTag(tagRouteDbAdapter, tagScriptPath));
214
+ }
215
+
216
+ if (eventsEnabled) {
217
+ tagRoutes.push(trackWebEvent(tagRouteDbAdapter, trackWebEventPath, { useQueue: tagRouteQueueIngestionEnabled }));
218
+ }
219
+
220
+ if (includeLegacyTagRoutes) {
221
+ if (tagScriptEnabled && legacyTagScriptPath !== tagScriptPath) {
222
+ tagRoutes.push(lytxTag(tagRouteDbAdapter, legacyTagScriptPath));
223
+ }
224
+ if (eventsEnabled && legacyTrackWebEventPath !== trackWebEventPath) {
225
+ tagRoutes.push(
226
+ trackWebEvent(tagRouteDbAdapter, legacyTrackWebEventPath, { useQueue: tagRouteQueueIngestionEnabled }),
227
+ );
228
+ }
229
+ }
230
+
231
+ const app = defineApp<AppRequestInfo>([
232
+ ({ request }) => {
233
+ if (enableRequestLogging) console.log("🔥🔥🔥", request.method, request.url);
234
+ },
235
+ //NOTE: API ROUTES / no component or html rendering
236
+ ...tagRoutes,
237
+ ...(eventsEnabled ? [eventsApi] : []),
238
+ seedApi,
239
+ ...(authEnabled
240
+ ? [
241
+ route("/api/auth/*", (r) => authMiddleware(r)),
242
+ resendVerificationEmailRoute,
243
+ userApiRoutes,
244
+ ]
245
+ : []),
246
+ render<AppRequestInfo>(Document, [
247
+ route("/", [
248
+ onlyAllowGetPost, ({ request }) => {
249
+ return Response.redirect(new URL("/login", request.url).toString(), 308);
250
+ },
251
+ ]),
252
+ ...(authEnabled
253
+ ? [
254
+ route("/signup", [
255
+ onlyAllowGetPost, () => {
256
+ return <Signup authProviders={authProviders} emailPasswordEnabled={emailPasswordEnabled} />;
257
+ },
258
+ ]),
259
+ ]
260
+ : []),
261
+ ...(authEnabled
262
+ ? [
263
+ route("/login", [
264
+ onlyAllowGetPost,
265
+ () => {
266
+ return <Login authProviders={authProviders} emailPasswordEnabled={emailPasswordEnabled} />;
267
+ },
268
+ ]),
269
+ route("/verify-email", [
270
+ onlyAllowGetPost,
271
+ async ({ request }) => {
272
+ const requestId = crypto.randomUUID();
273
+ const url = new URL(request.url);
274
+ const token = url.searchParams.get("token") || "";
275
+
276
+ const callbackURL = url.searchParams.get("callbackURL") || undefined;
277
+ const safeCallbackURL =
278
+ callbackURL && callbackURL.startsWith("/") && !callbackURL.includes("://")
279
+ ? callbackURL
280
+ : undefined;
281
+
282
+ if (!token) {
283
+ if (IS_DEV) console.warn("Email verification failed: missing token", { requestId });
284
+ return (
285
+ <VerifyEmail
286
+ status={{
287
+ type: "error",
288
+ message:
289
+ "That verification link is missing a token. Please request a new verification email.",
290
+ callbackURL: safeCallbackURL,
291
+ }}
292
+ />
293
+ );
294
+ }
295
+
296
+ try {
297
+ const auth = getAuth();
298
+ await auth.api.verifyEmail({
299
+ query: {
300
+ token,
301
+ },
302
+ });
303
+
304
+ return (
305
+ <VerifyEmail
306
+ status={{
307
+ type: "success",
308
+ message: "Your email has been verified. You can continue.",
309
+ callbackURL: safeCallbackURL,
310
+ }}
311
+ />
312
+ );
313
+ } catch (error) {
314
+ const message = error instanceof Error ? error.message : "";
315
+ const normalized = message.toLowerCase();
316
+
317
+ const friendlyMessage =
318
+ normalized.includes("token_expired") || normalized.includes("expired")
319
+ ? "That verification link has expired. Please request a new verification email."
320
+ : normalized.includes("invalid_token") || normalized.includes("invalid")
321
+ ? "That verification link is invalid. Please request a new verification email."
322
+ : normalized.includes("user_not_found")
323
+ ? "We couldn't find an account for that link. Please request a new verification email."
324
+ : "We couldn't verify your email. Please request a new verification email.";
325
+
326
+ if (IS_DEV) console.warn("Email verification failed", {
327
+ requestId,
328
+ error: error instanceof Error ? { name: error.name, message: error.message } : error,
329
+ tokenPresent: Boolean(token),
330
+ });
331
+
332
+ return (
333
+ <VerifyEmail
334
+ status={{
335
+ type: "error",
336
+ message: friendlyMessage,
337
+ callbackURL: safeCallbackURL,
338
+ }}
339
+ />
340
+ );
341
+ }
342
+ },
343
+ ]),
344
+ ]
345
+ : []),
346
+ ...(dashboardEnabled
347
+ ? [layout(AppLayout, [
348
+ sessionMiddleware,
349
+ //PERF: This API PREFIX REQUIRES AUTHENTICATION
350
+ prefix<"/api", AppRequestInfo>("/api", [
351
+ world_countries,
352
+ getDashboardDataRoute,
353
+ getCurrentVisitorsRoute,
354
+ ...(aiEnabled ? [aiConfigRoute, aiChatRoute, aiTagSuggestRoute] : []),
355
+ siteEventsSqlRoute,
356
+ siteEventsSchemaRoute,
357
+ eventLabelsApi,
358
+ ...(reportBuilderEnabled ? [reportsApi] : []),
359
+ ///api/sites
360
+ //PERF: Add method to api prefix outside of sessionMiddleware loop
361
+ newSiteSetup(),
362
+ team_dashboard_endpoints
363
+ ]),
364
+ onlyAllowGetPost,
365
+ route("/dashboard", [
366
+ checkIfTeamSetupSites,
367
+ async ({ request, ctx }) => {
368
+ const pathname = new URL(request.url).pathname;
369
+ if (pathname === "/dashboard/") {
370
+ return Response.redirect(new URL("/dashboard", request.url).toString(), 308);
371
+ }
372
+
373
+ const toolbarState = getInitialToolbarState(ctx);
374
+ const timezone = await resolveUserTimeZoneForServerRender(
375
+ ctx,
376
+ (request as Request & { cf?: { timezone?: string } }).cf?.timezone,
377
+ );
378
+ const today = getDateStringInTimeZone(new Date(), timezone);
379
+ const todayStart = parseDateParam(today, { timeZone: timezone, boundary: "start" });
380
+ const todayEnd = parseDateParam(today, { timeZone: timezone, boundary: "end" });
381
+
382
+ let initialDashboardData: DashboardResponseData | null = null;
383
+ if (toolbarState.initialSiteId && todayStart && todayEnd) {
384
+ try {
385
+ const dashboardDataResult = await getDashboardDataCore({
386
+ ctx,
387
+ requestId: crypto.randomUUID(),
388
+ siteIdValue: toolbarState.initialSiteId,
389
+ dateStartValue: todayStart,
390
+ dateEndValue: todayEnd,
391
+ rawDateEnd: today,
392
+ normalizedTimezone: timezone,
393
+ normalizedDeviceType: null,
394
+ normalizedCountry: null,
395
+ normalizedSource: null,
396
+ normalizedPageUrl: null,
397
+ normalizedCity: null,
398
+ normalizedRegion: null,
399
+ normalizedEventName: null,
400
+ normalizedEventSummaryLimit: 50,
401
+ normalizedEventSummaryOffset: 0,
402
+ normalizedEventSummaryType: "all",
403
+ normalizedEventSummaryAction: "all",
404
+ normalizedEventSummarySortBy: "count",
405
+ normalizedEventSummarySortDirection: "desc",
406
+ eventSummarySearch: "",
407
+ });
408
+
409
+ if (dashboardDataResult.ok) {
410
+ initialDashboardData = dashboardDataResult.data;
411
+ }
412
+ } catch (error) {
413
+ if (IS_DEV) {
414
+ console.log("🔥🔥🔥 failed to prefetch today dashboard", error);
415
+ }
416
+ }
417
+ }
418
+
419
+ return (
420
+ <DashboardPage
421
+ activeReportBuilderItemId="create-report"
422
+ reportBuilderEnabled={reportBuilderEnabled}
423
+ askAiEnabled={askAiEnabled}
424
+ initialToolbarSites={toolbarState.initialSites}
425
+ initialToolbarSiteId={toolbarState.initialSiteId}
426
+ initialDashboardDateRange={{
427
+ start: today,
428
+ end: today,
429
+ preset: "Today",
430
+ }}
431
+ initialTimezone={timezone}
432
+ initialDashboardData={initialDashboardData}
433
+ />
434
+ );
435
+ },
436
+ ]),
437
+ layout<AppRequestInfo>(DashboardWorkspaceLayout, (reportBuilderEnabled
438
+ ? [
439
+ appRoute("/dashboard/reports", [
440
+ ({ request }) => {
441
+ return Response.redirect(new URL("/dashboard/reports/create-report", request.url).toString(), 308);
442
+ },
443
+ ]),
444
+ appRoute("/dashboard/reports/custom/new", [
445
+ checkIfTeamSetupSites,
446
+ ({ request }) => {
447
+ const url = new URL(request.url);
448
+ const template = url.searchParams.get("template");
449
+ return <CustomReportBuilderPage initialTemplate={template} />;
450
+ },
451
+ ]),
452
+ appRoute("/dashboard/reports/custom/*", [
453
+ checkIfTeamSetupSites,
454
+ ({ request }) => {
455
+ const pathname = new URL(request.url).pathname;
456
+ const marker = "/dashboard/reports/custom/";
457
+ const reportUuid = pathname.includes(marker)
458
+ ? decodeURIComponent(pathname.slice(pathname.indexOf(marker) + marker.length))
459
+ : "";
460
+
461
+ if (!reportUuid || reportUuid.includes("/") || reportUuid === "new") {
462
+ return Response.redirect(new URL("/dashboard/reports/create-report", request.url).toString(), 308);
463
+ }
464
+
465
+ return <CustomReportBuilderPage reportUuid={reportUuid} />;
466
+ },
467
+ ]),
468
+ appRoute("/dashboard/reports/create-report", [
469
+ checkIfTeamSetupSites,
470
+ (_info) => {
471
+ return <ReportBuilderWorkspace activeReportBuilderItemId="create-report" />;
472
+ },
473
+ ]),
474
+ appRoute("/dashboard/reports/create-reference", [
475
+ checkIfTeamSetupSites,
476
+ (_info) => {
477
+ return <ReportBuilderWorkspace activeReportBuilderItemId="create-reference" />;
478
+ },
479
+ ]),
480
+ appRoute("/dashboard/reports/ask-ai", [
481
+ checkIfTeamSetupSites,
482
+ ({ ctx, request }) => {
483
+ if (!askAiEnabled) {
484
+ return Response.redirect(new URL("/dashboard/reports/create-report", request.url).toString(), 308);
485
+ }
486
+
487
+ const aiConfig = getAiConfig(ctx.team.id);
488
+ const askAiWorkspaceProps = {
489
+ activeReportBuilderItemId: "ask-ai" as const,
490
+ initialAiConfigured: Boolean(aiConfig),
491
+ initialAiModel: aiConfig?.model ?? "",
492
+ } as const;
493
+
494
+ return <ReportBuilderWorkspace {...askAiWorkspaceProps} />;
495
+ },
496
+ ]),
497
+ appRoute("/dashboard/reports/create-dashboard", [
498
+ checkIfTeamSetupSites,
499
+ (_info) => {
500
+ return <ReportBuilderWorkspace activeReportBuilderItemId="create-dashboard" />;
501
+ },
502
+ ]),
503
+ appRoute("/dashboard/reports/create-notification", [
504
+ checkIfTeamSetupSites,
505
+ (_info) => {
506
+ return <ReportBuilderWorkspace activeReportBuilderItemId="create-notification" />;
507
+ },
508
+ ]),
509
+ ]
510
+ : [
511
+ appRoute("/dashboard/reports", [
512
+ ({ request }) => {
513
+ return Response.redirect(new URL("/dashboard", request.url).toString(), 308);
514
+ },
515
+ ]),
516
+ appRoute("/dashboard/reports/*", [
517
+ ({ request }) => {
518
+ return Response.redirect(new URL("/dashboard", request.url).toString(), 308);
519
+ },
520
+ ]),
521
+ ])),
522
+ ...(eventsEnabled
523
+ ? [
524
+ appRoute("/dashboard/events", [
525
+ checkIfTeamSetupSites,
526
+ async (_info) => {
527
+ return <EventsPage />;
528
+ },
529
+ ]),
530
+ ]
531
+ : []),
532
+ appRoute("/dashboard/new-site", [
533
+ (_info) => {
534
+ return <NewSiteSetup />;
535
+ },
536
+ ]),
537
+ appRoute("/dashboard/settings", [
538
+ async ({ ctx }) => {
539
+ const toolbarState = getInitialToolbarState(ctx);
540
+ const initialCurrentSite = toolbarState.initialSites.find(
541
+ (site) => site.site_id === toolbarState.initialSiteId,
542
+ ) ?? null;
543
+
544
+ const sessionUserSites = Array.isArray(ctx.session.userSites)
545
+ ? ctx.session.userSites
546
+ : [];
547
+
548
+ const initialUserSites = sessionUserSites.length > 0
549
+ ? sessionUserSites.map((site) => ({
550
+ site_id: site.site_id,
551
+ name: site.name ?? null,
552
+ domain: site.domain ?? null,
553
+ tag_id: site.tag_id,
554
+ createdAt: site.createdAt ?? null,
555
+ }))
556
+ : (ctx.sites ?? []).map((site) => ({
557
+ site_id: site.site_id,
558
+ name: site.name ?? null,
559
+ domain: site.domain ?? null,
560
+ tag_id: site.tag_id,
561
+ createdAt: null,
562
+ }));
563
+
564
+ const sessionTeam = ctx.session.team;
565
+
566
+ let initialTimezone: string | null =
567
+ ctx.session.timezone && typeof ctx.session.timezone === "string"
568
+ ? ctx.session.timezone
569
+ : null;
570
+
571
+ if (!initialTimezone) {
572
+ try {
573
+ const dbUser = await d1_client
574
+ .select({ timezone: user.timezone })
575
+ .from(user)
576
+ .where(eq(user.id, ctx.session.user.id))
577
+ .limit(1);
578
+ initialTimezone = dbUser[0]?.timezone ?? null;
579
+ } catch (error) {
580
+ if (IS_DEV) {
581
+ console.log("🔥🔥🔥 failed to prefetch user timezone", error);
582
+ }
583
+ }
584
+ }
585
+
586
+ const [teamSettingsResult] = await Promise.allSettled([
587
+ getTeamSettings(ctx.team.id),
588
+ ]);
589
+
590
+ const initialTeamSettings =
591
+ teamSettingsResult.status === "fulfilled" ? teamSettingsResult.value : null;
592
+ if (teamSettingsResult.status === "rejected" && IS_DEV) {
593
+ console.log("🔥🔥🔥 failed to prefetch team settings", teamSettingsResult.reason);
594
+ }
595
+
596
+ return (
597
+ <SettingsPage
598
+ initialSession={{
599
+ user: {
600
+ name: ctx.session.user?.name ?? null,
601
+ email: ctx.session.user?.email ?? null,
602
+ },
603
+ team: {
604
+ id: sessionTeam?.id ?? ctx.team.id,
605
+ name: sessionTeam?.name ?? ctx.team.name ?? null,
606
+ external_id: sessionTeam?.external_id ?? ctx.team.external_id ?? null,
607
+ },
608
+ role: ctx.user_role,
609
+ timezone: initialTimezone,
610
+ userSites: initialUserSites,
611
+ }}
612
+ initialCurrentSite={initialCurrentSite
613
+ ? {
614
+ id: initialCurrentSite.site_id,
615
+ name: initialCurrentSite.name,
616
+ tag_id: initialCurrentSite.tag_id,
617
+ }
618
+ : null}
619
+ initialSites={toolbarState.initialSites}
620
+ initialTeamSettings={initialTeamSettings}
621
+ />
622
+ );
623
+ },
624
+ ]),
625
+ appRoute("/dashboard/explore", [
626
+ checkIfTeamSetupSites,
627
+ ({ ctx }) => {
628
+ const toolbarState = getInitialToolbarState(ctx);
629
+ return (
630
+ <ExplorePage
631
+ initialSites={toolbarState.initialSites}
632
+ initialSiteId={toolbarState.initialSiteId}
633
+ />
634
+ );
635
+ },
636
+ ]),
637
+ ...(eventsEnabled
638
+ ? [
639
+ appRoute("/admin/events", [
640
+ ({ request }) => {
641
+ return Response.redirect(new URL("/dashboard/events", request.url).toString(), 308);
642
+ },
643
+ ]),
644
+ ]
645
+ : []),
646
+ appRoute("/new-site", [
647
+ ({ request }) => {
648
+ return Response.redirect(new URL("/dashboard/new-site", request.url).toString(), 308);
649
+ },
650
+ ]),
651
+ appRoute("/settings", [
652
+ ({ request }) => {
653
+ return Response.redirect(new URL("/dashboard/settings", request.url).toString(), 308);
654
+ },
655
+ ]),
656
+ appRoute("/explore", [
657
+ ({ request }) => {
658
+ return Response.redirect(new URL("/dashboard/explore", request.url).toString(), 308);
659
+ },
660
+ ]),
661
+ ])]
662
+ : []),
663
+
664
+ ]),
665
+ ]);
666
+
667
+ return {
668
+ fetch: app.fetch,
669
+ queue: handleQueueMessage,
670
+ };
671
+ }
672
+
673
+ const defaultApp = createLytxApp();
674
+
675
+ export default defaultApp;