/** * Opt-in analytics injection for SSR streams. * Supported environment variables: * - `GA_MEASUREMENT_ID` — Google Analytics 4 measurement ID * - `GTM_CONTAINER_ID` — Google Tag Manager web container ID * * Netlify configuration-file env vars are build-time only for serverless * functions, so the Vite/Nitro build paths also bake this public value into * SSR bundles. * * Amplitude and Sentry are initialized client-side via their npm packages * (see `packages/core/src/client/analytics.ts`). GTM and GA require script * tag injection because their loaders must be `` + `` ); } function getGtmHeadScript(containerId: string): string { const jsId = JSON.stringify(containerId); return ``; } function getGtmBodyFallback(containerId: string): string { const src = encodeURIComponent(containerId); return ``; } type AnalyticsInjection = { head: string; body: string; }; function getAnalyticsInjection(): AnalyticsInjection | null { const containerId = getGtmContainerId(); if (containerId) { return { head: getGtmHeadScript(containerId), body: getGtmBodyFallback(containerId), }; } const gaScript = getGaScript(); return gaScript ? { head: gaScript, body: "" } : null; } const HEAD_CLOSE_PATTERN = /<\/head>/i; const BODY_OPEN_PATTERN = /
]*>/i; /** * Add the configured analytics scripts to a complete HTML document. * * The normal app document is streamed through `wrapWithAnalytics`, but * framework-owned documents such as `/signup` are returned as strings by the * auth guard and need the same injection path. */ export function injectAnalyticsIntoHtml(html: string): string { const injection = getAnalyticsInjection(); if (!injection) return html; const headCloseMatch = HEAD_CLOSE_PATTERN.exec(html); if (!headCloseMatch || headCloseMatch.index === undefined) return html; let output = html.slice(0, headCloseMatch.index) + injection.head + html.slice(headCloseMatch.index); if (injection.body) { const bodyOpenMatch = BODY_OPEN_PATTERN.exec(output); if (bodyOpenMatch && bodyOpenMatch.index !== undefined) { const bodyEnd = bodyOpenMatch.index + bodyOpenMatch[0].length; output = output.slice(0, bodyEnd) + injection.body + output.slice(bodyEnd); } } return output; } export function wrapWithAnalytics( body: ReadableStream