← 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
M apps/mobile/App.tsxA apps/mobile/analytics.tsM apps/mobile/app.jsonM apps/mobile/package.json
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 →