Skip to content
Skip to content
Goodspeed

BUILT INTO EVERY GOODSPEED APP

Analytics

PostHog is wired through a consent-gated lazy singleton: when GDPR consent is on, no client is created until the user grants it. captureEvent, identify, and automatic $screen tracking on every expo-router navigation are then available, with offline events queued and flushed by the SDK.

  • Tier: Core
  • Status: Config-toggled
  • Config: features.analytics.enabled

WHY IT MATTERS

Analytics added after launch usually means a scramble: instrument the screens you forgot, reconcile event names, and accept that the first cohort's behavior is already gone. The template wires PostHog from the first build through lib/posthog.ts with a consent-gated lazy singleton. getPostHog() returns null, and every capture becomes a no-op, until three things hold: features.analytics.enabled is true, EXPO_PUBLIC_POSTHOG_API_KEY is set, and consent is satisfied. When GDPR consent is enabled (features.compliance.gdprConsent), the client is not created until the stored consent records analytics as true, and onConsentChanged(false) resets and nulls the client mid-session. captureEvent, identifyPostHogUser, and resetPostHogUser are the surface most screens touch.

Screen tracking is automatic. A useEffect in app/_layout.tsx watches expo-router's pathname and fires a $screen event with the route name on every navigation, including tab switches and modal pushes, so you do not hand-instrument each screen. Because every generated Goodspeed app reports into a shared PostHog project, the client registers app_slug and app_id as super-properties and creates a group('app', appId) association, which lets one app be filtered out of the shared project without standing up a separate PostHog instance per app. Events captured while offline are queued by posthog-react-native in AsyncStorage and flushed on reconnect. identifyPostHogUser stitches anonymous pre-login events to the user after sign-in, and resetPostHogUser clears that link on logout.

HOW IT IS WIRED

Real code from the GAS template

The code below is drawn from lib/posthog.ts in the gas-template repository. This is the code your generated app gets, not pseudocode, not a description of intent.

// lib/posthog.ts
export async function getPostHog(): Promise<PostHog | null> {
  if (!gasConfig.features.analytics.enabled || !apiKey) return null;

  const hasConsent = await checkAnalyticsConsent();
  if (!hasConsent) return null;

  if (!_posthog) {
    _posthog = new PostHog(apiKey, { host });

    // Shared project: tag every event with this app's id + slug
    const appId = process.env.EXPO_PUBLIC_GOODSPEED_APP_ID ?? '';
    const superProps: Record<string, string> = { app_slug: gasConfig.app.slug };
    if (appId) superProps.app_id = appId;
    _posthog.register(superProps);
    if (appId) _posthog.group('app', appId);

    if (gasConfig.features.analytics.sessionRecording) {
      _posthog.register({ $session_recording_enabled: true });
    }
  }

  return _posthog;
}

Source: goodspeed-apps/gas-template lib/posthog.ts

HONEST LIMITS

When Analytics is the wrong choice

PostHog is a cloud product. The default host is us.i.posthog.com, and while posthog-react-native queues events offline and flushes them later, all of that data ultimately leaves the device for PostHog's cloud. There is no self-hosted ingestion path provisioned by the template; you can point EXPO_PUBLIC_POSTHOG_HOST at your own instance, but standing up and operating that infrastructure is on you. For an app under a strict no-third-party-data mandate, plan for self-hosting or a different analytics layer before launch rather than after. Data residency is a related gap to close deliberately. The default host is the US ingestion endpoint, and nothing in the template enforces an EU endpoint even when features.compliance.gdprConsent is on. An app serving EU users that needs GDPR-aligned residency has to set EXPO_PUBLIC_POSTHOG_HOST to the EU host explicitly. Treat the host as a compliance decision, not a default, and confirm it matches where your users are before you ship.

Tier: Core · Config-toggled

  1. Evaluate your use case

    Check whether analytics aligns with your target audience, platform constraints, and regulatory environment before enabling it.

  2. Audit the config

    The `features.analytics.enabled` flag controls this feature. Set it to false in gas.config.ts to disable the feature entirely with no residual code paths.

  3. Seek alternatives

    If the built-in implementation does not fit, the generated codebase is standard React Native + Expo code. Any library in the Expo ecosystem can replace the default.

APPS USING THIS FEATURE

Apps built with Analytics

These apps were generated by Goodspeed and use analytics as a core part of their experience. Each link goes to the full app marketing page.

CAPABILITIES

Analytics capability breakdown

Concrete dimensions of what the built-in analytics implementation covers. These reflect the actual template code, not a marketing summary.

ItemDescriptionStrength
Event capturecaptureEvent(event, properties) or useAnalytics().track() from any component; properties pass through sanitizeData before dispatch, and the call is a no-op when the client is null.captureEvent
Automatic screen trackingA useEffect on expo-router's pathname fires a $screen event with the route name on every navigation, including tab switches and modal pushes, with no per-screen wiring.expo-router
Identity stitchingidentifyPostHogUser(userId, traits) associates prior anonymous events with the authenticated user after login; resetPostHogUser() clears the distinct id on logout.identify
Consent gateWhen features.compliance.gdprConsent is on, getPostHog stays null until consent records analytics as true; onConsentChanged(false) shuts the client down mid-session.GDPR-aware
Shared-project taggingEvery app registers app_slug and app_id super-properties and a group('app', appId) association, so per-app reporting works inside one shared PostHog project with no separate instance.Super-props

COMMON QUESTIONS

Does the app track users before they accept a consent banner?

When features.compliance.gdprConsent is on, no. getPostHog() calls checkAnalyticsConsent() first and stays null until AsyncStorage records analytics consent as true, so any captureEvent before that is a no-op. One nuance worth knowing: consent checking fails open, so on a brand-new install with no stored consent record yet, events can be captured by default until the banner is answered with a denial. If you need strict opt-in with zero pre-consent capture, set the default consent state to denied and require an explicit accept before the first event.

Can I self-host PostHog or use the EU region?

Yes, by configuration. The client is created as new PostHog(apiKey, { host }), where host comes from EXPO_PUBLIC_POSTHOG_HOST and defaults to us.i.posthog.com. Point it at eu.i.posthog.com for the EU region, or at your own self-hosted PostHog URL. The template does not provision either, so the infrastructure and its operation are yours; only the endpoint is a config change.

How does automatic screen tracking work, and can I turn it off?

A useEffect in app/_layout.tsx depends on expo-router's pathname and calls captureEvent('$screen', { $screen_name: pathname }) on every route change, so navigation is tracked without touching individual screens. To suppress it for a route, branch on the pathname before capturing, or remove the effect if you prefer to fire $screen manually. Because it runs through captureEvent, it inherits the same consent and enabled gates, so disabling analytics or denying consent also stops screen events.

What happens to events captured while the device is offline?

posthog-react-native queues events in AsyncStorage and flushes them when connectivity returns; that buffering is handled inside the SDK rather than in lib/posthog.ts, so you do not lose events during a dead zone. The template does not override the SDK's flush interval or queue size, so those follow PostHog's defaults. If you need tighter control over batching, configure the PostHog client options where it is constructed in lib/posthog.ts.

GET IT BUILT INTO YOUR APP

Score your idea and get analytics wired in from day one