GenerateSaaS

Analytics

Wire one or more analytics providers through config.analytics, gating cookie-dropping ones behind consent.

config.analytics (in @repo/config) enables web analytics - there is no backend package, scripts render client-side. The presence of a provider sub-key turns it on; there is no single enabled flag.

Providers

Add a provider's sub-key under config.analytics to enable it. The two tiers differ only by GDPR cookie use: consent-required providers wait for the cookie banner, privacy-focused ones load immediately.

ProviderKeyConfig valueConsent required?
Google Analytics 4google{ measurementId, ads? }Yes - waits for consent
PostHogposthog{ publicKey, host }Yes - waits for consent
Umamiumami{ websiteId, host }No - privacy-focused
Plausibleplausible{ domain?, host? }No - privacy-focused
OpenPanelopenpanel{ clientId, trackScreenViews? }No - privacy-focused
DataFastdatafast{ websiteId, domain }No - privacy-focused
Ahrefsahrefs{ key }No - privacy-focused
Vercel AnalyticsverceltrueNo - privacy-focused
Vercel Speed InsightsvercelSpeedInsightstrueNo - performance only
  • Shipped default: a generated project ships with no config.analytics key, so every provider is absent and off until you add one.
  • Consent-required (google, posthog) drop cookies and fire only after the visitor accepts.
  • Privacy-focused never trigger the consent banner and never wait on it.
  • vercel and vercelSpeedInsights are booleans (true to enable); every other provider takes a config object whose keys are all required unless marked ?.
  • vercelSpeedInsights ships Core Web Vitals only - it does not count toward isEnabled or fire track/trackRevenue/identify.

All analytics keys are public (publishable) - they ship to the browser. Never paste a secret API key into config.

Development behavior

config.analytics.enableInDev controls whether scripts load during local development.

KeyTypeType default (key omitted)Description
enableInDevbooleanfalseLoad analytics locally so you can test tracking before deploying
  • When omitted, analytics stay dark in development - so a generated config without this key never pollutes real dashboards.
  • A generated project ships no config.analytics key at all, so enableInDev stays false until you add a provider. Set it true while wiring a provider to test tracking locally, then false once you point providers at your own dashboards.

Tracking calls

One unified useAnalytics() returns the dispatch functions; each fans out to every enabled provider at once (consent-required ones only once consent is granted).

const { track, trackRevenue, identify, reset } = useAnalytics();

track("signup_completed", { plan: "pro" }); // custom event → all providers
trackRevenue(29, "USD", { orderId: "ord_123" }); // revenue event → all providers
identify(userId, { email }); // user → consent-aware subset (see note)
reset(); // clear identity → called automatically on sign-out
FunctionSignatureReaches
track(event, properties?)every enabled provider
trackRevenue(amount, currency, options?)every enabled provider; options takes orderId and email
identify(userId, traits?)only providers with a user/profile concept (Umami, OpenPanel, DataFast, GA, PostHog)
reset()providers that store an identity (GA, PostHog, OpenPanel) - clears it
  • useAnalytics() runs client-side only; all functions no-op on the server and when no provider is enabled.
  • identify fires automatically for authenticated users, and reset fires automatically when the session ends (sign-out, expiry) - so the next visitor on the same browser is never attributed to the old account. You only call them yourself for custom flows.
  • GA receives the pseudonymous user_id only - Google's policies prohibit sending PII (email, name) to GA, so identify traits are dropped there.
  • When payments are enabled, the revenue event emits automatically after checkout - you don't call trackRevenue yourself for purchases.
  • trackRevenue auto-fills email from the current session for DataFast attribution when you omit it.
  • Adding more providers needs no code change - the same calls reach all of them.

Built-in events

The boilerplate fires the full conversion funnel automatically - you only add track calls for product-specific events. Names follow GA4 recommended events where one exists; everything flows through the same fan-out, so every enabled provider receives every event.

EventFired whenProperties
sign_upAn account is created (password form, OAuth, magic link)method
loginA returning user signs in (any method, incl. passkey and 2FA)method
begin_checkoutA checkout is initiated for a plan, product, or creditstype, planId / productId / credits, interval
purchaseThe user returns from a successful checkoutamount, currency, orderId, type
checkout_cancelledThe user backs out of the provider checkout-
waitlist_joinedA visitor joins the waitlist-
onboarding_completedThe onboarding form is submittedmarketingOptIn
organization_createdAn organization is createdsource (onboarding / settings)
member_invitedAn organization invitation is sentrole
invitation_acceptedAn organization invitation is accepted-
api_key_createdAn API key is created in developer settings-
newsletter_subscribed / newsletter_unsubscribedThe marketing-emails toggle in profile settings changes-
contact_form_submittedThe contact form is sent successfully-
account_deletedThe user confirms account deletion-
  • Auth flows that end in a redirect (OAuth, magic link, post-login navigation) are tracked after the redirect: the attempt is marked in localStorage (15 minute TTL) and resolved on the next authenticated page load, so the event is never lost to the navigation.
  • For those redirect flows, accounts created within the last 5 minutes resolve to sign_up; older accounts resolve to login.
  • Pageviews are not in this table because each provider's script reports them automatically.

Running Google Ads needs conversions reported back to Google. Add ads under config.analytics.google to wire conversion tracking onto the gtag.js that GA4 already loads - no extra script, same consent gating:

analytics: {
  google: {
    measurementId: "G-XXXXXXXXXX",
    ads: {
      conversionId: "AW-XXXXXXXXX",
      conversionLabels: {
        purchase: "AbC-D_efG-h12_34iJ",
        sign_up: "ZyX-w9vU8t76_54sR",
      },
    },
  },
},

Both values come from Google Ads: create a conversion action under Goals → Conversions, choose the Google tag setup, and copy the AW- conversion ID plus the per-action label from the snippet it shows.

  • conversionId alone registers Google Ads as a second gtag destination: remarketing audiences and the conversion linker (click-ID capture) work immediately. Consent grants already carry the Consent Mode v2 signals (ad_storage, ad_user_data, ad_personalization) Google requires for EEA traffic.
  • conversionLabels maps built-in or custom event names to conversion labels; each labeled event additionally fires a conversion ping to AW-XXXXXXXXX/LABEL. The purchase label automatically carries value, currency, and transaction_id, so value-based Smart Bidding and conversion deduplication work out of the box (enable "use transaction IDs" on the conversion action).
  • The ads key is strictly additive: omit it and GA4 behaves exactly as before; omit a label and that event sends no conversion ping.

Prefer not to tag conversions in code? Link GA4 to your Google Ads account and import GA4 key events as conversions instead - that path needs no ads config at all, at the cost of delayed, modeled imports.

Frequently asked questions

Why is there no analytics.enabled flag? A provider is on exactly when its sub-key is present. Remove the sub-key to disable it; remove config.analytics entirely to disable all analytics.

Which providers force the cookie banner to appear? Only google and posthog. Under the "auto" banner mode, enabling either surfaces the consent banner for GDPR visitors - privacy-focused providers never do. See cookie consent.

Do my keys leak by being in config? No - analytics keys are publishable by design and meant to ship to the browser. The warning is only to stop secret keys from ending up there.

Next steps

On this page