diff --git a/.env.example b/.env.example
index 21a74f820..15b28241f 100644
--- a/.env.example
+++ b/.env.example
@@ -235,6 +235,12 @@ VITE_WS_API_URL=
# Client-side Sentry DSN (optional). Leave empty to disable error reporting.
VITE_SENTRY_DSN=
+# Umami Analytics (privacy-friendly, only loaded in production builds)
+# VITE_UMAMI_URL: URL to your Umami script.js (e.g. https://analytics.example.com/script.js)
+# VITE_UMAMI_WEBSITE_ID: Website ID from your Umami dashboard
+VITE_UMAMI_URL=
+VITE_UMAMI_WEBSITE_ID=
+
# Map interaction mode:
# - "flat" keeps pitch/rotation disabled (2D interaction)
# - "3d" enables pitch/rotation interactions (default)
diff --git a/index.html b/index.html
index fe9e4fe71..32fe7a2af 100644
--- a/index.html
+++ b/index.html
@@ -117,9 +117,6 @@
}
-
-
-
diff --git a/src/main.ts b/src/main.ts
index 95a36376d..85c55ebed 100644
--- a/src/main.ts
+++ b/src/main.ts
@@ -8,6 +8,19 @@ import { installUtmInterceptor } from './utils/utm';
const sentryDsn = import.meta.env.VITE_SENTRY_DSN?.trim();
+// Umami Analytics — only load in production builds when env vars are configured
+if (import.meta.env.PROD) {
+ const umamiUrl = import.meta.env.VITE_UMAMI_URL?.trim();
+ const umamiWebsiteId = import.meta.env.VITE_UMAMI_WEBSITE_ID?.trim();
+ if (umamiUrl && umamiWebsiteId) {
+ const script = document.createElement('script');
+ script.defer = true;
+ script.src = umamiUrl;
+ script.dataset.websiteId = umamiWebsiteId;
+ document.head.appendChild(script);
+ }
+}
+
// Initialize Sentry error tracking (early as possible)
Sentry.init({
dsn: sentryDsn || undefined,
diff --git a/vite.config.ts b/vite.config.ts
index 3fd123028..b7e08e427 100644
--- a/vite.config.ts
+++ b/vite.config.ts
@@ -38,6 +38,18 @@ function brotliPrecompressPlugin(): Plugin {
};
}
+function umamiPlugin(umamiUrl: string, umamiWebsiteId: string): Plugin {
+ return {
+ name: 'umami-analytics',
+ apply: 'build',
+ transformIndexHtml(html) {
+ if (!umamiUrl || !umamiWebsiteId) return html;
+ const scriptTag = `\n `;
+ return html.replace('', `${scriptTag}\n `);
+ },
+ };
+}
+
function htmlVariantPlugin(activeMeta: VariantMeta, activeVariant: string, isDesktopBuild: boolean): Plugin {
return {
name: 'html-variant',
@@ -608,6 +620,8 @@ export default defineConfig(({ mode }) => {
const isDesktopBuild = process.env.VITE_DESKTOP_RUNTIME === '1';
const activeVariant = process.env.VITE_VARIANT || 'full';
const activeMeta = VARIANT_META[activeVariant] || VARIANT_META.full;
+ const umamiUrl = env.VITE_UMAMI_URL || '';
+ const umamiWebsiteId = env.VITE_UMAMI_WEBSITE_ID || '';
return {
define: {
@@ -615,6 +629,7 @@ export default defineConfig(({ mode }) => {
},
plugins: [
htmlVariantPlugin(activeMeta, activeVariant, isDesktopBuild),
+ umamiPlugin(umamiUrl, umamiWebsiteId),
polymarketPlugin(),
rssProxyPlugin(),
youtubeLivePlugin(),