From a5af1d035275638245086814e96decc32c46d364 Mon Sep 17 00:00:00 2001 From: jplanckeel Date: Tue, 7 Apr 2026 16:08:18 +0200 Subject: [PATCH] feat(analytics): add Microsoft Clarity integration for user analytics - Add CLARITY_PROJECT_ID environment variable support in Go server - Escape clarityProjectId to prevent XSS injection attacks - Include clarityProjectId in generated config.js output - Add Helm chart values and deployment template for Clarity configuration - Define clarityProjectId in TypeScript config interface with documentation - Initialize Microsoft Clarity script in main.tsx when project ID is configured - Support configuration via environment variables and Vite build variables --- cmd/serv.go | 8 ++++++-- helm/tracker/templates/deployment.yaml | 4 ++++ helm/tracker/values.yaml | 5 +++++ web/src/config.ts | 3 +++ web/src/main.tsx | 14 ++++++++++++++ 5 files changed, 32 insertions(+), 2 deletions(-) diff --git a/cmd/serv.go b/cmd/serv.go index c2be22a..6283c76 100644 --- a/cmd/serv.go +++ b/cmd/serv.go @@ -143,6 +143,8 @@ var serv = &cobra.Command{ homerURL := os.Getenv("HOMER_URL") + clarityProjectID := os.Getenv("CLARITY_PROJECT_ID") + // Escape values to prevent XSS injection jiraDomain = html.EscapeString(jiraDomain) jiraProjectKey = html.EscapeString(jiraProjectKey) @@ -150,6 +152,7 @@ var serv = &cobra.Command{ slackEventsChannel = html.EscapeString(slackEventsChannel) buyMeCoffeeURL = html.EscapeString(buyMeCoffeeURL) homerURL = html.EscapeString(homerURL) + clarityProjectID = html.EscapeString(clarityProjectID) w.Header().Set("Content-Type", "application/javascript") w.Header().Set("Cache-Control", "no-cache, no-store, must-revalidate") @@ -166,8 +169,9 @@ var serv = &cobra.Command{ }, demoMode: %s, buyMeCoffeeUrl: "%s", - homerUrl: "%s" -};`, jiraDomain, jiraProjectKey, slackWorkspace, slackEventsChannel, demoMode, buyMeCoffeeURL, homerURL) + homerUrl: "%s", + clarityProjectId: "%s" +};`, jiraDomain, jiraProjectKey, slackWorkspace, slackEventsChannel, demoMode, buyMeCoffeeURL, homerURL, clarityProjectID) if err != nil { slog.Error("Failed to write config.js response", "error", err) http.Error(w, "Internal server error", http.StatusInternalServerError) diff --git a/helm/tracker/templates/deployment.yaml b/helm/tracker/templates/deployment.yaml index d81729c..2133dfd 100644 --- a/helm/tracker/templates/deployment.yaml +++ b/helm/tracker/templates/deployment.yaml @@ -79,6 +79,10 @@ spec: - name: HOMER_URL value: {{ .Values.env.homer.url | quote }} {{- end }} + {{- if .Values.env.clarity.projectId }} + - name: CLARITY_PROJECT_ID + value: {{ .Values.env.clarity.projectId | quote }} + {{- end }} command: - ./tracker - serv diff --git a/helm/tracker/values.yaml b/helm/tracker/values.yaml index 2057b14..13e6f55 100644 --- a/helm/tracker/values.yaml +++ b/helm/tracker/values.yaml @@ -29,6 +29,11 @@ env: # URL of your Homer dashboard to import links from (optional) # Example: http://homer.example.com url: "" + clarity: + # Microsoft Clarity project ID for user analytics (optional) + # Leave empty to disable Clarity entirely. + # Get your project ID at https://clarity.microsoft.com + projectId: "" image: repository: bananaops/tracker diff --git a/web/src/config.ts b/web/src/config.ts index 5633865..ca974b6 100644 --- a/web/src/config.ts +++ b/web/src/config.ts @@ -35,6 +35,8 @@ export interface TrackerConfig { homerUrl?: string demoMode?: boolean buyMeCoffeeUrl?: string + /** Microsoft Clarity project ID. Leave empty to disable analytics. */ + clarityProjectId?: string } declare global { @@ -68,6 +70,7 @@ export const config: TrackerConfig & { api: { baseUrl: string } } = { }, demoMode: window.TRACKER_CONFIG?.demoMode, buyMeCoffeeUrl: window.TRACKER_CONFIG?.buyMeCoffeeUrl, + clarityProjectId: window.TRACKER_CONFIG?.clarityProjectId || (import.meta as any).env?.VITE_CLARITY_PROJECT_ID || '', api: { baseUrl: (import.meta as any).env?.VITE_API_BASE_URL || '/api/v1alpha1', diff --git a/web/src/main.tsx b/web/src/main.tsx index e9471be..cbf0b05 100644 --- a/web/src/main.tsx +++ b/web/src/main.tsx @@ -4,6 +4,20 @@ import { QueryClient, QueryClientProvider } from '@tanstack/react-query' import { ThemeProvider } from './contexts/ThemeContext' import App from './App' import './index.css' +import { config } from './config' + +// Initialize Microsoft Clarity if a project ID is configured +if (config.clarityProjectId) { + const clarityId = config.clarityProjectId + ;(function (c: any, l: Document, a: string, r: string, i: string) { + c[a] = c[a] || function (...args: unknown[]) { (c[a].q = c[a].q || []).push(args) } + const t = l.createElement(r) as HTMLScriptElement + t.async = true + t.src = 'https://www.clarity.ms/tag/' + i + const y = l.getElementsByTagName(r)[0] + y.parentNode!.insertBefore(t, y) + })(window, document, 'clarity', 'script', clarityId) +} // FontAwesome configuration import { library } from '@fortawesome/fontawesome-svg-core'