Configure Google Analytics 4 with gtag.js in a Phoenix 1.8 app — disabled in dev/test, enabled in production via environment variable.
What Gets Configured
- config.exs — GA disabled by default with
enabled: false, measurement_id: nil - runtime.exs — GA enabled in production with
GOOGLE_ANALYTICS_MEASUREMENT_IDenv var - root.html.heex — Conditional gtag.js script injection
- .env.sample —
GOOGLE_ANALYTICS_MEASUREMENT_IDentry
App Name Detection
Detect from mix.exs:
- App module prefix (e.g.,
MyApp) — the module namespace - OTP app name (e.g.,
:my_app) — used in config atoms - Web module prefix (e.g.,
MyAppWeb) — for layout references
Also detect:
- Root layout file — Look for
root.html.heexinlib/<app>_web/components/layouts/ - Existing GA config — Check all config files for existing
:google_analyticssettings - Existing gtag scripts — Check root layout for existing Google Analytics script tags
Phase 0: Discovery
CRITICAL: Audit the target app before making any changes.
Discovery Checklist
-
Root layout — Locate
root.html.heexinlib/<app>_web/components/layouts/- If missing: stop and notify user — the layout must exist before adding GA
-
Existing GA script — Search root layout for
googletagmanager.com/gtagorgtag(- If found: GA is already in the layout — note and skip layout changes
-
Existing config — Check each config file:
config/config.exs— look for:google_analyticsconfigconfig/runtime.exs— look for:google_analyticsinif config_env() == :prodblock
-
Existing .env.sample — Check for
GOOGLE_ANALYTICS_MEASUREMENT_IDentry -
CSP headers — Search for Content-Security-Policy configuration
- If CSP is configured: note that
https://www.googletagmanager.comandhttps://www.google-analytics.commust be allowed inscript-srcandconnect-src
- If CSP is configured: note that
Discovery Report
Present findings to user. If GA is already fully configured, confirm and skip. Otherwise show what will be added/changed.
Phase 1: Implementation
Execute in order, skipping anything that already exists.
Step 1: Base Config (config/config.exs)
Add if not present:
# Google Analytics Configuration
# Measurement ID is set via GOOGLE_ANALYTICS_MEASUREMENT_ID env var in runtime.exs
config :<otp_app>, :google_analytics,
enabled: false,
measurement_id: nil
Step 2: Runtime Config (config/runtime.exs)
Inside the if config_env() == :prod do block, add:
# Google Analytics Configuration for Production
config :<otp_app>, :google_analytics,
enabled: true,
measurement_id: System.get_env("GOOGLE_ANALYTICS_MEASUREMENT_ID")
Important: Place this inside the existing if config_env() == :prod do block — do not create a duplicate conditional.
Note: The measurement ID is not required — if missing, GA will be "enabled" but the script won't load because the conditional in the layout checks for a truthy measurement_id. This allows deploying without GA configured.
Step 3: Root Layout (root.html.heex)
Add the gtag.js script block just before the closing </head> tag:
<%= if Application.get_env(:<otp_app>, :google_analytics)[:enabled] do %>
<script
async
src={"https://www.googletagmanager.com/gtag/js?id=#{Application.get_env(:<otp_app>, :google_analytics)[:measurement_id]}"}
>
</script>
<script>
window.dataLayer = window.dataLayer || [];
function gtag(){dataLayer.push(arguments);}
gtag('js', new Date());
gtag('config', '<%= Application.get_env(:<otp_app>, :google_analytics)[:measurement_id] %>');
</script>
<% end %>
Placement: Insert before </head> — after meta tags, CSS, and other scripts. The async attribute ensures it doesn't block page rendering.
Step 4: Environment File (.env.sample)
Add these entries if not already present:
# Google Analytics
GOOGLE_ANALYTICS_MEASUREMENT_ID=
Phase 2: Verification
- Run
mix compile --warnings-as-errorsto verify config compiles - Check that
mix teststill passes - Verify the script does NOT appear in dev by:
- Checking that
config/config.exshasenabled: false
- Checking that
- Tell the user to:
- Set
GOOGLE_ANALYTICS_MEASUREMENT_IDin their production environment (e.g.,G-XXXXXXXXXX) - Find the measurement ID in Google Analytics: Admin > Data Streams > Web > Measurement ID
- If using CSP headers, add
https://www.googletagmanager.comandhttps://www.google-analytics.comtoscript-srcandconnect-src
- Set
Notes
- GA4 uses measurement IDs that start with
G-(e.g.,G-SBPZDTZ6RW) - The gtag.js script loads asynchronously and won't block page rendering
- GA is completely disabled in dev/test — no network requests to Google
- The
enabledflag provides a clean way to toggle GA without removing config - If you need to test GA locally, temporarily set
enabled: trueinconfig/dev.exs:config :<otp_app>, :google_analytics, enabled: true, measurement_id: "G-XXXXXXXXXX" - For GDPR compliance, consider adding a cookie consent banner before enabling GA — gtag.js sets cookies by default