[object Object]

← back to Nineoh Guide

analytics: wire Firebase Analytics (→GA4) + Crashlytics into the 90210 iOS app — screen-view tracking on tab change, safe no-op wrapper, expo-build-properties static frameworks + googleServicesFile

702110a9d54e817844a3e4df542afe71abe06853 · 2026-08-05 15:57:06 -0700 · Steve

Files touched

Diff

commit 702110a9d54e817844a3e4df542afe71abe06853
Author: Steve <steve@designerwallcoverings.com>
Date:   Wed Aug 5 15:57:06 2026 -0700

    analytics: wire Firebase Analytics (→GA4) + Crashlytics into the 90210 iOS app — screen-view tracking on tab change, safe no-op wrapper, expo-build-properties static frameworks + googleServicesFile
---
 apps/mobile/App.tsx      |  7 +++++++
 apps/mobile/analytics.ts | 47 +++++++++++++++++++++++++++++++++++++++++++++++
 apps/mobile/app.json     |  9 +++++++++
 apps/mobile/package.json |  4 ++++
 4 files changed, 67 insertions(+)

diff --git a/apps/mobile/App.tsx b/apps/mobile/App.tsx
index 075588e..2f10796 100644
--- a/apps/mobile/App.tsx
+++ b/apps/mobile/App.tsx
@@ -26,6 +26,7 @@ import { requestTrackingPermissionsAsync } from "expo-tracking-transparency";
 // TEST ad unit for now — replace with your real AdMob banner unit ID before launch.
 const BANNER_UNIT_ID = TestIds.BANNER;
 import { APP, EpisodeSchema, type Episode } from "@nineoh/core";
+import { logScreen } from "./analytics";
 
 // API base resolution (in precedence order):
 //  1. EXPO_PUBLIC_API_BASE  — set as an EAS Secret for TestFlight/prod builds
@@ -179,6 +180,12 @@ function isAhead(
 export default function App() {
   const [tab, setTab] = useState<TabKey>("episodes");
 
+  // Firebase Analytics (→ GA4): log a screen view whenever the visible tab
+  // changes (also fires once on mount for the initial "episodes" tab).
+  useEffect(() => {
+    logScreen(tab);
+  }, [tab]);
+
   // Show selector — the guide covers two shows; default to the ORIGINAL.
   const [shows, setShows] = useState<Show[]>([]);
   const [selectedShowId, setSelectedShowId] = useState<string | null>(null);
diff --git a/apps/mobile/analytics.ts b/apps/mobile/analytics.ts
new file mode 100644
index 0000000..bb9f30f
--- /dev/null
+++ b/apps/mobile/analytics.ts
@@ -0,0 +1,47 @@
+// analytics.ts — Firebase Analytics (→ GA4) + Crashlytics for the 90210 guide.
+//
+// Screen views + custom events flow to GA4 (viewable in the Firebase / GA4 iOS
+// apps); Crashlytics auto-captures native + unhandled-JS crashes and lets us
+// record handled errors. All calls are safe no-ops if the native Firebase
+// module isn't present (e.g. Expo Go / a build without GoogleService-Info.plist),
+// so the app never crashes because analytics isn't wired.
+import analytics from "@react-native-firebase/analytics";
+import crashlytics from "@react-native-firebase/crashlytics";
+
+let ready = true;
+try {
+  analytics();
+} catch {
+  ready = false;
+}
+
+/** Log a screen view (call whenever the visible tab/screen changes). */
+export async function logScreen(screen: string): Promise<void> {
+  if (!ready) return;
+  try {
+    await analytics().logScreenView({ screen_name: screen, screen_class: screen });
+  } catch {
+    /* non-fatal */
+  }
+}
+
+/** Log a custom event (e.g. episode_open, watchlist_add). */
+export async function logEvent(name: string, params?: Record<string, unknown>): Promise<void> {
+  if (!ready) return;
+  try {
+    await analytics().logEvent(name, params as Record<string, string | number | boolean>);
+  } catch {
+    /* non-fatal */
+  }
+}
+
+/** Record a handled error to Crashlytics (unhandled crashes are auto-captured). */
+export function recordError(err: unknown, context?: string): void {
+  if (!ready) return;
+  try {
+    if (context) crashlytics().log(context);
+    crashlytics().recordError(err instanceof Error ? err : new Error(String(err)));
+  } catch {
+    /* non-fatal */
+  }
+}
diff --git a/apps/mobile/app.json b/apps/mobile/app.json
index cfe55ce..135f355 100644
--- a/apps/mobile/app.json
+++ b/apps/mobile/app.json
@@ -11,12 +11,21 @@
       "bundleIdentifier": "com.abrams.nineohguide",
       "supportsTablet": true,
       "usesAppleSignIn": true,
+      "googleServicesFile": "./GoogleService-Info.plist",
       "infoPlist": {
         "ITSAppUsesNonExemptEncryption": false
       }
     },
     "plugins": [
       "expo-apple-authentication",
+      "@react-native-firebase/app",
+      "@react-native-firebase/crashlytics",
+      [
+        "expo-build-properties",
+        {
+          "ios": { "useFrameworks": "static" }
+        }
+      ],
       [
         "react-native-google-mobile-ads",
         {
diff --git a/apps/mobile/package.json b/apps/mobile/package.json
index 141d70a..67fba21 100644
--- a/apps/mobile/package.json
+++ b/apps/mobile/package.json
@@ -13,10 +13,14 @@
     "@babel/runtime": "^7.29.0",
     "@nineoh/core": "workspace:*",
     "@react-native-async-storage/async-storage": "2.2.0",
+    "@react-native-firebase/app": "^22.4.0",
+    "@react-native-firebase/analytics": "^22.4.0",
+    "@react-native-firebase/crashlytics": "^22.4.0",
     "babel-preset-expo": "~57.0.5",
     "expo": "~57.0.10",
     "expo-apple-authentication": "~57.0.1",
     "expo-blur": "~57.0.2",
+    "expo-build-properties": "~1.0.9",
     "expo-constants": "~57.0.9",
     "expo-linear-gradient": "~57.0.1",
     "expo-status-bar": "~57.0.1",

← f1299ed auto-save: 2026-08-05T15:43:31 (1 files) — db/ingest-origina  ·  back to Nineoh Guide  ·  Add Fan Club — cast-hosted podcasts (90210MG, misSPELLING, I 7a969e0 →