From d56bf6ff33b3434bb9d47fdedf8d6e1fd1124be6 Mon Sep 17 00:00:00 2001 From: Melvyn <56388157+Melvynx@users.noreply.github.com> Date: Fri, 10 Jul 2026 16:29:50 +0000 Subject: [PATCH 1/2] track Umami visits when DNT is enabled --- apps/web/src/routes/__root.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/apps/web/src/routes/__root.tsx b/apps/web/src/routes/__root.tsx index 8d27e5cc..a8c31dd8 100644 --- a/apps/web/src/routes/__root.tsx +++ b/apps/web/src/routes/__root.tsx @@ -47,7 +47,6 @@ function RootComponent() { src={UMAMI_SCRIPT_URL} data-website-id={UMAMI_WEBSITE_ID} data-domains="saveit.now" - data-do-not-track="true" /> From cccb4c814ef1c55d34e43481bab559432251911a Mon Sep 17 00:00:00 2001 From: Melvyn <56388157+Melvynx@users.noreply.github.com> Date: Fri, 10 Jul 2026 16:36:03 +0000 Subject: [PATCH 2/2] proxy Umami through SaveIt --- apps/web/src/routeTree.gen.ts | 21 ++++++++ apps/web/src/routes/__root.tsx | 3 +- apps/web/src/routes/api.insights.$.ts | 72 +++++++++++++++++++++++++++ 3 files changed, 95 insertions(+), 1 deletion(-) create mode 100644 apps/web/src/routes/api.insights.$.ts diff --git a/apps/web/src/routeTree.gen.ts b/apps/web/src/routeTree.gen.ts index 7b3bbaab..da9f2068 100644 --- a/apps/web/src/routeTree.gen.ts +++ b/apps/web/src/routeTree.gen.ts @@ -68,6 +68,7 @@ import { Route as ApiWebhooksStripeRouteImport } from "./routes/api.webhooks.str import { Route as ApiV1SplatRouteImport } from "./routes/api.v1.$"; import { Route as ApiUserLimitsRouteImport } from "./routes/api.user.limits"; import { Route as ApiMobileCheckoutRouteImport } from "./routes/api.mobile.checkout"; +import { Route as ApiInsightsSplatRouteImport } from "./routes/api.insights.$"; import { Route as ApiBookmarksBookmarkIdRouteImport } from "./routes/api.bookmarks.$bookmarkId"; import { Route as ApiAuthSplatRouteImport } from "./routes/api.auth.$"; import { Route as AdminUsersUserIdRouteImport } from "./routes/admin.users.$userId"; @@ -369,6 +370,11 @@ const ApiMobileCheckoutRoute = ApiMobileCheckoutRouteImport.update({ path: "/api/mobile/checkout", getParentRoute: () => rootRouteImport, } as any); +const ApiInsightsSplatRoute = ApiInsightsSplatRouteImport.update({ + id: "/api/insights/$", + path: "/api/insights/$", + getParentRoute: () => rootRouteImport, +} as any); const ApiBookmarksBookmarkIdRoute = ApiBookmarksBookmarkIdRouteImport.update({ id: "/$bookmarkId", path: "/$bookmarkId", @@ -454,6 +460,7 @@ export interface FileRoutesByFullPath { "/admin/users/$userId": typeof AdminUsersUserIdRoute; "/api/auth/$": typeof ApiAuthSplatRoute; "/api/bookmarks/$bookmarkId": typeof ApiBookmarksBookmarkIdRouteWithChildren; + "/api/insights/$": typeof ApiInsightsSplatRoute; "/api/mobile/checkout": typeof ApiMobileCheckoutRoute; "/api/user/limits": typeof ApiUserLimitsRoute; "/api/v1/$": typeof ApiV1SplatRoute; @@ -520,6 +527,7 @@ export interface FileRoutesByTo { "/admin/users/$userId": typeof AdminUsersUserIdRoute; "/api/auth/$": typeof ApiAuthSplatRoute; "/api/bookmarks/$bookmarkId": typeof ApiBookmarksBookmarkIdRouteWithChildren; + "/api/insights/$": typeof ApiInsightsSplatRoute; "/api/mobile/checkout": typeof ApiMobileCheckoutRoute; "/api/user/limits": typeof ApiUserLimitsRoute; "/api/v1/$": typeof ApiV1SplatRoute; @@ -587,6 +595,7 @@ export interface FileRoutesById { "/admin/users/$userId": typeof AdminUsersUserIdRoute; "/api/auth/$": typeof ApiAuthSplatRoute; "/api/bookmarks/$bookmarkId": typeof ApiBookmarksBookmarkIdRouteWithChildren; + "/api/insights/$": typeof ApiInsightsSplatRoute; "/api/mobile/checkout": typeof ApiMobileCheckoutRoute; "/api/user/limits": typeof ApiUserLimitsRoute; "/api/v1/$": typeof ApiV1SplatRoute; @@ -655,6 +664,7 @@ export interface FileRouteTypes { | "/admin/users/$userId" | "/api/auth/$" | "/api/bookmarks/$bookmarkId" + | "/api/insights/$" | "/api/mobile/checkout" | "/api/user/limits" | "/api/v1/$" @@ -721,6 +731,7 @@ export interface FileRouteTypes { | "/admin/users/$userId" | "/api/auth/$" | "/api/bookmarks/$bookmarkId" + | "/api/insights/$" | "/api/mobile/checkout" | "/api/user/limits" | "/api/v1/$" @@ -787,6 +798,7 @@ export interface FileRouteTypes { | "/admin/users/$userId" | "/api/auth/$" | "/api/bookmarks/$bookmarkId" + | "/api/insights/$" | "/api/mobile/checkout" | "/api/user/limits" | "/api/v1/$" @@ -835,6 +847,7 @@ export interface RootRouteChildren { USlugRoute: typeof USlugRoute; UnsubscribeUserIdRoute: typeof UnsubscribeUserIdRoute; ApiAuthSplatRoute: typeof ApiAuthSplatRoute; + ApiInsightsSplatRoute: typeof ApiInsightsSplatRoute; ApiMobileCheckoutRoute: typeof ApiMobileCheckoutRoute; ApiUserLimitsRoute: typeof ApiUserLimitsRoute; ApiV1SplatRoute: typeof ApiV1SplatRoute; @@ -1256,6 +1269,13 @@ declare module "@tanstack/react-router" { preLoaderRoute: typeof ApiMobileCheckoutRouteImport; parentRoute: typeof rootRouteImport; }; + "/api/insights/$": { + id: "/api/insights/$"; + path: "/api/insights/$"; + fullPath: "/api/insights/$"; + preLoaderRoute: typeof ApiInsightsSplatRouteImport; + parentRoute: typeof rootRouteImport; + }; "/api/bookmarks/$bookmarkId": { id: "/api/bookmarks/$bookmarkId"; path: "/$bookmarkId"; @@ -1497,6 +1517,7 @@ const rootRouteChildren: RootRouteChildren = { USlugRoute: USlugRoute, UnsubscribeUserIdRoute: UnsubscribeUserIdRoute, ApiAuthSplatRoute: ApiAuthSplatRoute, + ApiInsightsSplatRoute: ApiInsightsSplatRoute, ApiMobileCheckoutRoute: ApiMobileCheckoutRoute, ApiUserLimitsRoute: ApiUserLimitsRoute, ApiV1SplatRoute: ApiV1SplatRoute, diff --git a/apps/web/src/routes/__root.tsx b/apps/web/src/routes/__root.tsx index a8c31dd8..4421f7c6 100644 --- a/apps/web/src/routes/__root.tsx +++ b/apps/web/src/routes/__root.tsx @@ -8,7 +8,7 @@ import { } from "@tanstack/react-router"; import { cn } from "@workspace/ui/lib/utils"; -const UMAMI_SCRIPT_URL = "https://analytics.melvynx.dev/script.js"; +const UMAMI_SCRIPT_URL = "/api/insights/script"; const UMAMI_WEBSITE_ID = "c077f623-0462-459b-a3fe-9ba279dabc82"; export const Route = createRootRoute({ @@ -47,6 +47,7 @@ function RootComponent() { src={UMAMI_SCRIPT_URL} data-website-id={UMAMI_WEBSITE_ID} data-domains="saveit.now" + data-host-url="/api/insights" /> diff --git a/apps/web/src/routes/api.insights.$.ts b/apps/web/src/routes/api.insights.$.ts new file mode 100644 index 00000000..0f6bc81c --- /dev/null +++ b/apps/web/src/routes/api.insights.$.ts @@ -0,0 +1,72 @@ +import { createFileRoute } from "@tanstack/react-router"; + +const UMAMI_ORIGIN = "https://analytics.melvynx.dev"; +const MAX_EVENT_BYTES = 64 * 1024; + +const forwardedRequestHeaders = [ + "accept-language", + "user-agent", + "x-forwarded-for", + "x-real-ip", +] as const; + +function getProxyHeaders(request: Request) { + const headers = new Headers(); + + for (const name of forwardedRequestHeaders) { + const value = request.headers.get(name); + if (value) headers.set(name, value); + } + + return headers; +} + +function proxyResponse(response: Response, cacheControl: string) { + const headers = new Headers({ "Cache-Control": cacheControl }); + const contentType = response.headers.get("content-type"); + if (contentType) headers.set("Content-Type", contentType); + + return new Response(response.body, { + status: response.status, + headers, + }); +} + +export const Route = createFileRoute("/api/insights/$")({ + server: { + handlers: { + GET: async ({ request }: { request: Request }) => { + if (new URL(request.url).pathname !== "/api/insights/script") { + return new Response("Not found", { status: 404 }); + } + + const response = await fetch(`${UMAMI_ORIGIN}/script.js`, { + headers: getProxyHeaders(request), + }); + + return proxyResponse(response, "public, max-age=3600"); + }, + POST: async ({ request }: { request: Request }) => { + if (new URL(request.url).pathname !== "/api/insights/api/send") { + return new Response("Not found", { status: 404 }); + } + + const body = await request.arrayBuffer(); + if (body.byteLength > MAX_EVENT_BYTES) { + return new Response("Payload too large", { status: 413 }); + } + + const headers = getProxyHeaders(request); + headers.set("Content-Type", "application/json"); + + const response = await fetch(`${UMAMI_ORIGIN}/api/send`, { + method: "POST", + headers, + body, + }); + + return proxyResponse(response, "no-store"); + }, + }, + }, +});