mirror of
https://github.com/jjbrunton/Sauci.git
synced 2026-10-31 18:19:43 +00:00
feat(mobile): replace Sentry with Firebase Crashlytics
Migrate error tracking from Sentry to Firebase Crashlytics. Bump version to 1.0.4, add Discord notification triggers.
This commit is contained in:
1 parent
620369a7ce
commit
14b0199625
16 files changed
+649
-525
No files matched your search
@@ -80,8 +80,6 @@ def enableProguardInReleaseBuilds = (findProperty('android.enableProguardInRelea
|
||||
*/
|
||||
def jscFlavor = 'org.webkit:android-jsc:+'
|
||||
|
||||
apply from: new File(["node", "--print", "require('path').dirname(require.resolve('@sentry/react-native/package.json'))"].execute().text.trim(), "sentry.gradle")
|
||||
|
||||
android {
|
||||
ndkVersion rootProject.ext.ndkVersion
|
||||
|
||||
@@ -93,8 +91,8 @@ android {
|
||||
applicationId 'com.sauci.app'
|
||||
minSdkVersion rootProject.ext.minSdkVersion
|
||||
targetSdkVersion rootProject.ext.targetSdkVersion
|
||||
versionCode 37
|
||||
versionName "1.0.3"
|
||||
versionCode 42
|
||||
versionName "1.0.4"
|
||||
}
|
||||
signingConfigs {
|
||||
debug {
|
||||
@@ -177,4 +175,5 @@ dependencies {
|
||||
}
|
||||
}
|
||||
|
||||
apply plugin: 'com.google.gms.google-services'
|
||||
apply plugin: 'com.google.gms.google-services'
|
||||
apply plugin: 'com.google.firebase.crashlytics'
|
||||
@@ -15,6 +15,7 @@ buildscript {
|
||||
mavenCentral()
|
||||
}
|
||||
dependencies {
|
||||
classpath 'com.google.firebase:firebase-crashlytics-gradle:3.0.6'
|
||||
classpath 'com.google.gms:google-services:4.4.1'
|
||||
classpath('com.android.tools.build:gradle')
|
||||
classpath('com.facebook.react:react-native-gradle-plugin')
|
||||
|
||||
+5
-11
@@ -2,7 +2,7 @@
|
||||
"expo": {
|
||||
"name": "Sauci",
|
||||
"slug": "sauci",
|
||||
"version": "1.0.3",
|
||||
"version": "1.0.4",
|
||||
"orientation": "portrait",
|
||||
"icon": "./assets/icon.png",
|
||||
"scheme": "app.sauci",
|
||||
@@ -16,10 +16,11 @@
|
||||
"ios": {
|
||||
"supportsTablet": false,
|
||||
"bundleIdentifier": "com.sauci.app",
|
||||
"buildNumber": "33",
|
||||
"buildNumber": "35",
|
||||
"usesAppleSignIn": true,
|
||||
"googleServicesFile": "./GoogleService-Info.plist",
|
||||
"infoPlist": {
|
||||
"GOOGLE_ANALYTICS_INAPURCHASE_COLLECTION_ENABLED": false,
|
||||
"UIBackgroundModes": [
|
||||
"remote-notification"
|
||||
],
|
||||
@@ -177,7 +178,7 @@
|
||||
"backgroundColor": "#1a1a2e"
|
||||
},
|
||||
"package": "com.sauci.app",
|
||||
"versionCode": 37,
|
||||
"versionCode": 42,
|
||||
"googleServicesFile": "./google-services.json",
|
||||
"permissions": [
|
||||
"RECEIVE_BOOT_COMPLETED",
|
||||
@@ -227,14 +228,7 @@
|
||||
"faceIDPermission": "Sauci uses Face ID to unlock the app and keep your conversations private."
|
||||
}
|
||||
],
|
||||
[
|
||||
"@sentry/react-native/expo",
|
||||
{
|
||||
"organization": "molten-prints",
|
||||
"project": "sauci"
|
||||
}
|
||||
],
|
||||
"@sentry/react-native",
|
||||
"@react-native-firebase/crashlytics",
|
||||
"expo-font",
|
||||
"expo-asset",
|
||||
[
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
// Polyfills must be imported FIRST before any other React Native imports
|
||||
import "../src/polyfills/web";
|
||||
|
||||
// Initialize Sentry early to capture all errors
|
||||
import { initSentry, setUserContext, clearUserContext } from "../src/lib/sentry";
|
||||
initSentry();
|
||||
// Initialize Crashlytics early to capture all errors
|
||||
import { initCrashlytics, setUserContext, clearUserContext } from "../src/lib/crashlytics";
|
||||
initCrashlytics();
|
||||
|
||||
// Analytics imports (initialization deferred until app mounts)
|
||||
import { initAnalytics, setUserId, clearUserId, logScreenView, Events } from "../src/lib/analytics";
|
||||
@@ -212,7 +212,7 @@ export default function RootLayout() {
|
||||
if (event === "SIGNED_IN" || event === "TOKEN_REFRESHED" || event === "USER_UPDATED") {
|
||||
console.log('[Auth] Calling fetchUser...');
|
||||
fetchUser();
|
||||
// Set Sentry user context for error tracking
|
||||
// Set Crashlytics user context for error tracking
|
||||
if (session?.user) {
|
||||
setUserContext(session.user.id, session.user.email);
|
||||
// Set Analytics user ID
|
||||
|
||||
@@ -19,7 +19,6 @@
|
||||
"env": {
|
||||
"EXPO_PUBLIC_SUPABASE_URL": "https://ckjcrkjpmhqhiucifukx.supabase.co",
|
||||
"EXPO_PUBLIC_SUPABASE_ANON_KEY": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6ImNramNya2pwbWhxaGl1Y2lmdWt4Iiwicm9sZSI6ImFub24iLCJpYXQiOjE3NjcxMDc1ODEsImV4cCI6MjA4MjY4MzU4MX0.7FA_RmFXUjlmZF96O9xlAUfxHrQrSIZcJclNReLL4lI",
|
||||
"EXPO_PUBLIC_SENTRY_DSN": "https://03a91f8a1b1ecdce2d5f38a4c23ca9d5@o4510613894070272.ingest.de.sentry.io/4510613896560720",
|
||||
"EXPO_PUBLIC_REVENUECAT_IOS_API_KEY": "appl_lfPyYonFQtVytftPAfXIrGsMNFu",
|
||||
"EXPO_PUBLIC_REVENUECAT_ANDROID_API_KEY": "goog_zoHtaRswsgvxtvYWIhZyvrPBGgh",
|
||||
"EXPO_PUBLIC_REVENUECAT_ENTITLEMENT_ID": "Sauci Pro",
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
{
|
||||
"react-native": {
|
||||
"analytics_auto_collection_enabled": true,
|
||||
"google_analytics_adid_collection_enabled": false
|
||||
"google_analytics_adid_collection_enabled": false,
|
||||
"crashlytics_auto_collection_enabled": true,
|
||||
"crashlytics_debug_enabled": false,
|
||||
"crashlytics_ndk_enabled": true
|
||||
}
|
||||
}
|
||||
@@ -21,7 +21,7 @@
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>$(PRODUCT_BUNDLE_PACKAGE_TYPE)</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>1.0.3</string>
|
||||
<string>1.0.4</string>
|
||||
<key>CFBundleSignature</key>
|
||||
<string>????</string>
|
||||
<key>CFBundleURLTypes</key>
|
||||
@@ -47,7 +47,9 @@
|
||||
</dict>
|
||||
</array>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>33</string>
|
||||
<string>35</string>
|
||||
<key>GOOGLE_ANALYTICS_INAPURCHASE_COLLECTION_ENABLED</key>
|
||||
<false/>
|
||||
<key>LSMinimumSystemVersion</key>
|
||||
<string>12.0</string>
|
||||
<key>LSRequiresIPhoneOS</key>
|
||||
|
||||
@@ -24,11 +24,11 @@
|
||||
"@react-native-async-storage/async-storage": "1.23.1",
|
||||
"@react-native-firebase/analytics": "^23.8.2",
|
||||
"@react-native-firebase/app": "^23.8.2",
|
||||
"@react-native-firebase/crashlytics": "^23.8.4",
|
||||
"@react-native-google-signin/google-signin": "^16.1.1",
|
||||
"@react-navigation/bottom-tabs": "^7.0.0",
|
||||
"@react-navigation/native": "^7.0.0",
|
||||
"@react-navigation/native-stack": "^7.0.0",
|
||||
"@sentry/react-native": "~6.10.0",
|
||||
"@supabase/supabase-js": "^2.39.0",
|
||||
"@tanstack/react-query": "^5.17.0",
|
||||
"babel-preset-expo": "~12.0.0",
|
||||
|
||||
+118
-118
@@ -72,35 +72,35 @@ function getAnalyticsInstance(): FirebaseAnalyticsTypes.Module | null {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export async function initAnalytics() {
|
||||
if (isInitialized) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Skip initialization in non-RN environments (e.g., during tsc/Node.js)
|
||||
if (typeof window === "undefined" && typeof global !== "undefined" && !("__BUNDLE_START_TIME__" in global)) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
|
||||
export async function initAnalytics() {
|
||||
if (isInitialized) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Skip initialization in non-RN environments (e.g., during tsc/Node.js)
|
||||
if (typeof window === "undefined" && typeof global !== "undefined" && !("__BUNDLE_START_TIME__" in global)) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const analytics = getAnalyticsInstance();
|
||||
if (analytics) {
|
||||
await setAnalyticsCollectionEnabled(analytics, true);
|
||||
isInitialized = true;
|
||||
console.log("[Analytics] Firebase Analytics initialized");
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn("[Analytics] Failed to initialize Firebase Analytics:", error);
|
||||
}
|
||||
}
|
||||
|
||||
// Log a custom event
|
||||
export function logEvent(
|
||||
name: string,
|
||||
params?: Record<string, string | number | boolean>
|
||||
) {
|
||||
try {
|
||||
} catch (error) {
|
||||
console.warn("[Analytics] Failed to initialize Firebase Analytics:", error);
|
||||
}
|
||||
}
|
||||
|
||||
// Log a custom event
|
||||
export function logEvent(
|
||||
name: string,
|
||||
params?: Record<string, string | number | boolean>
|
||||
) {
|
||||
try {
|
||||
if (__DEV__) console.log("[Analytics] Event:", name, params);
|
||||
const analytics = getAnalyticsInstance();
|
||||
if (analytics) {
|
||||
@@ -109,11 +109,11 @@ export function logEvent(
|
||||
} catch (error) {
|
||||
console.warn("[Analytics] Failed to log event:", error);
|
||||
}
|
||||
}
|
||||
|
||||
// Set user ID for tracking
|
||||
export function setUserId(userId: string) {
|
||||
try {
|
||||
}
|
||||
|
||||
// Set user ID for tracking
|
||||
export function setUserId(userId: string) {
|
||||
try {
|
||||
if (__DEV__) console.log("[Analytics] Set user ID:", userId);
|
||||
const analytics = getAnalyticsInstance();
|
||||
if (analytics) {
|
||||
@@ -122,11 +122,11 @@ export function setUserId(userId: string) {
|
||||
} catch (error) {
|
||||
console.warn("[Analytics] Failed to set user ID:", error);
|
||||
}
|
||||
}
|
||||
|
||||
// Clear user ID on logout
|
||||
export function clearUserId() {
|
||||
try {
|
||||
}
|
||||
|
||||
// Clear user ID on logout
|
||||
export function clearUserId() {
|
||||
try {
|
||||
if (__DEV__) console.log("[Analytics] Cleared user ID");
|
||||
const analytics = getAnalyticsInstance();
|
||||
if (analytics) {
|
||||
@@ -136,13 +136,13 @@ export function clearUserId() {
|
||||
} catch (error) {
|
||||
console.warn("[Analytics] Failed to clear user ID:", error);
|
||||
}
|
||||
}
|
||||
|
||||
// Set user properties
|
||||
export function setUserProperties(
|
||||
properties: Record<string, string | null>
|
||||
) {
|
||||
try {
|
||||
}
|
||||
|
||||
// Set user properties
|
||||
export function setUserProperties(
|
||||
properties: Record<string, string | null>
|
||||
) {
|
||||
try {
|
||||
if (__DEV__) console.log("[Analytics] Set user properties:", properties);
|
||||
const analytics = getAnalyticsInstance();
|
||||
if (analytics) {
|
||||
@@ -154,11 +154,11 @@ export function setUserProperties(
|
||||
} catch (error) {
|
||||
console.warn("[Analytics] Failed to set user properties:", error);
|
||||
}
|
||||
}
|
||||
|
||||
// Log screen view
|
||||
export function logScreenView(screenName: string, screenClass?: string) {
|
||||
try {
|
||||
}
|
||||
|
||||
// Log screen view
|
||||
export function logScreenView(screenName: string, screenClass?: string) {
|
||||
try {
|
||||
if (__DEV__) console.log("[Analytics] Screen view:", screenName);
|
||||
const analytics = getAnalyticsInstance();
|
||||
if (analytics) {
|
||||
@@ -166,12 +166,12 @@ export function logScreenView(screenName: string, screenClass?: string) {
|
||||
screen_name: screenName,
|
||||
screen_class: screenClass ?? screenName,
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn("[Analytics] Failed to log screen view:", error);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn("[Analytics] Failed to log screen view:", error);
|
||||
}
|
||||
}
|
||||
|
||||
// Flush events (useful before app backgrounding)
|
||||
export function flush() {
|
||||
// Firebase Analytics batches and sends events automatically
|
||||
@@ -193,35 +193,35 @@ function safeParam(value: unknown, maxLength = 100): string {
|
||||
|
||||
// Pre-defined event helpers for common actions
|
||||
export const Events = {
|
||||
// Auth events
|
||||
signUp: (method: string) => logEvent("sign_up", { method }),
|
||||
signIn: (method: string) => logEvent("login", { method }),
|
||||
signOut: () => logEvent("sign_out"),
|
||||
|
||||
// Onboarding events
|
||||
onboardingStart: () => logEvent("onboarding_start"),
|
||||
onboardingStageComplete: (stage: string) =>
|
||||
logEvent("onboarding_stage_complete", { stage }),
|
||||
onboardingComplete: () => logEvent("onboarding_complete"),
|
||||
|
||||
// Pairing events
|
||||
coupleCreated: () => logEvent("couple_created"),
|
||||
coupleJoined: () => logEvent("couple_joined"),
|
||||
codeShared: () => logEvent("invite_code_shared"),
|
||||
pairingCancelled: () => logEvent("pairing_cancelled"),
|
||||
|
||||
// Swipe/question events
|
||||
questionAnswered: (answer: string, packId?: string) =>
|
||||
logEvent("question_answered", { answer, pack_id: packId ?? "unknown" }),
|
||||
questionSkipped: () => logEvent("question_skipped"),
|
||||
matchCreated: (matchType: string) => logEvent("match_created", { match_type: matchType }),
|
||||
allQuestionsExhausted: () => logEvent("all_questions_exhausted"),
|
||||
|
||||
// Chat events
|
||||
matchViewed: () => logEvent("match_viewed"),
|
||||
messageSent: () => logEvent("message_sent"),
|
||||
mediaUploaded: () => logEvent("chat_media_uploaded"),
|
||||
|
||||
// Auth events
|
||||
signUp: (method: string) => logEvent("sign_up", { method }),
|
||||
signIn: (method: string) => logEvent("login", { method }),
|
||||
signOut: () => logEvent("sign_out"),
|
||||
|
||||
// Onboarding events
|
||||
onboardingStart: () => logEvent("onboarding_start"),
|
||||
onboardingStageComplete: (stage: string) =>
|
||||
logEvent("onboarding_stage_complete", { stage }),
|
||||
onboardingComplete: () => logEvent("onboarding_complete"),
|
||||
|
||||
// Pairing events
|
||||
coupleCreated: () => logEvent("couple_created"),
|
||||
coupleJoined: () => logEvent("couple_joined"),
|
||||
codeShared: () => logEvent("invite_code_shared"),
|
||||
pairingCancelled: () => logEvent("pairing_cancelled"),
|
||||
|
||||
// Swipe/question events
|
||||
questionAnswered: (answer: string, packId?: string) =>
|
||||
logEvent("question_answered", { answer, pack_id: packId ?? "unknown" }),
|
||||
questionSkipped: () => logEvent("question_skipped"),
|
||||
matchCreated: (matchType: string) => logEvent("match_created", { match_type: matchType }),
|
||||
allQuestionsExhausted: () => logEvent("all_questions_exhausted"),
|
||||
|
||||
// Chat events
|
||||
matchViewed: () => logEvent("match_viewed"),
|
||||
messageSent: () => logEvent("message_sent"),
|
||||
mediaUploaded: () => logEvent("chat_media_uploaded"),
|
||||
|
||||
// Subscription events
|
||||
paywallShown: (source: string) => logEvent("paywall_shown", { source }),
|
||||
paywallClosed: (source: string, reason: string) =>
|
||||
@@ -240,42 +240,42 @@ export const Events = {
|
||||
error_message: safeParam(errorMessage),
|
||||
}),
|
||||
purchaseCompleted: (packageType: string) =>
|
||||
logEvent("purchase", { package_type: packageType }),
|
||||
logEvent("purchase_completed", { package_type: packageType }),
|
||||
purchaseRestored: () => logEvent("purchase_restored"),
|
||||
|
||||
// Pack events
|
||||
packEnabled: (packId: string) => logEvent("pack_enabled", { pack_id: packId }),
|
||||
packDisabled: (packId: string) => logEvent("pack_disabled", { pack_id: packId }),
|
||||
|
||||
// App lifecycle events
|
||||
appOpened: (type: "cold" | "warm") => logEvent("app_opened", { type }),
|
||||
|
||||
// Profile events
|
||||
profileUpdated: (fields: string[]) => logEvent("profile_updated", { fields: fields.join(",") }),
|
||||
avatarUploaded: () => logEvent("avatar_uploaded"),
|
||||
|
||||
// Notification events
|
||||
notificationPermissionGranted: () => logEvent("notification_permission_granted"),
|
||||
notificationPermissionDenied: () => logEvent("notification_permission_denied"),
|
||||
|
||||
// Relationship events
|
||||
relationshipEnded: () => logEvent("relationship_ended"),
|
||||
accountDeleted: () => logEvent("account_deleted"),
|
||||
|
||||
// Milestone events
|
||||
firstMatch: () => logEvent("first_match"),
|
||||
milestoneMatch: (count: number) => logEvent("milestone_match", { match_count: count }),
|
||||
|
||||
// Invite events
|
||||
inviteCodeCopied: () => logEvent("invite_code_copied"),
|
||||
|
||||
// Tutorial events
|
||||
tutorialStarted: (tutorial: "swipe" | "matches") =>
|
||||
logEvent("tutorial_started", { tutorial }),
|
||||
tutorialStepViewed: (tutorial: "swipe" | "matches", step: number, stepName: string) =>
|
||||
logEvent("tutorial_step_viewed", { tutorial, step, step_name: stepName }),
|
||||
tutorialCompleted: (tutorial: "swipe" | "matches") =>
|
||||
logEvent("tutorial_completed", { tutorial }),
|
||||
tutorialSkipped: (tutorial: "swipe" | "matches", atStep: number) =>
|
||||
logEvent("tutorial_skipped", { tutorial, at_step: atStep }),
|
||||
};
|
||||
|
||||
// Pack events
|
||||
packEnabled: (packId: string) => logEvent("pack_enabled", { pack_id: packId }),
|
||||
packDisabled: (packId: string) => logEvent("pack_disabled", { pack_id: packId }),
|
||||
|
||||
// App lifecycle events
|
||||
appOpened: (type: "cold" | "warm") => logEvent("app_opened", { type }),
|
||||
|
||||
// Profile events
|
||||
profileUpdated: (fields: string[]) => logEvent("profile_updated", { fields: fields.join(",") }),
|
||||
avatarUploaded: () => logEvent("avatar_uploaded"),
|
||||
|
||||
// Notification events
|
||||
notificationPermissionGranted: () => logEvent("notification_permission_granted"),
|
||||
notificationPermissionDenied: () => logEvent("notification_permission_denied"),
|
||||
|
||||
// Relationship events
|
||||
relationshipEnded: () => logEvent("relationship_ended"),
|
||||
accountDeleted: () => logEvent("account_deleted"),
|
||||
|
||||
// Milestone events
|
||||
firstMatch: () => logEvent("first_match"),
|
||||
milestoneMatch: (count: number) => logEvent("milestone_match", { match_count: count }),
|
||||
|
||||
// Invite events
|
||||
inviteCodeCopied: () => logEvent("invite_code_copied"),
|
||||
|
||||
// Tutorial events
|
||||
tutorialStarted: (tutorial: "swipe" | "matches") =>
|
||||
logEvent("tutorial_started", { tutorial }),
|
||||
tutorialStepViewed: (tutorial: "swipe" | "matches", step: number, stepName: string) =>
|
||||
logEvent("tutorial_step_viewed", { tutorial, step, step_name: stepName }),
|
||||
tutorialCompleted: (tutorial: "swipe" | "matches") =>
|
||||
logEvent("tutorial_completed", { tutorial }),
|
||||
tutorialSkipped: (tutorial: "swipe" | "matches", atStep: number) =>
|
||||
logEvent("tutorial_skipped", { tutorial, at_step: atStep }),
|
||||
};
|
||||
@@ -0,0 +1,128 @@
|
||||
import { Platform } from "react-native";
|
||||
import Constants from "expo-constants";
|
||||
import crashlytics from "@react-native-firebase/crashlytics";
|
||||
|
||||
let isInitialized = false;
|
||||
let crashlyticsUnavailable = false;
|
||||
let crashlyticsUnavailableLogged = false;
|
||||
|
||||
function getErrorMessage(error: unknown): string {
|
||||
if (!error) return "";
|
||||
if (typeof error === "string") return error;
|
||||
if (error instanceof Error) return error.message;
|
||||
if (typeof (error as { message?: unknown }).message === "string") {
|
||||
return (error as { message?: string }).message || "";
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
function isCrashlyticsSupported(): boolean {
|
||||
if (Platform.OS === "web") return false;
|
||||
if (Constants.appOwnership === "expo") return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
function logCrashlyticsUnavailableOnce(reason: string) {
|
||||
if (crashlyticsUnavailableLogged) return;
|
||||
crashlyticsUnavailableLogged = true;
|
||||
if (__DEV__) console.log("[Crashlytics] " + reason);
|
||||
}
|
||||
|
||||
function getCrashlyticsInstance(): ReturnType<typeof crashlytics> | null {
|
||||
if (crashlyticsUnavailable) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!isCrashlyticsSupported()) {
|
||||
crashlyticsUnavailable = true;
|
||||
logCrashlyticsUnavailableOnce("Skipping Firebase Crashlytics on Expo Go or web.");
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
return crashlytics();
|
||||
} catch (error) {
|
||||
const message = getErrorMessage(error);
|
||||
if (message.includes("module could not be found")) {
|
||||
crashlyticsUnavailable = true;
|
||||
logCrashlyticsUnavailableOnce(
|
||||
"Firebase Crashlytics native module missing. Rebuild with a dev client or native app."
|
||||
);
|
||||
return null;
|
||||
}
|
||||
console.warn("[Crashlytics] Failed to get crashlytics instance:", error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export function initCrashlytics() {
|
||||
if (isInitialized) return;
|
||||
|
||||
try {
|
||||
const instance = getCrashlyticsInstance();
|
||||
if (instance) {
|
||||
instance.setCrashlyticsCollectionEnabled(true);
|
||||
isInitialized = true;
|
||||
console.log("[Crashlytics] Firebase Crashlytics initialized");
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn("[Crashlytics] Failed to initialize:", error);
|
||||
}
|
||||
}
|
||||
|
||||
export function captureError(error: Error, context?: Record<string, unknown>) {
|
||||
try {
|
||||
const instance = getCrashlyticsInstance();
|
||||
if (!instance) return;
|
||||
|
||||
if (context) {
|
||||
for (const [key, value] of Object.entries(context)) {
|
||||
instance.setAttribute(key, String(value ?? ""));
|
||||
}
|
||||
}
|
||||
instance.recordError(error);
|
||||
} catch (e) {
|
||||
console.warn("[Crashlytics] Failed to capture error:", e);
|
||||
}
|
||||
}
|
||||
|
||||
export function setUserContext(userId: string, email?: string) {
|
||||
try {
|
||||
const instance = getCrashlyticsInstance();
|
||||
if (!instance) return;
|
||||
|
||||
instance.setUserId(userId);
|
||||
if (email) {
|
||||
instance.setAttribute("email", email);
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn("[Crashlytics] Failed to set user context:", error);
|
||||
}
|
||||
}
|
||||
|
||||
export function clearUserContext() {
|
||||
try {
|
||||
const instance = getCrashlyticsInstance();
|
||||
if (!instance) return;
|
||||
|
||||
instance.setUserId("");
|
||||
instance.setAttribute("email", "");
|
||||
} catch (error) {
|
||||
console.warn("[Crashlytics] Failed to clear user context:", error);
|
||||
}
|
||||
}
|
||||
|
||||
export function addBreadcrumb(
|
||||
message: string,
|
||||
category: string,
|
||||
data?: Record<string, unknown>
|
||||
) {
|
||||
try {
|
||||
const instance = getCrashlyticsInstance();
|
||||
if (!instance) return;
|
||||
|
||||
instance.log(`[${category}] ${message}${data ? " " + JSON.stringify(data) : ""}`);
|
||||
} catch (error) {
|
||||
console.warn("[Crashlytics] Failed to add breadcrumb:", error);
|
||||
}
|
||||
}
|
||||
@@ -3,7 +3,7 @@ import Constants from "expo-constants";
|
||||
import { Platform } from "react-native";
|
||||
import { router } from "expo-router";
|
||||
import { supabase } from "./supabase";
|
||||
import { captureError } from "./sentry";
|
||||
import { captureError } from "./crashlytics";
|
||||
import { Events } from "./analytics";
|
||||
import { useMatchStore } from "../store/matchStore";
|
||||
|
||||
|
||||
@@ -1,72 +0,0 @@
|
||||
import * as Sentry from "@sentry/react-native";
|
||||
import Constants from "expo-constants";
|
||||
|
||||
const SENTRY_DSN = process.env.EXPO_PUBLIC_SENTRY_DSN;
|
||||
|
||||
export function initSentry() {
|
||||
if (!SENTRY_DSN) {
|
||||
console.warn("Sentry DSN not configured - error tracking disabled");
|
||||
return;
|
||||
}
|
||||
|
||||
Sentry.init({
|
||||
dsn: SENTRY_DSN,
|
||||
debug: __DEV__,
|
||||
enabled: !__DEV__, // Only enable in production
|
||||
environment: __DEV__ ? "development" : "production",
|
||||
release: Constants.expoConfig?.version ?? "1.0.0",
|
||||
dist: Constants.expoConfig?.ios?.buildNumber ?? Constants.expoConfig?.android?.versionCode?.toString(),
|
||||
|
||||
// Performance monitoring
|
||||
tracesSampleRate: 0.2, // 20% of transactions for performance
|
||||
|
||||
// Session tracking
|
||||
enableAutoSessionTracking: true,
|
||||
sessionTrackingIntervalMillis: 30000,
|
||||
|
||||
// Capture unhandled promise rejections
|
||||
enableNative: true,
|
||||
|
||||
// Filter out noisy errors
|
||||
beforeSend(event) {
|
||||
// Filter out network errors that are expected (e.g., user offline)
|
||||
if (event.exception?.values?.[0]?.value?.includes("Network request failed")) {
|
||||
return null;
|
||||
}
|
||||
return event;
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
// Helper to capture errors with context
|
||||
export function captureError(error: Error, context?: Record<string, unknown>) {
|
||||
if (context) {
|
||||
Sentry.setContext("additional", context);
|
||||
}
|
||||
Sentry.captureException(error);
|
||||
}
|
||||
|
||||
// Set user context when they log in
|
||||
export function setUserContext(userId: string, email?: string) {
|
||||
Sentry.setUser({
|
||||
id: userId,
|
||||
email,
|
||||
});
|
||||
}
|
||||
|
||||
// Clear user context on logout
|
||||
export function clearUserContext() {
|
||||
Sentry.setUser(null);
|
||||
}
|
||||
|
||||
// Add breadcrumb for debugging
|
||||
export function addBreadcrumb(message: string, category: string, data?: Record<string, unknown>) {
|
||||
Sentry.addBreadcrumb({
|
||||
message,
|
||||
category,
|
||||
data,
|
||||
level: "info",
|
||||
});
|
||||
}
|
||||
|
||||
export { Sentry };
|
||||
@@ -58,7 +58,7 @@ if (!isTestEnv && (!supabaseUrl || !supabaseAnonKey)) {
|
||||
if (!supabaseAnonKey) missing.push('EXPO_PUBLIC_SUPABASE_ANON_KEY');
|
||||
const errorMsg = `Missing required environment variables: ${missing.join(', ')}. App cannot start.`;
|
||||
console.error('[Supabase]', errorMsg);
|
||||
// In production, this will be caught by Sentry before crashing
|
||||
// In production, this will be caught by Crashlytics before crashing
|
||||
throw new Error(errorMsg);
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user