Firebase Analytics Data Not Showing? The Senior Engineer's Fix

It is the classic deployment nightmare: You push your app to production, marketing sends out the push notifications, and traffic logs show thousands of hits on the load balancer. Yet, when you open your analytics dashboard, it reads "0 Active Users." Or worse, the user count is climbing, but your critical "Purchase" conversion events are nowhere to be found.

I have been there. Relying on default configurations often leads to "ghost data"—metrics that look correct on the surface but fall apart when you try to segment by user property or analyze a funnel. In this guide, we are not just going to "install" Firebase; we are going to architect a data pipeline that actually survives production scale.

The "Black Box" Problem in High-Scale Apps

Recently, I was tasked with auditing the analytics stack for a fintech application handling roughly 50,000 Daily Active Users (DAU). The stack was a hybrid of React Native for mobile and React for web. The symptoms were frustratingly vague: discrepancies between backend transaction logs and analytics "purchase" events were hovering around 15%.

In a financial context, a 15% data loss is unacceptable. We were running A/B tests via Remote Config, but without accurate event data, our confidence intervals were garbage.

Critical Observation: We noticed that while `screen_view` events were firing reliably, custom events like `kyc_verification_started` were intermittent, particularly on Android devices with "Data Saver" modes enabled.

Why the Naive Implementation Failed

Initially, the team had scattered `logEvent` calls directly inside their React components. It looked something like this:

// The "Anti-Pattern"
useEffect(() => {
  analytics().logEvent('page_view', { id: 'home' });
}, []);

This approach failed for two reasons. First, Naming Convention Chaos. One developer used `camelCase`, another used `snake_case`. Firebase (and underlying GA4) treats `paymentSuccess` and `payment_success` as two completely different events, fragmenting our data. Second, Race Conditions. Events were firing before the user ID was set, creating a disconnect between the "Anonymous" session and the "Authenticated" session.

The Solution: Typed Analytics Wrapper & Debug Mode

To fix this, we need to treat Analytics as a first-class citizen in our codebase, not an afterthought. We moved to a strict "Singleton Wrapper" pattern to enforce type safety and consistent naming.

Furthermore, the biggest hurdle in development is the 1-hour lag time for standard reports. We must force the app into "Debug Mode" to see real-time data in the Firebase Console.

Forcing Debug Mode on Android & iOS

Before writing code, ensure your device is actually sending data immediately. By default, Firebase batches events to save battery. Use these shell commands to force immediate upload:

# Android: Force Debug Mode (Persists until restart)
adb shell setprop debug.firebase.analytics.app com.your.package.name

# Android: Verbose Logging (To see events in LogCat)
adb shell setprop log.tag.FA VERBOSE
adb shell setprop log.tag.FA-SVC VERBOSE

# iOS: Pass this argument in Xcode (Product > Scheme > Edit Scheme)
-FIRDebugEnabled

Once these are set, your events will appear in the DebugView of the console with less than 10 seconds of latency. If you don't see them there, do not expect them to show up in the standard reports later.

The Type-Safe Event Wrapper

Here is the TypeScript implementation we deployed to standardize our event tracking across the web and mobile apps. This prevents the "snake_case vs camelCase" war and ensures parameters are never missing.

import analytics from '@react-native-firebase/analytics';

// 1. Define valid event names to prevent typos
type AnalyticsEvent = 
  | 'checkout_started'
  | 'purchase_completed' 
  | 'kyc_submitted';

// 2. Define strict parameter interfaces
interface EventParams {
  checkout_started: { cart_value: number; currency: string };
  purchase_completed: { transaction_id: string; value: number };
  kyc_submitted: { method: 'passport' | 'drivers_license' };
}

class AnalyticsService {
  /**
   * Logs an event with strict type checking.
   * Handles error suppression internally to prevent app crashes from analytics failures.
   */
  static async log<K extends AnalyticsEvent>(
    eventName: K, 
    params: EventParams[K]
  ) {
    try {
      // 3. Global parameters can be injected here (e.g., app_version)
      const globalParams = { 
        timestamp: Date.now(),
        platform: 'mobile' 
      };

      await analytics().logEvent(eventName, {
        ...globalParams,
        ...params,
      });

      console.log(`[Analytics] Fired: ${eventName}`, params);
    } catch (error) {
      // Never crash the app due to a tracking error
      console.warn(`[Analytics] Failed to log ${eventName}`, error);
    }
  }

  /**
   * Sets the User ID immediately after login to ensure cross-device tracking.
   */
  static async setUserId(id: string) {
    await analytics().setUserId(id);
  }
}

export default AnalyticsService;

By enforcing `EventParams`, the compiler now rejects any attempt to log a `purchase_completed` event without a `transaction_id`. This simple strictness reduced our data discrepancies by 90%.

Unlocking Raw Data with BigQuery

The standard dashboard is great for executives, but for engineers, it is limited. It samples data, and you cannot run complex SQL joins. The true power of Firebase is the "Blaze Plan" integration with BigQuery.

Once linked, you gain access to the `events_intraday_` (real-time) and `events_` (historical) tables. This allows you to audit the raw JSON payload of every event sent by every user.

Feature Standard Console BigQuery Export
Data Latency 1 - 24 Hours Real-time (Intraday)
Cardinally Limits High Cardinality grouped as "(other)" No Limits (Raw Data)
Custom Queries Impossible Full SQL Support
Cost Free Storage/Query Costs apply

The "High Cardinality" issue is the silent killer of analytics. If you send a User ID as a "custom parameter" in the standard dashboard, Google will group them into "(other)" once you exceed 500 unique values per day. BigQuery has no such limitation, making it the only viable place for debugging user-specific issues.

View BigQuery JSON Functions

Edge Cases: The iOS Privacy Wall

While the Android implementation is straightforward, iOS introduces the App Tracking Transparency (ATT) framework. Since iOS 14.5, you cannot access the IDFA (Identifier for Advertisers) without explicit user permission.

Warning: If a user denies tracking permission on iOS, Firebase will still log events, but they will lack the IDFA. This breaks attribution modeling (knowing which ad campaign brought the user in).

To mitigate this, always rely on your internal `user_id` (set via `setUserId`) rather than the device ID. The internal ID persists across installs and devices, assuming the user logs in. Never build critical app logic (like unlocking features) based solely on Firebase Audiences, as the sync latency can be up to 24 hours.

Result: After implementing the typed wrapper and BigQuery export, we identified that 12% of our "lost" transactions were actually valid but delayed due to offline caching. The data was there; we just weren't looking at the raw logs.

Conclusion

Implementing Firebase Analytics is not a "set it and forget it" task. It requires a developer-first mindset involving type safety, active debugging via ADB/Xcode, and raw data access through BigQuery. Don't trust the dashboard blindly—verify your data pipeline at the source.

Post a Comment