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:
jjbrunton committed 2026-01-27 22:08:43 +00:00
1 parent 620369a7ce
commit 14b0199625
16 files changed
+649 -525

No files matched your search

+1
View File
@@ -23,6 +23,7 @@ apps/mobile/.env.*
# Supabase # Supabase
.supabase/ .supabase/
supabase/.temp/
# IDE # IDE
.idea/ .idea/
+4 -5
View File
@@ -80,8 +80,6 @@ def enableProguardInReleaseBuilds = (findProperty('android.enableProguardInRelea
*/ */
def jscFlavor = 'org.webkit:android-jsc:+' 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 { android {
ndkVersion rootProject.ext.ndkVersion ndkVersion rootProject.ext.ndkVersion
@@ -93,8 +91,8 @@ android {
applicationId 'com.sauci.app' applicationId 'com.sauci.app'
minSdkVersion rootProject.ext.minSdkVersion minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion targetSdkVersion rootProject.ext.targetSdkVersion
versionCode 37 versionCode 42
versionName "1.0.3" versionName "1.0.4"
} }
signingConfigs { signingConfigs {
debug { 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'
+1
View File
@@ -15,6 +15,7 @@ buildscript {
mavenCentral() mavenCentral()
} }
dependencies { dependencies {
classpath 'com.google.firebase:firebase-crashlytics-gradle:3.0.6'
classpath 'com.google.gms:google-services:4.4.1' classpath 'com.google.gms:google-services:4.4.1'
classpath('com.android.tools.build:gradle') classpath('com.android.tools.build:gradle')
classpath('com.facebook.react:react-native-gradle-plugin') classpath('com.facebook.react:react-native-gradle-plugin')
+5 -11
View File
@@ -2,7 +2,7 @@
"expo": { "expo": {
"name": "Sauci", "name": "Sauci",
"slug": "sauci", "slug": "sauci",
"version": "1.0.3", "version": "1.0.4",
"orientation": "portrait", "orientation": "portrait",
"icon": "./assets/icon.png", "icon": "./assets/icon.png",
"scheme": "app.sauci", "scheme": "app.sauci",
@@ -16,10 +16,11 @@
"ios": { "ios": {
"supportsTablet": false, "supportsTablet": false,
"bundleIdentifier": "com.sauci.app", "bundleIdentifier": "com.sauci.app",
"buildNumber": "33", "buildNumber": "35",
"usesAppleSignIn": true, "usesAppleSignIn": true,
"googleServicesFile": "./GoogleService-Info.plist", "googleServicesFile": "./GoogleService-Info.plist",
"infoPlist": { "infoPlist": {
"GOOGLE_ANALYTICS_INAPURCHASE_COLLECTION_ENABLED": false,
"UIBackgroundModes": [ "UIBackgroundModes": [
"remote-notification" "remote-notification"
], ],
@@ -177,7 +178,7 @@
"backgroundColor": "#1a1a2e" "backgroundColor": "#1a1a2e"
}, },
"package": "com.sauci.app", "package": "com.sauci.app",
"versionCode": 37, "versionCode": 42,
"googleServicesFile": "./google-services.json", "googleServicesFile": "./google-services.json",
"permissions": [ "permissions": [
"RECEIVE_BOOT_COMPLETED", "RECEIVE_BOOT_COMPLETED",
@@ -227,14 +228,7 @@
"faceIDPermission": "Sauci uses Face ID to unlock the app and keep your conversations private." "faceIDPermission": "Sauci uses Face ID to unlock the app and keep your conversations private."
} }
], ],
[ "@react-native-firebase/crashlytics",
"@sentry/react-native/expo",
{
"organization": "molten-prints",
"project": "sauci"
}
],
"@sentry/react-native",
"expo-font", "expo-font",
"expo-asset", "expo-asset",
[ [
+4 -4
View File
@@ -1,9 +1,9 @@
// Polyfills must be imported FIRST before any other React Native imports // Polyfills must be imported FIRST before any other React Native imports
import "../src/polyfills/web"; import "../src/polyfills/web";
// Initialize Sentry early to capture all errors // Initialize Crashlytics early to capture all errors
import { initSentry, setUserContext, clearUserContext } from "../src/lib/sentry"; import { initCrashlytics, setUserContext, clearUserContext } from "../src/lib/crashlytics";
initSentry(); initCrashlytics();
// Analytics imports (initialization deferred until app mounts) // Analytics imports (initialization deferred until app mounts)
import { initAnalytics, setUserId, clearUserId, logScreenView, Events } from "../src/lib/analytics"; 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") { if (event === "SIGNED_IN" || event === "TOKEN_REFRESHED" || event === "USER_UPDATED") {
console.log('[Auth] Calling fetchUser...'); console.log('[Auth] Calling fetchUser...');
fetchUser(); fetchUser();
// Set Sentry user context for error tracking // Set Crashlytics user context for error tracking
if (session?.user) { if (session?.user) {
setUserContext(session.user.id, session.user.email); setUserContext(session.user.id, session.user.email);
// Set Analytics user ID // Set Analytics user ID
-1
View File
@@ -19,7 +19,6 @@
"env": { "env": {
"EXPO_PUBLIC_SUPABASE_URL": "https://ckjcrkjpmhqhiucifukx.supabase.co", "EXPO_PUBLIC_SUPABASE_URL": "https://ckjcrkjpmhqhiucifukx.supabase.co",
"EXPO_PUBLIC_SUPABASE_ANON_KEY": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6ImNramNya2pwbWhxaGl1Y2lmdWt4Iiwicm9sZSI6ImFub24iLCJpYXQiOjE3NjcxMDc1ODEsImV4cCI6MjA4MjY4MzU4MX0.7FA_RmFXUjlmZF96O9xlAUfxHrQrSIZcJclNReLL4lI", "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_IOS_API_KEY": "appl_lfPyYonFQtVytftPAfXIrGsMNFu",
"EXPO_PUBLIC_REVENUECAT_ANDROID_API_KEY": "goog_zoHtaRswsgvxtvYWIhZyvrPBGgh", "EXPO_PUBLIC_REVENUECAT_ANDROID_API_KEY": "goog_zoHtaRswsgvxtvYWIhZyvrPBGgh",
"EXPO_PUBLIC_REVENUECAT_ENTITLEMENT_ID": "Sauci Pro", "EXPO_PUBLIC_REVENUECAT_ENTITLEMENT_ID": "Sauci Pro",
+4 -1
View File
@@ -1,6 +1,9 @@
{ {
"react-native": { "react-native": {
"analytics_auto_collection_enabled": true, "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
} }
} }
+4 -2
View File
@@ -21,7 +21,7 @@
<key>CFBundlePackageType</key> <key>CFBundlePackageType</key>
<string>$(PRODUCT_BUNDLE_PACKAGE_TYPE)</string> <string>$(PRODUCT_BUNDLE_PACKAGE_TYPE)</string>
<key>CFBundleShortVersionString</key> <key>CFBundleShortVersionString</key>
<string>1.0.3</string> <string>1.0.4</string>
<key>CFBundleSignature</key> <key>CFBundleSignature</key>
<string>????</string> <string>????</string>
<key>CFBundleURLTypes</key> <key>CFBundleURLTypes</key>
@@ -47,7 +47,9 @@
</dict> </dict>
</array> </array>
<key>CFBundleVersion</key> <key>CFBundleVersion</key>
<string>33</string> <string>35</string>
<key>GOOGLE_ANALYTICS_INAPURCHASE_COLLECTION_ENABLED</key>
<false/>
<key>LSMinimumSystemVersion</key> <key>LSMinimumSystemVersion</key>
<string>12.0</string> <string>12.0</string>
<key>LSRequiresIPhoneOS</key> <key>LSRequiresIPhoneOS</key>
+1 -1
View File
@@ -24,11 +24,11 @@
"@react-native-async-storage/async-storage": "1.23.1", "@react-native-async-storage/async-storage": "1.23.1",
"@react-native-firebase/analytics": "^23.8.2", "@react-native-firebase/analytics": "^23.8.2",
"@react-native-firebase/app": "^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-native-google-signin/google-signin": "^16.1.1",
"@react-navigation/bottom-tabs": "^7.0.0", "@react-navigation/bottom-tabs": "^7.0.0",
"@react-navigation/native": "^7.0.0", "@react-navigation/native": "^7.0.0",
"@react-navigation/native-stack": "^7.0.0", "@react-navigation/native-stack": "^7.0.0",
"@sentry/react-native": "~6.10.0",
"@supabase/supabase-js": "^2.39.0", "@supabase/supabase-js": "^2.39.0",
"@tanstack/react-query": "^5.17.0", "@tanstack/react-query": "^5.17.0",
"babel-preset-expo": "~12.0.0", "babel-preset-expo": "~12.0.0",
+118 -118
View File
@@ -72,35 +72,35 @@ function getAnalyticsInstance(): FirebaseAnalyticsTypes.Module | null {
return null; return null;
} }
} }
export async function initAnalytics() { export async function initAnalytics() {
if (isInitialized) { if (isInitialized) {
return; return;
} }
// Skip initialization in non-RN environments (e.g., during tsc/Node.js) // Skip initialization in non-RN environments (e.g., during tsc/Node.js)
if (typeof window === "undefined" && typeof global !== "undefined" && !("__BUNDLE_START_TIME__" in global)) { if (typeof window === "undefined" && typeof global !== "undefined" && !("__BUNDLE_START_TIME__" in global)) {
return; return;
} }
try { try {
const analytics = getAnalyticsInstance(); const analytics = getAnalyticsInstance();
if (analytics) { if (analytics) {
await setAnalyticsCollectionEnabled(analytics, true); await setAnalyticsCollectionEnabled(analytics, true);
isInitialized = true; isInitialized = true;
console.log("[Analytics] Firebase Analytics initialized"); console.log("[Analytics] Firebase Analytics initialized");
} }
} catch (error) { } catch (error) {
console.warn("[Analytics] Failed to initialize Firebase Analytics:", error); console.warn("[Analytics] Failed to initialize Firebase Analytics:", error);
} }
} }
// Log a custom event // Log a custom event
export function logEvent( export function logEvent(
name: string, name: string,
params?: Record<string, string | number | boolean> params?: Record<string, string | number | boolean>
) { ) {
try { try {
if (__DEV__) console.log("[Analytics] Event:", name, params); if (__DEV__) console.log("[Analytics] Event:", name, params);
const analytics = getAnalyticsInstance(); const analytics = getAnalyticsInstance();
if (analytics) { if (analytics) {
@@ -109,11 +109,11 @@ export function logEvent(
} catch (error) { } catch (error) {
console.warn("[Analytics] Failed to log event:", error); console.warn("[Analytics] Failed to log event:", error);
} }
} }
// Set user ID for tracking // Set user ID for tracking
export function setUserId(userId: string) { export function setUserId(userId: string) {
try { try {
if (__DEV__) console.log("[Analytics] Set user ID:", userId); if (__DEV__) console.log("[Analytics] Set user ID:", userId);
const analytics = getAnalyticsInstance(); const analytics = getAnalyticsInstance();
if (analytics) { if (analytics) {
@@ -122,11 +122,11 @@ export function setUserId(userId: string) {
} catch (error) { } catch (error) {
console.warn("[Analytics] Failed to set user ID:", error); console.warn("[Analytics] Failed to set user ID:", error);
} }
} }
// Clear user ID on logout // Clear user ID on logout
export function clearUserId() { export function clearUserId() {
try { try {
if (__DEV__) console.log("[Analytics] Cleared user ID"); if (__DEV__) console.log("[Analytics] Cleared user ID");
const analytics = getAnalyticsInstance(); const analytics = getAnalyticsInstance();
if (analytics) { if (analytics) {
@@ -136,13 +136,13 @@ export function clearUserId() {
} catch (error) { } catch (error) {
console.warn("[Analytics] Failed to clear user ID:", error); console.warn("[Analytics] Failed to clear user ID:", error);
} }
} }
// Set user properties // Set user properties
export function setUserProperties( export function setUserProperties(
properties: Record<string, string | null> properties: Record<string, string | null>
) { ) {
try { try {
if (__DEV__) console.log("[Analytics] Set user properties:", properties); if (__DEV__) console.log("[Analytics] Set user properties:", properties);
const analytics = getAnalyticsInstance(); const analytics = getAnalyticsInstance();
if (analytics) { if (analytics) {
@@ -154,11 +154,11 @@ export function setUserProperties(
} catch (error) { } catch (error) {
console.warn("[Analytics] Failed to set user properties:", error); console.warn("[Analytics] Failed to set user properties:", error);
} }
} }
// Log screen view // Log screen view
export function logScreenView(screenName: string, screenClass?: string) { export function logScreenView(screenName: string, screenClass?: string) {
try { try {
if (__DEV__) console.log("[Analytics] Screen view:", screenName); if (__DEV__) console.log("[Analytics] Screen view:", screenName);
const analytics = getAnalyticsInstance(); const analytics = getAnalyticsInstance();
if (analytics) { if (analytics) {
@@ -166,12 +166,12 @@ export function logScreenView(screenName: string, screenClass?: string) {
screen_name: screenName, screen_name: screenName,
screen_class: screenClass ?? screenName, screen_class: screenClass ?? screenName,
}); });
} }
} catch (error) { } catch (error) {
console.warn("[Analytics] Failed to log screen view:", error); console.warn("[Analytics] Failed to log screen view:", error);
} }
} }
// Flush events (useful before app backgrounding) // Flush events (useful before app backgrounding)
export function flush() { export function flush() {
// Firebase Analytics batches and sends events automatically // 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 // Pre-defined event helpers for common actions
export const Events = { export const Events = {
// Auth events // Auth events
signUp: (method: string) => logEvent("sign_up", { method }), signUp: (method: string) => logEvent("sign_up", { method }),
signIn: (method: string) => logEvent("login", { method }), signIn: (method: string) => logEvent("login", { method }),
signOut: () => logEvent("sign_out"), signOut: () => logEvent("sign_out"),
// Onboarding events // Onboarding events
onboardingStart: () => logEvent("onboarding_start"), onboardingStart: () => logEvent("onboarding_start"),
onboardingStageComplete: (stage: string) => onboardingStageComplete: (stage: string) =>
logEvent("onboarding_stage_complete", { stage }), logEvent("onboarding_stage_complete", { stage }),
onboardingComplete: () => logEvent("onboarding_complete"), onboardingComplete: () => logEvent("onboarding_complete"),
// Pairing events // Pairing events
coupleCreated: () => logEvent("couple_created"), coupleCreated: () => logEvent("couple_created"),
coupleJoined: () => logEvent("couple_joined"), coupleJoined: () => logEvent("couple_joined"),
codeShared: () => logEvent("invite_code_shared"), codeShared: () => logEvent("invite_code_shared"),
pairingCancelled: () => logEvent("pairing_cancelled"), pairingCancelled: () => logEvent("pairing_cancelled"),
// Swipe/question events // Swipe/question events
questionAnswered: (answer: string, packId?: string) => questionAnswered: (answer: string, packId?: string) =>
logEvent("question_answered", { answer, pack_id: packId ?? "unknown" }), logEvent("question_answered", { answer, pack_id: packId ?? "unknown" }),
questionSkipped: () => logEvent("question_skipped"), questionSkipped: () => logEvent("question_skipped"),
matchCreated: (matchType: string) => logEvent("match_created", { match_type: matchType }), matchCreated: (matchType: string) => logEvent("match_created", { match_type: matchType }),
allQuestionsExhausted: () => logEvent("all_questions_exhausted"), allQuestionsExhausted: () => logEvent("all_questions_exhausted"),
// Chat events // Chat events
matchViewed: () => logEvent("match_viewed"), matchViewed: () => logEvent("match_viewed"),
messageSent: () => logEvent("message_sent"), messageSent: () => logEvent("message_sent"),
mediaUploaded: () => logEvent("chat_media_uploaded"), mediaUploaded: () => logEvent("chat_media_uploaded"),
// Subscription events // Subscription events
paywallShown: (source: string) => logEvent("paywall_shown", { source }), paywallShown: (source: string) => logEvent("paywall_shown", { source }),
paywallClosed: (source: string, reason: string) => paywallClosed: (source: string, reason: string) =>
@@ -240,42 +240,42 @@ export const Events = {
error_message: safeParam(errorMessage), error_message: safeParam(errorMessage),
}), }),
purchaseCompleted: (packageType: string) => purchaseCompleted: (packageType: string) =>
logEvent("purchase", { package_type: packageType }), logEvent("purchase_completed", { package_type: packageType }),
purchaseRestored: () => logEvent("purchase_restored"), purchaseRestored: () => logEvent("purchase_restored"),
// Pack events // Pack events
packEnabled: (packId: string) => logEvent("pack_enabled", { pack_id: packId }), packEnabled: (packId: string) => logEvent("pack_enabled", { pack_id: packId }),
packDisabled: (packId: string) => logEvent("pack_disabled", { pack_id: packId }), packDisabled: (packId: string) => logEvent("pack_disabled", { pack_id: packId }),
// App lifecycle events // App lifecycle events
appOpened: (type: "cold" | "warm") => logEvent("app_opened", { type }), appOpened: (type: "cold" | "warm") => logEvent("app_opened", { type }),
// Profile events // Profile events
profileUpdated: (fields: string[]) => logEvent("profile_updated", { fields: fields.join(",") }), profileUpdated: (fields: string[]) => logEvent("profile_updated", { fields: fields.join(",") }),
avatarUploaded: () => logEvent("avatar_uploaded"), avatarUploaded: () => logEvent("avatar_uploaded"),
// Notification events // Notification events
notificationPermissionGranted: () => logEvent("notification_permission_granted"), notificationPermissionGranted: () => logEvent("notification_permission_granted"),
notificationPermissionDenied: () => logEvent("notification_permission_denied"), notificationPermissionDenied: () => logEvent("notification_permission_denied"),
// Relationship events // Relationship events
relationshipEnded: () => logEvent("relationship_ended"), relationshipEnded: () => logEvent("relationship_ended"),
accountDeleted: () => logEvent("account_deleted"), accountDeleted: () => logEvent("account_deleted"),
// Milestone events // Milestone events
firstMatch: () => logEvent("first_match"), firstMatch: () => logEvent("first_match"),
milestoneMatch: (count: number) => logEvent("milestone_match", { match_count: count }), milestoneMatch: (count: number) => logEvent("milestone_match", { match_count: count }),
// Invite events // Invite events
inviteCodeCopied: () => logEvent("invite_code_copied"), inviteCodeCopied: () => logEvent("invite_code_copied"),
// Tutorial events // Tutorial events
tutorialStarted: (tutorial: "swipe" | "matches") => tutorialStarted: (tutorial: "swipe" | "matches") =>
logEvent("tutorial_started", { tutorial }), logEvent("tutorial_started", { tutorial }),
tutorialStepViewed: (tutorial: "swipe" | "matches", step: number, stepName: string) => tutorialStepViewed: (tutorial: "swipe" | "matches", step: number, stepName: string) =>
logEvent("tutorial_step_viewed", { tutorial, step, step_name: stepName }), logEvent("tutorial_step_viewed", { tutorial, step, step_name: stepName }),
tutorialCompleted: (tutorial: "swipe" | "matches") => tutorialCompleted: (tutorial: "swipe" | "matches") =>
logEvent("tutorial_completed", { tutorial }), logEvent("tutorial_completed", { tutorial }),
tutorialSkipped: (tutorial: "swipe" | "matches", atStep: number) => tutorialSkipped: (tutorial: "swipe" | "matches", atStep: number) =>
logEvent("tutorial_skipped", { tutorial, at_step: atStep }), logEvent("tutorial_skipped", { tutorial, at_step: atStep }),
}; };
+128
View File
@@ -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);
}
}
+1 -1
View File
@@ -3,7 +3,7 @@ import Constants from "expo-constants";
import { Platform } from "react-native"; import { Platform } from "react-native";
import { router } from "expo-router"; import { router } from "expo-router";
import { supabase } from "./supabase"; import { supabase } from "./supabase";
import { captureError } from "./sentry"; import { captureError } from "./crashlytics";
import { Events } from "./analytics"; import { Events } from "./analytics";
import { useMatchStore } from "../store/matchStore"; import { useMatchStore } from "../store/matchStore";
-72
View File
@@ -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 };
+1 -1
View File
@@ -58,7 +58,7 @@ if (!isTestEnv && (!supabaseUrl || !supabaseAnonKey)) {
if (!supabaseAnonKey) missing.push('EXPO_PUBLIC_SUPABASE_ANON_KEY'); if (!supabaseAnonKey) missing.push('EXPO_PUBLIC_SUPABASE_ANON_KEY');
const errorMsg = `Missing required environment variables: ${missing.join(', ')}. App cannot start.`; const errorMsg = `Missing required environment variables: ${missing.join(', ')}. App cannot start.`;
console.error('[Supabase]', errorMsg); 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); throw new Error(errorMsg);
} }
+235 -308
View File
@@ -79,11 +79,11 @@
"@react-native-async-storage/async-storage": "1.23.1", "@react-native-async-storage/async-storage": "1.23.1",
"@react-native-firebase/analytics": "^23.8.2", "@react-native-firebase/analytics": "^23.8.2",
"@react-native-firebase/app": "^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-native-google-signin/google-signin": "^16.1.1",
"@react-navigation/bottom-tabs": "^7.0.0", "@react-navigation/bottom-tabs": "^7.0.0",
"@react-navigation/native": "^7.0.0", "@react-navigation/native": "^7.0.0",
"@react-navigation/native-stack": "^7.0.0", "@react-navigation/native-stack": "^7.0.0",
"@sentry/react-native": "~6.10.0",
"@supabase/supabase-js": "^2.39.0", "@supabase/supabase-js": "^2.39.0",
"@tanstack/react-query": "^5.17.0", "@tanstack/react-query": "^5.17.0",
"babel-preset-expo": "~12.0.0", "babel-preset-expo": "~12.0.0",
@@ -142,6 +142,168 @@
"typescript": "^5.3.3" "typescript": "^5.3.3"
} }
}, },
"apps/mobile/node_modules/@firebase/ai": {
"version": "2.7.0",
"resolved": "https://registry.npmjs.org/@firebase/ai/-/ai-2.7.0.tgz",
"integrity": "sha512-PwpCz+TtAMWICM7uQNO0mkSPpUKwrMV4NSwHkbVKDvPKoaQmSlO96vIz+Suw2Ao1EaUUsxYb5LGImHWt/fSnRQ==",
"license": "Apache-2.0",
"dependencies": {
"@firebase/app-check-interop-types": "0.3.3",
"@firebase/component": "0.7.0",
"@firebase/logger": "0.5.0",
"@firebase/util": "1.13.0",
"tslib": "^2.1.0"
},
"engines": {
"node": ">=20.0.0"
},
"peerDependencies": {
"@firebase/app": "0.x",
"@firebase/app-types": "0.x"
}
},
"apps/mobile/node_modules/@firebase/app": {
"version": "0.14.7",
"resolved": "https://registry.npmjs.org/@firebase/app/-/app-0.14.7.tgz",
"integrity": "sha512-o3ZfnOx0AWBD5n/36p2zPoB0rDDxQP8H/A60zDLvvfRLtW8b3LfCyV97GKpJaAVV1JMMl/BC89EDzMyzxFZxTw==",
"license": "Apache-2.0",
"dependencies": {
"@firebase/component": "0.7.0",
"@firebase/logger": "0.5.0",
"@firebase/util": "1.13.0",
"idb": "7.1.1",
"tslib": "^2.1.0"
},
"engines": {
"node": ">=20.0.0"
}
},
"apps/mobile/node_modules/@firebase/app-compat": {
"version": "0.5.7",
"resolved": "https://registry.npmjs.org/@firebase/app-compat/-/app-compat-0.5.7.tgz",
"integrity": "sha512-MO+jfap8IBZQ+K8L2QCiHObyMgpYHrxo4Hc7iJgfb9hjGRW/z1y6LWVdT9wBBK+VJ7cRP2DjAiWQP+thu53hHA==",
"license": "Apache-2.0",
"dependencies": {
"@firebase/app": "0.14.7",
"@firebase/component": "0.7.0",
"@firebase/logger": "0.5.0",
"@firebase/util": "1.13.0",
"tslib": "^2.1.0"
},
"engines": {
"node": ">=20.0.0"
}
},
"apps/mobile/node_modules/@firebase/firestore": {
"version": "4.10.0",
"resolved": "https://registry.npmjs.org/@firebase/firestore/-/firestore-4.10.0.tgz",
"integrity": "sha512-fgF6EbpoagGWh5Vwfu/7/jYgBFwUCwTlPNVF/aSjHcoEDRXpRsIqVfAFTp1LD+dWAUcAKEK3h+osk8spMJXtxA==",
"license": "Apache-2.0",
"dependencies": {
"@firebase/component": "0.7.0",
"@firebase/logger": "0.5.0",
"@firebase/util": "1.13.0",
"@firebase/webchannel-wrapper": "1.0.5",
"@grpc/grpc-js": "~1.9.0",
"@grpc/proto-loader": "^0.7.8",
"tslib": "^2.1.0"
},
"engines": {
"node": ">=20.0.0"
},
"peerDependencies": {
"@firebase/app": "0.x"
}
},
"apps/mobile/node_modules/@firebase/firestore-compat": {
"version": "0.4.4",
"resolved": "https://registry.npmjs.org/@firebase/firestore-compat/-/firestore-compat-0.4.4.tgz",
"integrity": "sha512-JvxxIgi+D5v9BecjLA1YomdyF7LA6CXhJuVK10b4GtRrB3m2O2hT1jJWbKYZYHUAjTaajkvnos+4U5VNxqkI2w==",
"license": "Apache-2.0",
"dependencies": {
"@firebase/component": "0.7.0",
"@firebase/firestore": "4.10.0",
"@firebase/firestore-types": "3.0.3",
"@firebase/util": "1.13.0",
"tslib": "^2.1.0"
},
"engines": {
"node": ">=20.0.0"
},
"peerDependencies": {
"@firebase/app-compat": "0.x"
}
},
"apps/mobile/node_modules/@firebase/remote-config": {
"version": "0.8.0",
"resolved": "https://registry.npmjs.org/@firebase/remote-config/-/remote-config-0.8.0.tgz",
"integrity": "sha512-sJz7C2VACeE257Z/3kY9Ap2WXbFsgsDLfaGfZmmToKAK39ipXxFan+vzB9CSbF6mP7bzjyzEnqPcMXhAnYE6fQ==",
"license": "Apache-2.0",
"dependencies": {
"@firebase/component": "0.7.0",
"@firebase/installations": "0.6.19",
"@firebase/logger": "0.5.0",
"@firebase/util": "1.13.0",
"tslib": "^2.1.0"
},
"peerDependencies": {
"@firebase/app": "0.x"
}
},
"apps/mobile/node_modules/@firebase/remote-config-compat": {
"version": "0.2.21",
"resolved": "https://registry.npmjs.org/@firebase/remote-config-compat/-/remote-config-compat-0.2.21.tgz",
"integrity": "sha512-9+lm0eUycxbu8GO25JfJe4s6R2xlDqlVt0CR6CvN9E6B4AFArEV4qfLoDVRgIEB7nHKwvH2nYRocPWfmjRQTnw==",
"license": "Apache-2.0",
"dependencies": {
"@firebase/component": "0.7.0",
"@firebase/logger": "0.5.0",
"@firebase/remote-config": "0.8.0",
"@firebase/remote-config-types": "0.5.0",
"@firebase/util": "1.13.0",
"tslib": "^2.1.0"
},
"peerDependencies": {
"@firebase/app-compat": "0.x"
}
},
"apps/mobile/node_modules/@react-native-firebase/app": {
"version": "23.8.4",
"resolved": "https://registry.npmjs.org/@react-native-firebase/app/-/app-23.8.4.tgz",
"integrity": "sha512-xURXijgL5gH5O6X7aUXKS5TObDT/aqWpdJpyUNWvRtG2cUxDRcLaSfJnSJP0TREBFAP1Dntdms2kmt5l2oSYZg==",
"license": "Apache-2.0",
"dependencies": {
"firebase": "12.8.0"
},
"peerDependencies": {
"expo": ">=47.0.0",
"react": "*",
"react-native": "*"
},
"peerDependenciesMeta": {
"expo": {
"optional": true
}
}
},
"apps/mobile/node_modules/@react-native-firebase/crashlytics": {
"version": "23.8.4",
"resolved": "https://registry.npmjs.org/@react-native-firebase/crashlytics/-/crashlytics-23.8.4.tgz",
"integrity": "sha512-5zGzbXQ7u24TqwTCRIqo63Sw2C6VgPdgEJnR+Iwh5G2NSt339wcbsxyLRaM5cP25M7Obve0JrUUG9bTDI2qIOQ==",
"license": "Apache-2.0",
"dependencies": {
"stacktrace-js": "^2.0.2"
},
"peerDependencies": {
"@react-native-firebase/app": "23.8.4",
"expo": ">=47.0.0"
},
"peerDependenciesMeta": {
"expo": {
"optional": true
}
}
},
"apps/mobile/node_modules/expo-sharing": { "apps/mobile/node_modules/expo-sharing": {
"version": "13.0.1", "version": "13.0.1",
"resolved": "https://registry.npmjs.org/expo-sharing/-/expo-sharing-13.0.1.tgz", "resolved": "https://registry.npmjs.org/expo-sharing/-/expo-sharing-13.0.1.tgz",
@@ -151,6 +313,66 @@
"expo": "*" "expo": "*"
} }
}, },
"apps/mobile/node_modules/firebase": {
"version": "12.8.0",
"resolved": "https://registry.npmjs.org/firebase/-/firebase-12.8.0.tgz",
"integrity": "sha512-S1tCIR3ENecee0tY2cfTHfMkXqkitHfbsvqpCtvsT0Zi9vDB7A4CodAjHfHCjVvu/XtGy1LHLjOasVcF10rCVw==",
"license": "Apache-2.0",
"dependencies": {
"@firebase/ai": "2.7.0",
"@firebase/analytics": "0.10.19",
"@firebase/analytics-compat": "0.2.25",
"@firebase/app": "0.14.7",
"@firebase/app-check": "0.11.0",
"@firebase/app-check-compat": "0.4.0",
"@firebase/app-compat": "0.5.7",
"@firebase/app-types": "0.9.3",
"@firebase/auth": "1.12.0",
"@firebase/auth-compat": "0.6.2",
"@firebase/data-connect": "0.3.12",
"@firebase/database": "1.1.0",
"@firebase/database-compat": "2.1.0",
"@firebase/firestore": "4.10.0",
"@firebase/firestore-compat": "0.4.4",
"@firebase/functions": "0.13.1",
"@firebase/functions-compat": "0.4.1",
"@firebase/installations": "0.6.19",
"@firebase/installations-compat": "0.2.19",
"@firebase/messaging": "0.12.23",
"@firebase/messaging-compat": "0.2.23",
"@firebase/performance": "0.7.9",
"@firebase/performance-compat": "0.2.22",
"@firebase/remote-config": "0.8.0",
"@firebase/remote-config-compat": "0.2.21",
"@firebase/storage": "0.14.0",
"@firebase/storage-compat": "0.4.0",
"@firebase/util": "1.13.0"
}
},
"apps/mobile/node_modules/firebase/node_modules/@firebase/auth": {
"version": "1.12.0",
"resolved": "https://registry.npmjs.org/@firebase/auth/-/auth-1.12.0.tgz",
"integrity": "sha512-zkvLpsrxynWHk07qGrUDfCSqKf4AvfZGEqJ7mVCtYGjNNDbGE71k0Yn84rg8QEZu4hQw1BC0qDEHzpNVBcSVmA==",
"license": "Apache-2.0",
"dependencies": {
"@firebase/component": "0.7.0",
"@firebase/logger": "0.5.0",
"@firebase/util": "1.13.0",
"tslib": "^2.1.0"
},
"engines": {
"node": ">=20.0.0"
},
"peerDependencies": {
"@firebase/app": "0.x",
"@react-native-async-storage/async-storage": "^2.2.0"
},
"peerDependenciesMeta": {
"@react-native-async-storage/async-storage": {
"optional": true
}
}
},
"apps/mobile/node_modules/react-native-svg": { "apps/mobile/node_modules/react-native-svg": {
"version": "15.8.0", "version": "15.8.0",
"resolved": "https://registry.npmjs.org/react-native-svg/-/react-native-svg-15.8.0.tgz", "resolved": "https://registry.npmjs.org/react-native-svg/-/react-native-svg-15.8.0.tgz",
@@ -4194,6 +4416,7 @@
"resolved": "https://registry.npmjs.org/@firebase/ai/-/ai-2.6.1.tgz", "resolved": "https://registry.npmjs.org/@firebase/ai/-/ai-2.6.1.tgz",
"integrity": "sha512-qJd9bpABqsanFnwdbjZEDbKKr1jRtuUZ+cHyNBLWsxobH4pd73QncvuO3XlMq4eKBLlg1f5jNdFpJ3G3ABu2Tg==", "integrity": "sha512-qJd9bpABqsanFnwdbjZEDbKKr1jRtuUZ+cHyNBLWsxobH4pd73QncvuO3XlMq4eKBLlg1f5jNdFpJ3G3ABu2Tg==",
"license": "Apache-2.0", "license": "Apache-2.0",
"peer": true,
"dependencies": { "dependencies": {
"@firebase/app-check-interop-types": "0.3.3", "@firebase/app-check-interop-types": "0.3.3",
"@firebase/component": "0.7.0", "@firebase/component": "0.7.0",
@@ -4252,6 +4475,7 @@
"resolved": "https://registry.npmjs.org/@firebase/app/-/app-0.14.6.tgz", "resolved": "https://registry.npmjs.org/@firebase/app/-/app-0.14.6.tgz",
"integrity": "sha512-4uyt8BOrBsSq6i4yiOV/gG6BnnrvTeyymlNcaN/dKvyU1GoolxAafvIvaNP1RCGPlNab3OuE4MKUQuv2lH+PLQ==", "integrity": "sha512-4uyt8BOrBsSq6i4yiOV/gG6BnnrvTeyymlNcaN/dKvyU1GoolxAafvIvaNP1RCGPlNab3OuE4MKUQuv2lH+PLQ==",
"license": "Apache-2.0", "license": "Apache-2.0",
"peer": true,
"dependencies": { "dependencies": {
"@firebase/component": "0.7.0", "@firebase/component": "0.7.0",
"@firebase/logger": "0.5.0", "@firebase/logger": "0.5.0",
@@ -4318,6 +4542,7 @@
"resolved": "https://registry.npmjs.org/@firebase/app-compat/-/app-compat-0.5.6.tgz", "resolved": "https://registry.npmjs.org/@firebase/app-compat/-/app-compat-0.5.6.tgz",
"integrity": "sha512-YYGARbutghQY4zZUWMYia0ib0Y/rb52y72/N0z3vglRHL7ii/AaK9SA7S/dzScVOlCdnbHXz+sc5Dq+r8fwFAg==", "integrity": "sha512-YYGARbutghQY4zZUWMYia0ib0Y/rb52y72/N0z3vglRHL7ii/AaK9SA7S/dzScVOlCdnbHXz+sc5Dq+r8fwFAg==",
"license": "Apache-2.0", "license": "Apache-2.0",
"peer": true,
"dependencies": { "dependencies": {
"@firebase/app": "0.14.6", "@firebase/app": "0.14.6",
"@firebase/component": "0.7.0", "@firebase/component": "0.7.0",
@@ -4473,6 +4698,7 @@
"resolved": "https://registry.npmjs.org/@firebase/firestore/-/firestore-4.9.3.tgz", "resolved": "https://registry.npmjs.org/@firebase/firestore/-/firestore-4.9.3.tgz",
"integrity": "sha512-RVuvhcQzs1sD5Osr2naQS71H0bQMbSnib16uOWAKk3GaKb/WBPyCYSr2Ry7MqlxDP/YhwknUxECL07lw9Rq1nA==", "integrity": "sha512-RVuvhcQzs1sD5Osr2naQS71H0bQMbSnib16uOWAKk3GaKb/WBPyCYSr2Ry7MqlxDP/YhwknUxECL07lw9Rq1nA==",
"license": "Apache-2.0", "license": "Apache-2.0",
"peer": true,
"dependencies": { "dependencies": {
"@firebase/component": "0.7.0", "@firebase/component": "0.7.0",
"@firebase/logger": "0.5.0", "@firebase/logger": "0.5.0",
@@ -4494,6 +4720,7 @@
"resolved": "https://registry.npmjs.org/@firebase/firestore-compat/-/firestore-compat-0.4.3.tgz", "resolved": "https://registry.npmjs.org/@firebase/firestore-compat/-/firestore-compat-0.4.3.tgz",
"integrity": "sha512-1ylF/njF68Pmb6p0erP0U78XQv1w77Wap4bUmqZ7ZVkmN1oMgplyu0TyirWtCBoKFRV2+SUZfWXvIij/z39LYg==", "integrity": "sha512-1ylF/njF68Pmb6p0erP0U78XQv1w77Wap4bUmqZ7ZVkmN1oMgplyu0TyirWtCBoKFRV2+SUZfWXvIij/z39LYg==",
"license": "Apache-2.0", "license": "Apache-2.0",
"peer": true,
"dependencies": { "dependencies": {
"@firebase/component": "0.7.0", "@firebase/component": "0.7.0",
"@firebase/firestore": "4.9.3", "@firebase/firestore": "4.9.3",
@@ -4698,6 +4925,7 @@
"resolved": "https://registry.npmjs.org/@firebase/remote-config/-/remote-config-0.7.0.tgz", "resolved": "https://registry.npmjs.org/@firebase/remote-config/-/remote-config-0.7.0.tgz",
"integrity": "sha512-dX95X6WlW7QlgNd7aaGdjAIZUiQkgWgNS+aKNu4Wv92H1T8Ue/NDUjZHd9xb8fHxLXIHNZeco9/qbZzr500MjQ==", "integrity": "sha512-dX95X6WlW7QlgNd7aaGdjAIZUiQkgWgNS+aKNu4Wv92H1T8Ue/NDUjZHd9xb8fHxLXIHNZeco9/qbZzr500MjQ==",
"license": "Apache-2.0", "license": "Apache-2.0",
"peer": true,
"dependencies": { "dependencies": {
"@firebase/component": "0.7.0", "@firebase/component": "0.7.0",
"@firebase/installations": "0.6.19", "@firebase/installations": "0.6.19",
@@ -4714,6 +4942,7 @@
"resolved": "https://registry.npmjs.org/@firebase/remote-config-compat/-/remote-config-compat-0.2.20.tgz", "resolved": "https://registry.npmjs.org/@firebase/remote-config-compat/-/remote-config-compat-0.2.20.tgz",
"integrity": "sha512-P/ULS9vU35EL9maG7xp66uljkZgcPMQOxLj3Zx2F289baTKSInE6+YIkgHEi1TwHoddC/AFePXPpshPlEFkbgg==", "integrity": "sha512-P/ULS9vU35EL9maG7xp66uljkZgcPMQOxLj3Zx2F289baTKSInE6+YIkgHEi1TwHoddC/AFePXPpshPlEFkbgg==",
"license": "Apache-2.0", "license": "Apache-2.0",
"peer": true,
"dependencies": { "dependencies": {
"@firebase/component": "0.7.0", "@firebase/component": "0.7.0",
"@firebase/logger": "0.5.0", "@firebase/logger": "0.5.0",
@@ -7759,6 +7988,7 @@
"resolved": "https://registry.npmjs.org/@react-native-firebase/app/-/app-23.8.3.tgz", "resolved": "https://registry.npmjs.org/@react-native-firebase/app/-/app-23.8.3.tgz",
"integrity": "sha512-woH8Y+9MInWK3SG2Y+V4bxi4406AuDDoUaB7x2WmEueUiJxVvlAvPJ/uGLPrqmUFyk0tiv2DXPlcEfBzxXUlZQ==", "integrity": "sha512-woH8Y+9MInWK3SG2Y+V4bxi4406AuDDoUaB7x2WmEueUiJxVvlAvPJ/uGLPrqmUFyk0tiv2DXPlcEfBzxXUlZQ==",
"license": "Apache-2.0", "license": "Apache-2.0",
"peer": true,
"dependencies": { "dependencies": {
"firebase": "12.7.0" "firebase": "12.7.0"
}, },
@@ -8662,303 +8892,6 @@
"join-component": "^1.1.0" "join-component": "^1.1.0"
} }
}, },
"node_modules/@sentry-internal/browser-utils": {
"version": "8.54.0",
"resolved": "https://registry.npmjs.org/@sentry-internal/browser-utils/-/browser-utils-8.54.0.tgz",
"integrity": "sha512-DKWCqb4YQosKn6aD45fhKyzhkdG7N6goGFDeyTaJFREJDFVDXiNDsYZu30nJ6BxMM7uQIaARhPAC5BXfoED3pQ==",
"license": "MIT",
"dependencies": {
"@sentry/core": "8.54.0"
},
"engines": {
"node": ">=14.18"
}
},
"node_modules/@sentry-internal/feedback": {
"version": "8.54.0",
"resolved": "https://registry.npmjs.org/@sentry-internal/feedback/-/feedback-8.54.0.tgz",
"integrity": "sha512-nQqRacOXoElpE0L0ADxUUII0I3A94niqG9Z4Fmsw6057QvyrV/LvTiMQBop6r5qLjwMqK+T33iR4/NQI5RhsXQ==",
"license": "MIT",
"dependencies": {
"@sentry/core": "8.54.0"
},
"engines": {
"node": ">=14.18"
}
},
"node_modules/@sentry-internal/replay": {
"version": "8.54.0",
"resolved": "https://registry.npmjs.org/@sentry-internal/replay/-/replay-8.54.0.tgz",
"integrity": "sha512-8xuBe06IaYIGJec53wUC12tY2q4z2Z0RPS2s1sLtbA00EvK1YDGuXp96IDD+HB9mnDMrQ/jW5f97g9TvPsPQUg==",
"license": "MIT",
"dependencies": {
"@sentry-internal/browser-utils": "8.54.0",
"@sentry/core": "8.54.0"
},
"engines": {
"node": ">=14.18"
}
},
"node_modules/@sentry-internal/replay-canvas": {
"version": "8.54.0",
"resolved": "https://registry.npmjs.org/@sentry-internal/replay-canvas/-/replay-canvas-8.54.0.tgz",
"integrity": "sha512-K/On3OAUBeq/TV2n+1EvObKC+WMV9npVXpVyJqCCyn8HYMm8FUGzuxeajzm0mlW4wDTPCQor6mK9/IgOquUzCw==",
"license": "MIT",
"dependencies": {
"@sentry-internal/replay": "8.54.0",
"@sentry/core": "8.54.0"
},
"engines": {
"node": ">=14.18"
}
},
"node_modules/@sentry/babel-plugin-component-annotate": {
"version": "3.2.2",
"resolved": "https://registry.npmjs.org/@sentry/babel-plugin-component-annotate/-/babel-plugin-component-annotate-3.2.2.tgz",
"integrity": "sha512-D+SKQ266ra/wo87s9+UI/rKQi3qhGPCR8eSCDe0VJudhjHsqyNU+JJ5lnIGCgmZaWFTXgdBP/gdr1Iz1zqGs4Q==",
"license": "MIT",
"engines": {
"node": ">= 14"
}
},
"node_modules/@sentry/browser": {
"version": "8.54.0",
"resolved": "https://registry.npmjs.org/@sentry/browser/-/browser-8.54.0.tgz",
"integrity": "sha512-BgUtvxFHin0fS0CmJVKTLXXZcke0Av729IVfi+2fJ4COX8HO7/HAP02RKaSQGmL2HmvWYTfNZ7529AnUtrM4Rg==",
"license": "MIT",
"dependencies": {
"@sentry-internal/browser-utils": "8.54.0",
"@sentry-internal/feedback": "8.54.0",
"@sentry-internal/replay": "8.54.0",
"@sentry-internal/replay-canvas": "8.54.0",
"@sentry/core": "8.54.0"
},
"engines": {
"node": ">=14.18"
}
},
"node_modules/@sentry/cli": {
"version": "2.42.4",
"resolved": "https://registry.npmjs.org/@sentry/cli/-/cli-2.42.4.tgz",
"integrity": "sha512-BoSZDAWJiz/40tu6LuMDkSgwk4xTsq6zwqYoUqLU3vKBR/VsaaQGvu6EWxZXORthfZU2/5Agz0+t220cge6VQw==",
"hasInstallScript": true,
"license": "BSD-3-Clause",
"dependencies": {
"https-proxy-agent": "^5.0.0",
"node-fetch": "^2.6.7",
"progress": "^2.0.3",
"proxy-from-env": "^1.1.0",
"which": "^2.0.2"
},
"bin": {
"sentry-cli": "bin/sentry-cli"
},
"engines": {
"node": ">= 10"
},
"optionalDependencies": {
"@sentry/cli-darwin": "2.42.4",
"@sentry/cli-linux-arm": "2.42.4",
"@sentry/cli-linux-arm64": "2.42.4",
"@sentry/cli-linux-i686": "2.42.4",
"@sentry/cli-linux-x64": "2.42.4",
"@sentry/cli-win32-i686": "2.42.4",
"@sentry/cli-win32-x64": "2.42.4"
}
},
"node_modules/@sentry/cli-darwin": {
"version": "2.42.4",
"resolved": "https://registry.npmjs.org/@sentry/cli-darwin/-/cli-darwin-2.42.4.tgz",
"integrity": "sha512-PZV4Y97VDWBR4rIt0HkJfXaBXlebIN2s/FDzC3iHINZE5OG62CDFsnC4/lbGlf2/UZLDaGGIK7mYwSHhTvN+HQ==",
"license": "BSD-3-Clause",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": ">=10"
}
},
"node_modules/@sentry/cli-linux-arm": {
"version": "2.42.4",
"resolved": "https://registry.npmjs.org/@sentry/cli-linux-arm/-/cli-linux-arm-2.42.4.tgz",
"integrity": "sha512-lBn0oeeg62h68/4Eo6zbPq99Idz5t0VRV48rEU/WKeM4MtQCvG/iGGQ3lBFW2yNiUBzXZIK9poXLEcgbwmcRVw==",
"cpu": [
"arm"
],
"license": "BSD-3-Clause",
"optional": true,
"os": [
"linux",
"freebsd"
],
"engines": {
"node": ">=10"
}
},
"node_modules/@sentry/cli-linux-arm64": {
"version": "2.42.4",
"resolved": "https://registry.npmjs.org/@sentry/cli-linux-arm64/-/cli-linux-arm64-2.42.4.tgz",
"integrity": "sha512-Ex8vRnryyzC/9e43daEmEqPS+9uirY/l6Hw2lAvhBblFaL7PTWNx52H+8GnYGd9Zy2H3rWNyBDYfHwnErg38zA==",
"cpu": [
"arm64"
],
"license": "BSD-3-Clause",
"optional": true,
"os": [
"linux",
"freebsd"
],
"engines": {
"node": ">=10"
}
},
"node_modules/@sentry/cli-linux-i686": {
"version": "2.42.4",
"resolved": "https://registry.npmjs.org/@sentry/cli-linux-i686/-/cli-linux-i686-2.42.4.tgz",
"integrity": "sha512-IBJg0aHjsLCL4LvcFa3cXIjA+4t5kPqBT9y+PoDu4goIFxYD8zl7mbUdGJutvJafTk8Akf4ss4JJXQBjg019zA==",
"cpu": [
"x86",
"ia32"
],
"license": "BSD-3-Clause",
"optional": true,
"os": [
"linux",
"freebsd"
],
"engines": {
"node": ">=10"
}
},
"node_modules/@sentry/cli-linux-x64": {
"version": "2.42.4",
"resolved": "https://registry.npmjs.org/@sentry/cli-linux-x64/-/cli-linux-x64-2.42.4.tgz",
"integrity": "sha512-gXI5OEiOSNiAEz7VCE6AZcAgHJ47mlgal3+NmbE8XcHmFOnyDws9FNie6PJAy8KZjXi3nqoBP9JVAbnmOix3uA==",
"cpu": [
"x64"
],
"license": "BSD-3-Clause",
"optional": true,
"os": [
"linux",
"freebsd"
],
"engines": {
"node": ">=10"
}
},
"node_modules/@sentry/cli-win32-i686": {
"version": "2.42.4",
"resolved": "https://registry.npmjs.org/@sentry/cli-win32-i686/-/cli-win32-i686-2.42.4.tgz",
"integrity": "sha512-vZuR3UPHKqOMniyrijrrsNwn9usaRysXq78F6WV0cL0ZyPLAmY+KBnTDSFk1Oig2pURnzaTm+RtcZu2fc8mlzg==",
"cpu": [
"x86",
"ia32"
],
"license": "BSD-3-Clause",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">=10"
}
},
"node_modules/@sentry/cli-win32-x64": {
"version": "2.42.4",
"resolved": "https://registry.npmjs.org/@sentry/cli-win32-x64/-/cli-win32-x64-2.42.4.tgz",
"integrity": "sha512-OIBj3uaQ6nAERSm5Dcf8UIhyElEEwMNsZEEppQpN4IKl0mrwb/57AznM23Dvpu6GR8WGbVQUSolt879YZR5E9g==",
"cpu": [
"x64"
],
"license": "BSD-3-Clause",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">=10"
}
},
"node_modules/@sentry/core": {
"version": "8.54.0",
"resolved": "https://registry.npmjs.org/@sentry/core/-/core-8.54.0.tgz",
"integrity": "sha512-03bWf+D1j28unOocY/5FDB6bUHtYlm6m6ollVejhg45ZmK9iPjdtxNWbrLsjT1WRym0Tjzowu+A3p+eebYEv0Q==",
"license": "MIT",
"engines": {
"node": ">=14.18"
}
},
"node_modules/@sentry/react": {
"version": "8.54.0",
"resolved": "https://registry.npmjs.org/@sentry/react/-/react-8.54.0.tgz",
"integrity": "sha512-42T/fp8snYN19Fy/2P0Mwotu4gcdy+1Lx+uYCNcYP1o7wNGigJ7qb27sW7W34GyCCHjoCCfQgeOqDQsyY8LC9w==",
"license": "MIT",
"dependencies": {
"@sentry/browser": "8.54.0",
"@sentry/core": "8.54.0",
"hoist-non-react-statics": "^3.3.2"
},
"engines": {
"node": ">=14.18"
},
"peerDependencies": {
"react": "^16.14.0 || 17.x || 18.x || 19.x"
}
},
"node_modules/@sentry/react-native": {
"version": "6.10.0",
"resolved": "https://registry.npmjs.org/@sentry/react-native/-/react-native-6.10.0.tgz",
"integrity": "sha512-B56vc+pnFHMiu3cabFb454v4qD0zObW6JVzJ5Gb6fIMdt93AFIJg10ZErzC+ump7xM4BOEROFFRuLiyvadvlPA==",
"license": "MIT",
"dependencies": {
"@sentry/babel-plugin-component-annotate": "3.2.2",
"@sentry/browser": "8.54.0",
"@sentry/cli": "2.42.4",
"@sentry/core": "8.54.0",
"@sentry/react": "8.54.0",
"@sentry/types": "8.54.0",
"@sentry/utils": "8.54.0"
},
"bin": {
"sentry-expo-upload-sourcemaps": "scripts/expo-upload-sourcemaps.js"
},
"peerDependencies": {
"expo": ">=49.0.0",
"react": ">=17.0.0",
"react-native": ">=0.65.0"
},
"peerDependenciesMeta": {
"expo": {
"optional": true
}
}
},
"node_modules/@sentry/types": {
"version": "8.54.0",
"resolved": "https://registry.npmjs.org/@sentry/types/-/types-8.54.0.tgz",
"integrity": "sha512-wztdtr7dOXQKi0iRvKc8XJhJ7HaAfOv8lGu0yqFOFwBZucO/SHnu87GOPi8mvrTiy1bentQO5l+zXWAaMvG4uw==",
"license": "MIT",
"dependencies": {
"@sentry/core": "8.54.0"
},
"engines": {
"node": ">=14.18"
}
},
"node_modules/@sentry/utils": {
"version": "8.54.0",
"resolved": "https://registry.npmjs.org/@sentry/utils/-/utils-8.54.0.tgz",
"integrity": "sha512-JL8UDjrsKxKclTdLXfuHfE7B3KbrAPEYP7tMyN/xiO2vsF6D84fjwYyalO0ZMtuFZE6vpSze8ZOLEh6hLnPYsw==",
"license": "MIT",
"dependencies": {
"@sentry/core": "8.54.0"
},
"engines": {
"node": ">=14.18"
}
},
"node_modules/@sinclair/typebox": { "node_modules/@sinclair/typebox": {
"version": "0.27.8", "version": "0.27.8",
"resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.8.tgz", "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.8.tgz",
@@ -10626,6 +10559,7 @@
"version": "6.0.2", "version": "6.0.2",
"resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz",
"integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==",
"dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"debug": "4" "debug": "4"
@@ -15477,6 +15411,7 @@
"resolved": "https://registry.npmjs.org/firebase/-/firebase-12.7.0.tgz", "resolved": "https://registry.npmjs.org/firebase/-/firebase-12.7.0.tgz",
"integrity": "sha512-ZBZg9jFo8uH4Emd7caOqtalKJfDGHnHQSrCPiqRAdTFQd0wL3ERilUBfhnhBLnlernugkN/o7nJa0p+sE71Izg==", "integrity": "sha512-ZBZg9jFo8uH4Emd7caOqtalKJfDGHnHQSrCPiqRAdTFQd0wL3ERilUBfhnhBLnlernugkN/o7nJa0p+sE71Izg==",
"license": "Apache-2.0", "license": "Apache-2.0",
"peer": true,
"dependencies": { "dependencies": {
"@firebase/ai": "2.6.1", "@firebase/ai": "2.6.1",
"@firebase/analytics": "0.10.19", "@firebase/analytics": "0.10.19",
@@ -15513,6 +15448,7 @@
"resolved": "https://registry.npmjs.org/@firebase/auth/-/auth-1.12.0.tgz", "resolved": "https://registry.npmjs.org/@firebase/auth/-/auth-1.12.0.tgz",
"integrity": "sha512-zkvLpsrxynWHk07qGrUDfCSqKf4AvfZGEqJ7mVCtYGjNNDbGE71k0Yn84rg8QEZu4hQw1BC0qDEHzpNVBcSVmA==", "integrity": "sha512-zkvLpsrxynWHk07qGrUDfCSqKf4AvfZGEqJ7mVCtYGjNNDbGE71k0Yn84rg8QEZu4hQw1BC0qDEHzpNVBcSVmA==",
"license": "Apache-2.0", "license": "Apache-2.0",
"peer": true,
"dependencies": { "dependencies": {
"@firebase/component": "0.7.0", "@firebase/component": "0.7.0",
"@firebase/logger": "0.5.0", "@firebase/logger": "0.5.0",
@@ -16317,6 +16253,7 @@
"version": "5.0.1", "version": "5.0.1",
"resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz",
"integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==",
"dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"agent-base": "6", "agent-base": "6",
@@ -21929,12 +21866,6 @@
"node": ">=12.0.0" "node": ">=12.0.0"
} }
}, },
"node_modules/proxy-from-env": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
"integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==",
"license": "MIT"
},
"node_modules/psl": { "node_modules/psl": {
"version": "1.15.0", "version": "1.15.0",
"resolved": "https://registry.npmjs.org/psl/-/psl-1.15.0.tgz", "resolved": "https://registry.npmjs.org/psl/-/psl-1.15.0.tgz",
@@ -23792,7 +23723,6 @@
"version": "2.0.10", "version": "2.0.10",
"resolved": "https://registry.npmjs.org/stack-generator/-/stack-generator-2.0.10.tgz", "resolved": "https://registry.npmjs.org/stack-generator/-/stack-generator-2.0.10.tgz",
"integrity": "sha512-mwnua/hkqM6pF4k8SnmZ2zfETsRUpWXREfA/goT8SLCV4iOFa4bzOX2nDipWAZFPTjLvQB82f5yaodMVhK0yJQ==", "integrity": "sha512-mwnua/hkqM6pF4k8SnmZ2zfETsRUpWXREfA/goT8SLCV4iOFa4bzOX2nDipWAZFPTjLvQB82f5yaodMVhK0yJQ==",
"dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"stackframe": "^1.3.4" "stackframe": "^1.3.4"
@@ -23829,7 +23759,6 @@
"version": "3.1.2", "version": "3.1.2",
"resolved": "https://registry.npmjs.org/stacktrace-gps/-/stacktrace-gps-3.1.2.tgz", "resolved": "https://registry.npmjs.org/stacktrace-gps/-/stacktrace-gps-3.1.2.tgz",
"integrity": "sha512-GcUgbO4Jsqqg6RxfyTHFiPxdPqF+3LFmQhm7MgCuYQOYuWyqxo5pwRPz5d/u6/WYJdEnWfK4r+jGbyD8TSggXQ==", "integrity": "sha512-GcUgbO4Jsqqg6RxfyTHFiPxdPqF+3LFmQhm7MgCuYQOYuWyqxo5pwRPz5d/u6/WYJdEnWfK4r+jGbyD8TSggXQ==",
"dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"source-map": "0.5.6", "source-map": "0.5.6",
@@ -23840,7 +23769,6 @@
"version": "0.5.6", "version": "0.5.6",
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.6.tgz", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.6.tgz",
"integrity": "sha512-MjZkVp0NHr5+TPihLcadqnlVoGIoWo4IBHptutGh9wI3ttUYvCG26HkSuDi+K6lsZ25syXJXcctwgyVCt//xqA==", "integrity": "sha512-MjZkVp0NHr5+TPihLcadqnlVoGIoWo4IBHptutGh9wI3ttUYvCG26HkSuDi+K6lsZ25syXJXcctwgyVCt//xqA==",
"dev": true,
"license": "BSD-3-Clause", "license": "BSD-3-Clause",
"engines": { "engines": {
"node": ">=0.10.0" "node": ">=0.10.0"
@@ -23850,7 +23778,6 @@
"version": "2.0.2", "version": "2.0.2",
"resolved": "https://registry.npmjs.org/stacktrace-js/-/stacktrace-js-2.0.2.tgz", "resolved": "https://registry.npmjs.org/stacktrace-js/-/stacktrace-js-2.0.2.tgz",
"integrity": "sha512-Je5vBeY4S1r/RnLydLl0TBTi3F2qdfWmYsGvtfZgEI+SCprPppaIhQf5nGcal4gI4cGpCV/duLcAzT1np6sQqg==", "integrity": "sha512-Je5vBeY4S1r/RnLydLl0TBTi3F2qdfWmYsGvtfZgEI+SCprPppaIhQf5nGcal4gI4cGpCV/duLcAzT1np6sQqg==",
"dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"error-stack-parser": "^2.0.6", "error-stack-parser": "^2.0.6",
@@ -0,0 +1,142 @@
-- Helper function: sends a Discord notification via the send-discord-notification edge function
CREATE OR REPLACE FUNCTION public.notify_discord_event(p_event text, p_payload jsonb)
RETURNS void
LANGUAGE plpgsql
SECURITY DEFINER
AS $$
BEGIN
PERFORM net.http_post(
url := 'https://ckjcrkjpmhqhiucifukx.supabase.co/functions/v1/send-discord-notification',
headers := jsonb_build_object('Content-Type', 'application/json'),
body := jsonb_build_object(
'event', p_event,
'payload', p_payload
)
);
END;
$$;
-- Trigger function: new user signup
CREATE OR REPLACE FUNCTION public.notify_new_user_discord()
RETURNS trigger
LANGUAGE plpgsql
SECURITY DEFINER
AS $$
BEGIN
PERFORM public.notify_discord_event(
'new_user',
jsonb_build_object(
'user_id', NEW.id,
'name', NEW.name,
'email', NEW.email,
'created_at', NEW.created_at
)
);
RETURN NEW;
END;
$$;
-- Trigger function: couple paired (fires when second partner joins)
CREATE OR REPLACE FUNCTION public.notify_couple_paired_discord()
RETURNS trigger
LANGUAGE plpgsql
SECURITY DEFINER
AS $$
DECLARE
v_member_count integer;
v_profiles jsonb;
BEGIN
IF NEW.couple_id IS NULL OR OLD.couple_id IS NOT DISTINCT FROM NEW.couple_id THEN
RETURN NEW;
END IF;
SELECT COUNT(*)
INTO v_member_count
FROM public.profiles
WHERE couple_id = NEW.couple_id;
IF v_member_count = 2 THEN
SELECT jsonb_agg(
jsonb_build_object(
'id', p.id,
'name', p.name,
'email', p.email
)
ORDER BY p.created_at
)
INTO v_profiles
FROM public.profiles p
WHERE p.couple_id = NEW.couple_id;
PERFORM public.notify_discord_event(
'couple_paired',
jsonb_build_object(
'couple_id', NEW.couple_id,
'profiles', COALESCE(v_profiles, '[]'::jsonb),
'paired_at', now()
)
);
END IF;
RETURN NEW;
END;
$$;
-- Trigger function: feedback submitted
CREATE OR REPLACE FUNCTION public.notify_feedback_submitted_discord()
RETURNS trigger
LANGUAGE plpgsql
SECURITY DEFINER
AS $$
DECLARE
v_user jsonb;
BEGIN
SELECT jsonb_build_object(
'id', p.id,
'name', p.name,
'email', p.email
)
INTO v_user
FROM public.profiles p
WHERE p.id = NEW.user_id;
PERFORM public.notify_discord_event(
'feedback_submitted',
jsonb_build_object(
'feedback_id', NEW.id,
'user_id', NEW.user_id,
'type', NEW.type,
'title', NEW.title,
'description', NEW.description,
'status', NEW.status,
'screenshot_url', NEW.screenshot_url,
'question_id', NEW.question_id,
'created_at', NEW.created_at,
'user', v_user
)
);
RETURN NEW;
END;
$$;
-- Create triggers (drop first to be idempotent)
DROP TRIGGER IF EXISTS on_profile_created_discord ON public.profiles;
CREATE TRIGGER on_profile_created_discord
AFTER INSERT ON public.profiles
FOR EACH ROW
EXECUTE FUNCTION public.notify_new_user_discord();
DROP TRIGGER IF EXISTS on_profile_couple_paired_discord ON public.profiles;
CREATE TRIGGER on_profile_couple_paired_discord
AFTER UPDATE OF couple_id ON public.profiles
FOR EACH ROW
WHEN (OLD.couple_id IS DISTINCT FROM NEW.couple_id AND NEW.couple_id IS NOT NULL)
EXECUTE FUNCTION public.notify_couple_paired_discord();
DROP TRIGGER IF EXISTS on_feedback_submitted_discord ON public.feedback;
CREATE TRIGGER on_feedback_submitted_discord
AFTER INSERT ON public.feedback
FOR EACH ROW
EXECUTE FUNCTION public.notify_feedback_submitted_discord();