From d1ddb9881e1fbbf0e7af732b2ee506d1c115b774 Mon Sep 17 00:00:00 2001 From: sepeterson <10458078+sepeterson@users.noreply.github.com> Date: Tue, 11 Nov 2025 10:46:40 -0600 Subject: [PATCH 01/32] MOB-993 add carousel button events --- .../Onboarding/OnboardingCarousel.js | 20 ++++++++++++++----- src/sharedHelpers/tracking.ts | 18 +++++++++++++++++ 2 files changed, 33 insertions(+), 5 deletions(-) create mode 100644 src/sharedHelpers/tracking.ts diff --git a/src/components/Onboarding/OnboardingCarousel.js b/src/components/Onboarding/OnboardingCarousel.js index ae6dd0b17..64a5707ab 100644 --- a/src/components/Onboarding/OnboardingCarousel.js +++ b/src/components/Onboarding/OnboardingCarousel.js @@ -29,6 +29,7 @@ import AnimatedDotsCarousel from "react-native-animated-dots-carousel"; import Animated, { interpolate, useAnimatedStyle, useSharedValue } from "react-native-reanimated"; import Carousel from "react-native-reanimated-carousel"; import { useOnboardingShown } from "sharedHelpers/installData"; +import { logFirebaseEvent } from "sharedHelpers/tracking"; import colors from "styles/tailwindColors"; const SlideItem = props => { @@ -75,7 +76,10 @@ const OnboardingCarousel = ( ) => { const [currentIndex, setCurrentIndex] = useState( 0 ); const [imagesLoaded, setImagesLoaded] = useState( false ); - const closeModal = () => setOnboardingShown( true ); + const closeModal = () => { + logFirebaseEvent( "onboarding_close_pressed", { currentIndex } ); + setOnboardingShown( true ); + }; const paginationColor = colors.white; const backgroundAnimation1 = useAnimatedStyle( () => { @@ -314,10 +318,16 @@ const OnboardingCarousel = ( ) => { level="primary" forceDark text={t( "CONTINUE" )} - onPress={() => ( - carouselRef.current?.getCurrentIndex() >= ONBOARDING_SLIDES.length - 1 - ? closeModal() - : carouselRef.current?.scrollTo( { count: 1, animated: true } ) )} + onPress={() => { + logFirebaseEvent( "onboarding_button_pressed", { current_slide: currentIndex } ); + const isLastSlide = carouselRef.current?.getCurrentIndex() + >= ONBOARDING_SLIDES.length - 1; + if ( isLastSlide ) { + closeModal(); + } else { + carouselRef.current?.scrollTo( { count: 1, animated: true } ); + } + }} /> diff --git a/src/sharedHelpers/tracking.ts b/src/sharedHelpers/tracking.ts new file mode 100644 index 000000000..f37e4a091 --- /dev/null +++ b/src/sharedHelpers/tracking.ts @@ -0,0 +1,18 @@ +import analytics from "@react-native-firebase/analytics"; +import { log } from "sharedHelpers/logger"; + +const logger = log.extend( "tracing.ts" ); + +type FirebaseParameters = Record + +// eslint-disable-next-line import/prefer-default-export +export const logFirebaseEvent = ( + eventId: string, + parameters: FirebaseParameters +) => { + // eslint-disable-next-line no-undef + if ( __DEV__ ) { + logger.info( `Firebase event: ${eventId} ${JSON.stringify( parameters )}` ); + } + void analytics().logEvent( eventId, parameters ); +}; From 74eac57dec75818229e748ed8885cfa43d96a790 Mon Sep 17 00:00:00 2001 From: sepeterson <10458078+sepeterson@users.noreply.github.com> Date: Tue, 11 Nov 2025 10:50:09 -0600 Subject: [PATCH 02/32] MOB-991 location permissions in callback dep --- src/components/Camera/AICamera/AICamera.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/components/Camera/AICamera/AICamera.js b/src/components/Camera/AICamera/AICamera.js index ed9f00988..2203b3ddb 100644 --- a/src/components/Camera/AICamera/AICamera.js +++ b/src/components/Camera/AICamera/AICamera.js @@ -16,6 +16,7 @@ import { VolumeManager } from "react-native-volume-manager"; import convertScoreToConfidence from "sharedHelpers/convertScores"; import { log } from "sharedHelpers/logger"; import { deleteSentinelFile, logStage } from "sharedHelpers/sentinelFiles"; +import { logFirebaseEvent } from "sharedHelpers/tracking"; import { useDebugMode, useLayoutPrefs, @@ -172,6 +173,7 @@ const AICamera = ( { const handleTakePhoto = useCallback( async ( ) => { await logStage( sentinelFileName, "take_photo_start" ); setHasTakenPhoto( true ); + logFirebaseEvent( "cv_camera_shutter_tap", { hasLocationPermissions } ); // this feels a little duplicative, but we're currently using aICameraSuggestion // to show the loading screen in Suggestions *without* setting an observation.taxon, // and we're using visionResult to populate ObsEdit *with* the taxon @@ -195,7 +197,8 @@ const AICamera = ( { setAICameraSuggestion, sentinelFileName, takePhotoAndStoreUri, - result + result, + hasLocationPermissions ] ); useEffect( () => { From 6ec3ffcb5c7dd2bc7326c02247d4603275ded4e5 Mon Sep 17 00:00:00 2001 From: sepeterson <10458078+sepeterson@users.noreply.github.com> Date: Wed, 12 Nov 2025 08:45:55 -0600 Subject: [PATCH 03/32] MOB-989 add firebase perf monitoring --- android/app/build.gradle | 1 + android/build.gradle | 1 + ios/Podfile.lock | 67 ++++++++++++++++++++++++++++++++++++++++ package-lock.json | 16 ++++++++++ package.json | 1 + 5 files changed, 86 insertions(+) diff --git a/android/app/build.gradle b/android/app/build.gradle index e37317eae..5b3c1eb52 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -2,6 +2,7 @@ apply plugin: "com.android.application" apply plugin: "org.jetbrains.kotlin.android" apply plugin: "com.facebook.react" apply plugin: "com.google.gms.google-services" +apply plugin: 'com.google.firebase.firebase-perf' // set up different .env files so fastlane will automatically build using .env // and dev builds will use .env.staging diff --git a/android/build.gradle b/android/build.gradle index ceb6dda83..438a40264 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -22,6 +22,7 @@ buildscript { classpath("com.facebook.react:react-native-gradle-plugin") classpath("org.jetbrains.kotlin:kotlin-gradle-plugin") classpath("com.google.gms:google-services:4.4.3") + classpath 'com.google.firebase:perf-plugin:2.0.1' } } diff --git a/ios/Podfile.lock b/ios/Podfile.lock index 76197d770..7a0250243 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -39,6 +39,11 @@ PODS: - FBLazyVector (0.80.2) - Firebase/CoreOnly (12.3.0): - FirebaseCore (~> 12.3.0) + - Firebase/Performance (12.3.0): + - Firebase/CoreOnly + - FirebasePerformance (~> 12.3.0) + - FirebaseABTesting (12.3.0): + - FirebaseCore (~> 12.3.0) - FirebaseAnalytics/Core (12.3.0): - FirebaseCore (~> 12.3.0) - FirebaseInstallations (~> 12.3.0) @@ -61,6 +66,8 @@ PODS: - FirebaseCoreInternal (~> 12.3.0) - GoogleUtilities/Environment (~> 8.1) - GoogleUtilities/Logger (~> 8.1) + - FirebaseCoreExtension (12.3.0): + - FirebaseCore (~> 12.3.0) - FirebaseCoreInternal (12.3.0): - "GoogleUtilities/NSData+zlib (~> 8.1)" - FirebaseInstallations (12.3.0): @@ -68,6 +75,35 @@ PODS: - GoogleUtilities/Environment (~> 8.1) - GoogleUtilities/UserDefaults (~> 8.1) - PromisesObjC (~> 2.4) + - FirebasePerformance (12.3.0): + - FirebaseCore (~> 12.3.0) + - FirebaseInstallations (~> 12.3.0) + - FirebaseRemoteConfig (~> 12.3.0) + - FirebaseSessions (~> 12.3.0) + - GoogleDataTransport (~> 10.1) + - GoogleUtilities/Environment (~> 8.1) + - GoogleUtilities/MethodSwizzler (~> 8.1) + - GoogleUtilities/UserDefaults (~> 8.1) + - nanopb (~> 3.30910.0) + - FirebaseRemoteConfig (12.3.0): + - FirebaseABTesting (~> 12.3.0) + - FirebaseCore (~> 12.3.0) + - FirebaseInstallations (~> 12.3.0) + - FirebaseRemoteConfigInterop (~> 12.3.0) + - FirebaseSharedSwift (~> 12.3.0) + - GoogleUtilities/Environment (~> 8.1) + - "GoogleUtilities/NSData+zlib (~> 8.1)" + - FirebaseRemoteConfigInterop (12.3.0) + - FirebaseSessions (12.3.0): + - FirebaseCore (~> 12.3.0) + - FirebaseCoreExtension (~> 12.3.0) + - FirebaseInstallations (~> 12.3.0) + - GoogleDataTransport (~> 10.1) + - GoogleUtilities/Environment (~> 8.1) + - GoogleUtilities/UserDefaults (~> 8.1) + - nanopb (~> 3.30910.0) + - PromisesSwift (~> 2.1) + - FirebaseSharedSwift (12.3.0) - fmt (11.0.2) - glog (0.3.5) - GoogleAppMeasurement/Core (12.3.0): @@ -83,6 +119,9 @@ PODS: - GoogleUtilities/Network (~> 8.1) - "GoogleUtilities/NSData+zlib (~> 8.1)" - nanopb (~> 3.30910.0) + - GoogleDataTransport (10.1.0): + - nanopb (~> 3.30910.0) + - PromisesObjC (~> 2.4) - GoogleSignIn (7.1.0): - AppAuth (< 2.0, >= 1.7.3) - GTMAppAuth (< 5.0, >= 4.1.1) @@ -128,6 +167,8 @@ PODS: - nanopb/decode (3.30910.0) - nanopb/encode (3.30910.0) - PromisesObjC (2.4.0) + - PromisesSwift (2.4.0): + - PromisesObjC (= 2.4.0) - RCT-Folly (2024.11.18.00): - boost - DoubleConversion @@ -2711,6 +2752,10 @@ PODS: - RNFBApp (23.4.0): - Firebase/CoreOnly (= 12.3.0) - React-Core + - RNFBPerf (23.4.0): + - Firebase/Performance (= 12.3.0) + - React-Core + - RNFBApp - RNFS (2.20.0): - React-Core - RNGestureHandler (2.28.0): @@ -3310,6 +3355,7 @@ DEPENDENCIES: - RNDeviceInfo (from `../node_modules/react-native-device-info`) - "RNFBAnalytics (from `../node_modules/@react-native-firebase/analytics`)" - "RNFBApp (from `../node_modules/@react-native-firebase/app`)" + - "RNFBPerf (from `../node_modules/@react-native-firebase/perf`)" - RNFS (from `../node_modules/react-native-fs`) - RNGestureHandler (from `../node_modules/react-native-gesture-handler`) - "RNGoogleSignin (from `../node_modules/@react-native-google-signin/google-signin`)" @@ -3329,11 +3375,19 @@ SPEC REPOS: trunk: - AppAuth - Firebase + - FirebaseABTesting - FirebaseAnalytics - FirebaseCore + - FirebaseCoreExtension - FirebaseCoreInternal - FirebaseInstallations + - FirebasePerformance + - FirebaseRemoteConfig + - FirebaseRemoteConfigInterop + - FirebaseSessions + - FirebaseSharedSwift - GoogleAppMeasurement + - GoogleDataTransport - GoogleSignIn - GoogleUtilities - GTMAppAuth @@ -3341,6 +3395,7 @@ SPEC REPOS: - Mute - nanopb - PromisesObjC + - PromisesSwift - React-Codegen - SocketRocket @@ -3556,6 +3611,8 @@ EXTERNAL SOURCES: :path: "../node_modules/@react-native-firebase/analytics" RNFBApp: :path: "../node_modules/@react-native-firebase/app" + RNFBPerf: + :path: "../node_modules/@react-native-firebase/perf" RNFS: :path: "../node_modules/react-native-fs" RNGestureHandler: @@ -3592,13 +3649,21 @@ SPEC CHECKSUMS: FasterImage: 0543472141d8f64716efdeb3bb886a6fe814f3b9 FBLazyVector: 86588b5a1547e7a417942a08f49559b184e002c8 Firebase: f5439b235721ceeef14ca1f327c0da8e4e8556b5 + FirebaseABTesting: 6e748dfecb81a49539c030adb5b12263acf62865 FirebaseAnalytics: fefb765365c6d3a4f2cb9d2c99dcf595124d41ae FirebaseCore: ff47fe1ad3ab9ef66edd3e8bc4647b493d2067f8 + FirebaseCoreExtension: 898ca55c7cb126f83747b32cb0daa95394b10853 FirebaseCoreInternal: a9e1ff270f217489d9258563b693d11a312903bf FirebaseInstallations: ca48ec60ea51b66b9f214a91847ea3720cde97f5 + FirebasePerformance: 12f8e5e26b7b7ae91db36e5fcd01e0c8b323e364 + FirebaseRemoteConfig: 4f4785cfc62e3e3b2f3715d1b7e41ade5ac0419e + FirebaseRemoteConfigInterop: 66d61ad6cee1cd2137cb8dedf468112e9ba92c6a + FirebaseSessions: 352b204966530e5cb7036c02d8c366922210e681 + FirebaseSharedSwift: a08758c5e2617f3ebb47865b45ecca6f02cd35ea fmt: a40bb5bd0294ea969aaaba240a927bd33d878cdd glog: 5683914934d5b6e4240e497e0f4a3b42d1854183 GoogleAppMeasurement: 9074f50943fe26a7829be3a2b0043270ad935b9e + GoogleDataTransport: aae35b7ea0c09004c3797d53c8c41f66f219d6a7 GoogleSignIn: d4281ab6cf21542b1cfaff85c191f230b399d2db GoogleUtilities: 00c88b9a86066ef77f0da2fab05f65d7768ed8e1 GTMAppAuth: f69bd07d68cd3b766125f7e072c45d7340dea0de @@ -3607,6 +3672,7 @@ SPEC CHECKSUMS: Mute: 20135a96076f140cc82bfc8b810e2d6150d8ec7e nanopb: fad817b59e0457d11a5dfbde799381cd727c1275 PromisesObjC: f5707f49cb48b9636751c5b2e7d227e43fba9f47 + PromisesSwift: 9d77319bbe72ebf6d872900551f7eeba9bce2851 RCT-Folly: 846fda9475e61ec7bcbf8a3fe81edfcaeb090669 RCTDeprecation: 300c5eb91114d4339b0bb39505d0f4824d7299b7 RCTRequired: e0446b01093475b7082fbeee5d1ef4ad1fe20ac4 @@ -3704,6 +3770,7 @@ SPEC CHECKSUMS: RNDeviceInfo: d863506092aef7e7af3a1c350c913d867d795047 RNFBAnalytics: 2c4927af02d587595a7888a8275b7e3a2ccc1842 RNFBApp: 0211ae65fadcb017cc787b2bd539847330687b93 + RNFBPerf: 3e7b00d193c37044790f92053fc013c12f385c75 RNFS: 89de7d7f4c0f6bafa05343c578f61118c8282ed8 RNGestureHandler: a52292df4f8fc7daab354ad2c37be401d4c3bed8 RNGoogleSignin: 65a7b18dd8fd9f279068ede103db4840f6782d99 diff --git a/package-lock.json b/package-lock.json index ceab6a8cd..7eb44da0f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -24,6 +24,7 @@ "@react-native-community/slider": "^4.5.0", "@react-native-firebase/analytics": "^23.4.0", "@react-native-firebase/app": "^23.3.1", + "@react-native-firebase/perf": "^23.4.0", "@react-native-google-signin/google-signin": "^13.1.0", "@react-native-picker/picker": "^2.11.1", "@react-native-vector-icons/common": "^12.3.0", @@ -5494,6 +5495,21 @@ } } }, + "node_modules/@react-native-firebase/perf": { + "version": "23.4.0", + "resolved": "https://registry.npmjs.org/@react-native-firebase/perf/-/perf-23.4.0.tgz", + "integrity": "sha512-E8UAmAdRIxlW3s6CLkmr4UHZSvMF62CxrG/QVDMZwJ0W/TYlyQZGooU5XE1wNYGxz8V3lPvCM1+7HemqrXrDUg==", + "license": "Apache-2.0", + "peerDependencies": { + "@react-native-firebase/app": "23.4.0", + "expo": ">=47.0.0" + }, + "peerDependenciesMeta": { + "expo": { + "optional": true + } + } + }, "node_modules/@react-native-google-signin/google-signin": { "version": "13.1.0", "resolved": "https://registry.npmjs.org/@react-native-google-signin/google-signin/-/google-signin-13.1.0.tgz", diff --git a/package.json b/package.json index e8ea45d47..75de4eb4e 100644 --- a/package.json +++ b/package.json @@ -60,6 +60,7 @@ "@react-native-community/slider": "^4.5.0", "@react-native-firebase/analytics": "^23.4.0", "@react-native-firebase/app": "^23.3.1", + "@react-native-firebase/perf": "^23.4.0", "@react-native-google-signin/google-signin": "^13.1.0", "@react-native-picker/picker": "^2.11.1", "@react-native-vector-icons/common": "^12.3.0", From 72c8f233523512319272c80b3f8f82c3ac542fab Mon Sep 17 00:00:00 2001 From: sepeterson <10458078+sepeterson@users.noreply.github.com> Date: Wed, 12 Nov 2025 14:49:52 -0600 Subject: [PATCH 04/32] MOB-989 implement global traces --- src/components/Camera/AICamera/AICamera.js | 9 +++-- src/components/Match/MatchContainer.js | 6 +++- src/stores/createFirebaseTraceSlice.ts | 41 ++++++++++++++++++++++ src/stores/useStore.js | 2 ++ 4 files changed, 55 insertions(+), 3 deletions(-) create mode 100644 src/stores/createFirebaseTraceSlice.ts diff --git a/src/components/Camera/AICamera/AICamera.js b/src/components/Camera/AICamera/AICamera.js index 2203b3ddb..1705dcb35 100644 --- a/src/components/Camera/AICamera/AICamera.js +++ b/src/components/Camera/AICamera/AICamera.js @@ -24,6 +24,7 @@ import { useTranslation } from "sharedHooks"; import { isDebugMode } from "sharedHooks/useDebugMode"; +import { FIREBASE_TRACES } from "stores/createFirebaseTraceSlice"; import useStore from "stores/useStore"; import colors from "styles/tailwindColors"; @@ -126,6 +127,8 @@ const AICamera = ( { ? device.formats[debugFormatIndex] : undefined; + const startTrace = useStore( state => state.startTrace ); + const toggleLocation = () => { if ( !useLocation && !hasLocationPermissions ) { requestLocationPermissions( ); @@ -173,7 +176,8 @@ const AICamera = ( { const handleTakePhoto = useCallback( async ( ) => { await logStage( sentinelFileName, "take_photo_start" ); setHasTakenPhoto( true ); - logFirebaseEvent( "cv_camera_shutter_tap", { hasLocationPermissions } ); + logFirebaseEvent( "ai_camera_shutter_tap", { hasLocationPermissions } ); + startTrace( FIREBASE_TRACES.AI_CAMERA_TO_MATCH ); // this feels a little duplicative, but we're currently using aICameraSuggestion // to show the loading screen in Suggestions *without* setting an observation.taxon, // and we're using visionResult to populate ObsEdit *with* the taxon @@ -198,7 +202,8 @@ const AICamera = ( { sentinelFileName, takePhotoAndStoreUri, result, - hasLocationPermissions + hasLocationPermissions, + startTrace ] ); useEffect( () => { diff --git a/src/components/Match/MatchContainer.js b/src/components/Match/MatchContainer.js index d90056b8f..257101b14 100644 --- a/src/components/Match/MatchContainer.js +++ b/src/components/Match/MatchContainer.js @@ -28,6 +28,7 @@ import { useExitObservationFlow, useLocationPermission, useSuggestions, useWatchPosition } from "sharedHooks"; import { isDebugMode } from "sharedHooks/useDebugMode"; +import { FIREBASE_TRACES } from "stores/createFirebaseTraceSlice"; import useStore from "stores/useStore"; import tryToReplaceWithLocalTaxon from "./helpers/tryToReplaceWithLocalTaxon"; @@ -125,6 +126,8 @@ const MatchContainer = ( ) => { shouldUseEvidenceLocation: evidenceHasLocation } ); + const stopTrace = useStore( state => state.stopTrace ); + const { scoreImageParams, onlineFetchStatus, @@ -167,6 +170,7 @@ const MatchContainer = ( ) => { const onFetched = useCallback( ( { isOnline }: { isOnline: boolean } ) => { if ( isOnline ) { + stopTrace( FIREBASE_TRACES.AI_CAMERA_TO_MATCH ); dispatch( { type: "SET_ONLINE_FETCH_STATUS", onlineFetchStatus: FETCH_STATUS_ONLINE_FETCHED @@ -191,7 +195,7 @@ const MatchContainer = ( ) => { } } }, - [onlineFetchStatus] + [onlineFetchStatus, stopTrace] ); const { diff --git a/src/stores/createFirebaseTraceSlice.ts b/src/stores/createFirebaseTraceSlice.ts new file mode 100644 index 000000000..dddb8969d --- /dev/null +++ b/src/stores/createFirebaseTraceSlice.ts @@ -0,0 +1,41 @@ +import perf, { FirebasePerformanceTypes } from "@react-native-firebase/perf"; +import { StateCreator } from "zustand"; + +export enum FIREBASE_TRACES { + AI_CAMERA_TO_MATCH = "ai_camera_to_match", +} + +export interface FirebaseTraceSlice { + activeTraces: Record; + startTrace: ( traceId: string ) => Promise; + stopTrace: ( traceId: string ) => Promise; +} + +const createFirebaseTraceSlice: StateCreator += ( set, get ) => ( { + activeTraces: {}, + + startTrace: async ( traceId: string ) => { + const trace = await perf().startTrace( traceId ); + set( state => ( { + activeTraces: { + ...state.activeTraces, + [traceId]: trace + } + } ) ); + }, + + stopTrace: async ( traceId: string ) => { + const { activeTraces } = get(); + const selectedTrace = activeTraces[traceId]; + if ( selectedTrace ) { + await selectedTrace.stop(); + set( state => { + const { [traceId]: _, ...remainingTraces } = state.activeTraces; + return { activeTraces: remainingTraces }; + } ); + } + } +} ); + +export default createFirebaseTraceSlice; diff --git a/src/stores/useStore.js b/src/stores/useStore.js index 0cee0c40b..980d1dcef 100644 --- a/src/stores/useStore.js +++ b/src/stores/useStore.js @@ -4,6 +4,7 @@ import { create } from "zustand"; import { createJSONStorage, persist } from "zustand/middleware"; import createExploreSlice from "./createExploreSlice"; +import createFirebaseTraceSlice from "./createFirebaseTraceSlice"; import createLayoutSlice from "./createLayoutSlice"; import createMyObsSlice from "./createMyObsSlice"; import createObservationFlowSlice from "./createObservationFlowSlice"; @@ -32,6 +33,7 @@ const useStore = create( persist( // Let's make our slices const slices = [ createExploreSlice( ...args ), + createFirebaseTraceSlice( ...args ), createLayoutSlice( ...args ), createMyObsSlice( ...args ), createObservationFlowSlice( ...args ), From 78ca8d9c0c5bd39d23216a6631cfe787a913da4f Mon Sep 17 00:00:00 2001 From: sepeterson <10458078+sepeterson@users.noreply.github.com> Date: Wed, 12 Nov 2025 15:14:05 -0600 Subject: [PATCH 05/32] MOB-991 rename continue firebase event --- src/components/Onboarding/OnboardingCarousel.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/components/Onboarding/OnboardingCarousel.js b/src/components/Onboarding/OnboardingCarousel.js index 64a5707ab..2df26d603 100644 --- a/src/components/Onboarding/OnboardingCarousel.js +++ b/src/components/Onboarding/OnboardingCarousel.js @@ -319,7 +319,10 @@ const OnboardingCarousel = ( ) => { forceDark text={t( "CONTINUE" )} onPress={() => { - logFirebaseEvent( "onboarding_button_pressed", { current_slide: currentIndex } ); + logFirebaseEvent( + "onboarding_continue_pressed", + { current_slide: currentIndex } + ); const isLastSlide = carouselRef.current?.getCurrentIndex() >= ONBOARDING_SLIDES.length - 1; if ( isLastSlide ) { From fb4a994dd02702404a43af5442617918f99b6a78 Mon Sep 17 00:00:00 2001 From: sepeterson <10458078+sepeterson@users.noreply.github.com> Date: Wed, 12 Nov 2025 16:15:36 -0600 Subject: [PATCH 06/32] MOB-991 add jest mock for perf --- tests/jest.setup.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/tests/jest.setup.js b/tests/jest.setup.js index bdae3ca75..6655e5312 100644 --- a/tests/jest.setup.js +++ b/tests/jest.setup.js @@ -109,6 +109,10 @@ jest.mock( "@react-native-firebase/analytics", () => ( { logEvent: jest.fn( ) } ) ); +jest.mock( "@react-native-firebase/perf", () => ( { + startTrace: jest.fn( ) +} ) ); + // see https://stackoverflow.com/questions/42268673/jest-test-animated-view-for-react-native-app // for more details about this withAnimatedTimeTravelEnabled approach. basically, this // allows us to step through animation frames when a screen is first loading when we're using the From ffafb7fd3aa74ff5020f4015b14ad3501bb8ca97 Mon Sep 17 00:00:00 2001 From: sepeterson <10458078+sepeterson@users.noreply.github.com> Date: Thu, 13 Nov 2025 16:44:59 -0600 Subject: [PATCH 07/32] MOB-991 clear traces on a timeout --- src/stores/createFirebaseTraceSlice.ts | 33 ++++++++++++++++++++++---- 1 file changed, 28 insertions(+), 5 deletions(-) diff --git a/src/stores/createFirebaseTraceSlice.ts b/src/stores/createFirebaseTraceSlice.ts index dddb8969d..8083ea298 100644 --- a/src/stores/createFirebaseTraceSlice.ts +++ b/src/stores/createFirebaseTraceSlice.ts @@ -5,10 +5,17 @@ export enum FIREBASE_TRACES { AI_CAMERA_TO_MATCH = "ai_camera_to_match", } +interface TraceData { + trace: FirebasePerformanceTypes.Trace; + // eslint-disable-next-line no-undef + timeoutId: NodeJS.Timeout; +} + export interface FirebaseTraceSlice { - activeTraces: Record; + activeTraces: Record; startTrace: ( traceId: string ) => Promise; stopTrace: ( traceId: string ) => Promise; + clearTrace: ( traceId: string ) => void; } const createFirebaseTraceSlice: StateCreator @@ -17,19 +24,35 @@ const createFirebaseTraceSlice: StateCreator startTrace: async ( traceId: string ) => { const trace = await perf().startTrace( traceId ); + + const timeoutId = setTimeout( () => { + get().clearTrace( traceId ); + }, 10000 ); + set( state => ( { activeTraces: { ...state.activeTraces, - [traceId]: trace + [traceId]: { trace, timeoutId } } } ) ); }, stopTrace: async ( traceId: string ) => { const { activeTraces } = get(); - const selectedTrace = activeTraces[traceId]; - if ( selectedTrace ) { - await selectedTrace.stop(); + const traceData = activeTraces[traceId]; + if ( traceData ) { + get().clearTrace( traceId ); + + await traceData.trace.stop(); + } + }, + + clearTrace: ( traceId: string ) => { + const { activeTraces } = get(); + const traceData = activeTraces[traceId]; + if ( traceData ) { + clearTimeout( traceData.timeoutId ); + set( state => { const { [traceId]: _, ...remainingTraces } = state.activeTraces; return { activeTraces: remainingTraces }; From 8b80c1746460b5a33f00a86af1b9b9cc30b25caf Mon Sep 17 00:00:00 2001 From: sepeterson <10458078+sepeterson@users.noreply.github.com> Date: Thu, 13 Nov 2025 16:45:49 -0600 Subject: [PATCH 08/32] MOB-991 separate timeout --- src/stores/createFirebaseTraceSlice.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/stores/createFirebaseTraceSlice.ts b/src/stores/createFirebaseTraceSlice.ts index 8083ea298..7491593b1 100644 --- a/src/stores/createFirebaseTraceSlice.ts +++ b/src/stores/createFirebaseTraceSlice.ts @@ -11,6 +11,8 @@ interface TraceData { timeoutId: NodeJS.Timeout; } +const TRACE_TIMEOUT = 10000; + export interface FirebaseTraceSlice { activeTraces: Record; startTrace: ( traceId: string ) => Promise; @@ -27,7 +29,7 @@ const createFirebaseTraceSlice: StateCreator const timeoutId = setTimeout( () => { get().clearTrace( traceId ); - }, 10000 ); + }, TRACE_TIMEOUT ); set( state => ( { activeTraces: { From d576fb10e9d563eeff6f26efb5bb3b239f54f356 Mon Sep 17 00:00:00 2001 From: sepeterson <10458078+sepeterson@users.noreply.github.com> Date: Fri, 14 Nov 2025 14:06:00 -0600 Subject: [PATCH 09/32] MOB-991 stop trace on timeout --- src/stores/createFirebaseTraceSlice.ts | 13 ++----------- 1 file changed, 2 insertions(+), 11 deletions(-) diff --git a/src/stores/createFirebaseTraceSlice.ts b/src/stores/createFirebaseTraceSlice.ts index 7491593b1..14b2e900e 100644 --- a/src/stores/createFirebaseTraceSlice.ts +++ b/src/stores/createFirebaseTraceSlice.ts @@ -17,7 +17,6 @@ export interface FirebaseTraceSlice { activeTraces: Record; startTrace: ( traceId: string ) => Promise; stopTrace: ( traceId: string ) => Promise; - clearTrace: ( traceId: string ) => void; } const createFirebaseTraceSlice: StateCreator @@ -28,7 +27,7 @@ const createFirebaseTraceSlice: StateCreator const trace = await perf().startTrace( traceId ); const timeoutId = setTimeout( () => { - get().clearTrace( traceId ); + get().stopTrace( traceId ); }, TRACE_TIMEOUT ); set( state => ( { @@ -43,17 +42,9 @@ const createFirebaseTraceSlice: StateCreator const { activeTraces } = get(); const traceData = activeTraces[traceId]; if ( traceData ) { - get().clearTrace( traceId ); + clearTimeout( traceData.timeoutId ); await traceData.trace.stop(); - } - }, - - clearTrace: ( traceId: string ) => { - const { activeTraces } = get(); - const traceData = activeTraces[traceId]; - if ( traceData ) { - clearTimeout( traceData.timeoutId ); set( state => { const { [traceId]: _, ...remainingTraces } = state.activeTraces; From 359d8042caca42eaa1bd88ae4698090ad3b3a72d Mon Sep 17 00:00:00 2001 From: sepeterson <10458078+sepeterson@users.noreply.github.com> Date: Fri, 14 Nov 2025 16:59:11 -0600 Subject: [PATCH 10/32] MOB-989 support custom trace attributes and move stopTrace to catch all match screen scenarios --- src/components/Match/MatchContainer.js | 23 ++++++++++++++++++++--- src/stores/createFirebaseTraceSlice.ts | 24 +++++++++++++++++++++--- 2 files changed, 41 insertions(+), 6 deletions(-) diff --git a/src/components/Match/MatchContainer.js b/src/components/Match/MatchContainer.js index 257101b14..e0572e716 100644 --- a/src/components/Match/MatchContainer.js +++ b/src/components/Match/MatchContainer.js @@ -28,7 +28,7 @@ import { useExitObservationFlow, useLocationPermission, useSuggestions, useWatchPosition } from "sharedHooks"; import { isDebugMode } from "sharedHooks/useDebugMode"; -import { FIREBASE_TRACES } from "stores/createFirebaseTraceSlice"; +import { FIREBASE_TRACE_ATTRIBUTES, FIREBASE_TRACES } from "stores/createFirebaseTraceSlice"; import useStore from "stores/useStore"; import tryToReplaceWithLocalTaxon from "./helpers/tryToReplaceWithLocalTaxon"; @@ -170,7 +170,6 @@ const MatchContainer = ( ) => { const onFetched = useCallback( ( { isOnline }: { isOnline: boolean } ) => { if ( isOnline ) { - stopTrace( FIREBASE_TRACES.AI_CAMERA_TO_MATCH ); dispatch( { type: "SET_ONLINE_FETCH_STATUS", onlineFetchStatus: FETCH_STATUS_ONLINE_FETCHED @@ -195,7 +194,7 @@ const MatchContainer = ( ) => { } } }, - [onlineFetchStatus, stopTrace] + [onlineFetchStatus] ); const { @@ -430,6 +429,24 @@ const MatchContainer = ( ) => { const suggestionsLoading = onlineFetchStatus === FETCH_STATUS_LOADING || offlineFetchStatus === FETCH_STATUS_LOADING; + useEffect( ( ) => { + if ( + onlineSuggestionsAttempted + && !suggestionsLoading + ) { + // This should capture a case where online and offline have had a chance to load + stopTrace( + FIREBASE_TRACES.AI_CAMERA_TO_MATCH, + { [FIREBASE_TRACE_ATTRIBUTES.ONLINE]: `${!usingOfflineSuggestions}` } + ); + } + }, [ + onlineSuggestionsAttempted, + suggestionsLoading, + stopTrace, + usingOfflineSuggestions + ] ); + // Remove the top suggestion from the list of other suggestions const otherSuggestions = orderedSuggestions .filter( suggestion => suggestion.taxon.id !== taxonId ); diff --git a/src/stores/createFirebaseTraceSlice.ts b/src/stores/createFirebaseTraceSlice.ts index 14b2e900e..63c485ac1 100644 --- a/src/stores/createFirebaseTraceSlice.ts +++ b/src/stores/createFirebaseTraceSlice.ts @@ -5,6 +5,10 @@ export enum FIREBASE_TRACES { AI_CAMERA_TO_MATCH = "ai_camera_to_match", } +export enum FIREBASE_TRACE_ATTRIBUTES { + ONLINE = "online", +} + interface TraceData { trace: FirebasePerformanceTypes.Trace; // eslint-disable-next-line no-undef @@ -23,13 +27,17 @@ const createFirebaseTraceSlice: StateCreator = ( set, get ) => ( { activeTraces: {}, - startTrace: async ( traceId: string ) => { + startTrace: async ( traceId: string, attributes: Record = {} ) => { const trace = await perf().startTrace( traceId ); const timeoutId = setTimeout( () => { get().stopTrace( traceId ); }, TRACE_TIMEOUT ); + Object.entries( attributes ).forEach( ( [key, value] ) => { + trace.putAttribute( key, value ); + } ); + set( state => ( { activeTraces: { ...state.activeTraces, @@ -38,13 +46,23 @@ const createFirebaseTraceSlice: StateCreator } ) ); }, - stopTrace: async ( traceId: string ) => { + stopTrace: async ( traceId: string, attributes: Record = {} ) => { const { activeTraces } = get(); const traceData = activeTraces[traceId]; if ( traceData ) { clearTimeout( traceData.timeoutId ); - await traceData.trace.stop(); + const { trace } = traceData; + try { + Object.entries( attributes ).forEach( ( [key, value] ) => { + trace.putAttribute( key, value ); + } ); + } catch ( _ ) { + /* this can error for a few reasons like value being a non-string + but we still need to stop the trace */ + } + + await trace.stop(); set( state => { const { [traceId]: _, ...remainingTraces } = state.activeTraces; From 1d2bef20da1236529e04f370f0ec0c8255302f4a Mon Sep 17 00:00:00 2001 From: sepeterson <10458078+sepeterson@users.noreply.github.com> Date: Fri, 14 Nov 2025 16:59:55 -0600 Subject: [PATCH 11/32] MOB-989 try block for startTrace attributes --- src/stores/createFirebaseTraceSlice.ts | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/src/stores/createFirebaseTraceSlice.ts b/src/stores/createFirebaseTraceSlice.ts index 63c485ac1..97d9ed6a3 100644 --- a/src/stores/createFirebaseTraceSlice.ts +++ b/src/stores/createFirebaseTraceSlice.ts @@ -34,9 +34,14 @@ const createFirebaseTraceSlice: StateCreator get().stopTrace( traceId ); }, TRACE_TIMEOUT ); - Object.entries( attributes ).forEach( ( [key, value] ) => { - trace.putAttribute( key, value ); - } ); + try { + Object.entries( attributes ).forEach( ( [key, value] ) => { + trace.putAttribute( key, value ); + } ); + } catch ( _ ) { + /* this can error for a few reasons like value being a non-string + but we still need to stop the trace */ + } set( state => ( { activeTraces: { From 3f2bcf043d768cdf6cb44a4e722ac3754b7e264a Mon Sep 17 00:00:00 2001 From: sepeterson <10458078+sepeterson@users.noreply.github.com> Date: Fri, 14 Nov 2025 17:02:07 -0600 Subject: [PATCH 12/32] MOB-991 consolidate onboarding event properties --- src/components/Onboarding/OnboardingCarousel.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/Onboarding/OnboardingCarousel.js b/src/components/Onboarding/OnboardingCarousel.js index 2df26d603..1f8932258 100644 --- a/src/components/Onboarding/OnboardingCarousel.js +++ b/src/components/Onboarding/OnboardingCarousel.js @@ -77,7 +77,7 @@ const OnboardingCarousel = ( ) => { const [imagesLoaded, setImagesLoaded] = useState( false ); const closeModal = () => { - logFirebaseEvent( "onboarding_close_pressed", { currentIndex } ); + logFirebaseEvent( "onboarding_close_pressed", { current_slide: currentIndex } ); setOnboardingShown( true ); }; From 38686bfdc2e34fb94550664af764575536fc2d43 Mon Sep 17 00:00:00 2001 From: sepeterson <10458078+sepeterson@users.noreply.github.com> Date: Mon, 17 Nov 2025 16:31:10 -0600 Subject: [PATCH 13/32] MOB-991 typeof timeout --- src/stores/createFirebaseTraceSlice.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/stores/createFirebaseTraceSlice.ts b/src/stores/createFirebaseTraceSlice.ts index 97d9ed6a3..8e8630f47 100644 --- a/src/stores/createFirebaseTraceSlice.ts +++ b/src/stores/createFirebaseTraceSlice.ts @@ -11,8 +11,7 @@ export enum FIREBASE_TRACE_ATTRIBUTES { interface TraceData { trace: FirebasePerformanceTypes.Trace; - // eslint-disable-next-line no-undef - timeoutId: NodeJS.Timeout; + timeoutId: ReturnType; } const TRACE_TIMEOUT = 10000; From 1704288590b4e29232c94f04a2c809b847ed5d2d Mon Sep 17 00:00:00 2001 From: sepeterson <10458078+sepeterson@users.noreply.github.com> Date: Fri, 21 Nov 2025 11:12:29 -0600 Subject: [PATCH 14/32] MOB-512 consistent gradle styling --- android/app/build.gradle | 2 +- android/build.gradle | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/android/app/build.gradle b/android/app/build.gradle index 5b3c1eb52..26f622cbe 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -2,7 +2,7 @@ apply plugin: "com.android.application" apply plugin: "org.jetbrains.kotlin.android" apply plugin: "com.facebook.react" apply plugin: "com.google.gms.google-services" -apply plugin: 'com.google.firebase.firebase-perf' +apply plugin: "com.google.firebase.firebase-perf" // set up different .env files so fastlane will automatically build using .env // and dev builds will use .env.staging diff --git a/android/build.gradle b/android/build.gradle index 438a40264..34249e940 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -22,7 +22,7 @@ buildscript { classpath("com.facebook.react:react-native-gradle-plugin") classpath("org.jetbrains.kotlin:kotlin-gradle-plugin") classpath("com.google.gms:google-services:4.4.3") - classpath 'com.google.firebase:perf-plugin:2.0.1' + classpath("com.google.firebase:perf-plugin:2.0.1") } } From 221ef816d4c4b7242e2ee1d516969919a7773872 Mon Sep 17 00:00:00 2001 From: sepeterson <10458078+sepeterson@users.noreply.github.com> Date: Fri, 21 Nov 2025 11:18:19 -0600 Subject: [PATCH 15/32] MOB-991 revise screen tracking --- src/navigation/OfflineNavigationGuard.tsx | 11 +++-------- src/sharedHelpers/tracking.ts | 20 +++++++++++++++++--- 2 files changed, 20 insertions(+), 11 deletions(-) diff --git a/src/navigation/OfflineNavigationGuard.tsx b/src/navigation/OfflineNavigationGuard.tsx index 0315723d2..6579cf61e 100644 --- a/src/navigation/OfflineNavigationGuard.tsx +++ b/src/navigation/OfflineNavigationGuard.tsx @@ -1,10 +1,10 @@ import { useNetInfo } from "@react-native-community/netinfo"; -import { getAnalytics, logEvent } from "@react-native-firebase/analytics"; import { NavigationContainer } from "@react-navigation/native"; import React, { PropsWithChildren, useRef } from "react"; import { Alert } from "react-native"; +import { logFirebaseScreenView } from "sharedHelpers/tracking"; import { useTranslation } from "sharedHooks"; import { navigationRef } from "./navigationUtils"; @@ -21,13 +21,8 @@ const OfflineNavigationGuard = ( { children }: PropsWithChildren ) => { const previousRouteName = routeNameRef.current; const currentRouteName = navigationRef.current?.getCurrentRoute( )?.name; // Basic screen tracking with Firebase Analytics - if ( previousRouteName !== currentRouteName ) { - const analytics = getAnalytics(); - // @ts-expect-error https://github.com/invertase/react-native-firebase/pull/8687 - logEvent( analytics, "screen_view", { - screen_name: currentRouteName, - screen_class: currentRouteName - } ); + if ( previousRouteName !== currentRouteName && currentRouteName ) { + logFirebaseScreenView( currentRouteName ); } if ( currentRouteName === "Login" && !isConnected ) { // return to previous screen if offline diff --git a/src/sharedHelpers/tracking.ts b/src/sharedHelpers/tracking.ts index f37e4a091..297362cd3 100644 --- a/src/sharedHelpers/tracking.ts +++ b/src/sharedHelpers/tracking.ts @@ -1,4 +1,6 @@ -import analytics from "@react-native-firebase/analytics"; +import { + getAnalytics, logEvent, logScreenView +} from "@react-native-firebase/analytics"; import { log } from "sharedHelpers/logger"; const logger = log.extend( "tracing.ts" ); @@ -8,11 +10,23 @@ type FirebaseParameters = Record // eslint-disable-next-line import/prefer-default-export export const logFirebaseEvent = ( eventId: string, - parameters: FirebaseParameters + parameters?: FirebaseParameters ) => { // eslint-disable-next-line no-undef if ( __DEV__ ) { logger.info( `Firebase event: ${eventId} ${JSON.stringify( parameters )}` ); } - void analytics().logEvent( eventId, parameters ); + const analytics = getAnalytics(); + logEvent( analytics, eventId, parameters ); +}; + +export const logFirebaseScreenView = ( + screenName: string +) => { + // eslint-disable-next-line no-undef + if ( __DEV__ ) { + logger.info( `Firebase screen view: ${screenName}` ); + } + const analytics = getAnalytics(); + logScreenView( analytics, { screen_name: screenName, screen_class: screenName } ); }; From db79ce1d71638e65331b08bef99be27c1115ac58 Mon Sep 17 00:00:00 2001 From: sepeterson <10458078+sepeterson@users.noreply.github.com> Date: Fri, 21 Nov 2025 11:24:13 -0600 Subject: [PATCH 16/32] MOB-991 more unique zustand names --- src/stores/createFirebaseTraceSlice.ts | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/src/stores/createFirebaseTraceSlice.ts b/src/stores/createFirebaseTraceSlice.ts index 8e8630f47..d87a1d2cb 100644 --- a/src/stores/createFirebaseTraceSlice.ts +++ b/src/stores/createFirebaseTraceSlice.ts @@ -17,20 +17,20 @@ interface TraceData { const TRACE_TIMEOUT = 10000; export interface FirebaseTraceSlice { - activeTraces: Record; - startTrace: ( traceId: string ) => Promise; - stopTrace: ( traceId: string ) => Promise; + activeFirebaseTraces: Record; + startFirebaseTrace: ( traceId: string ) => Promise; + stopFirebaseTrace: ( traceId: string ) => Promise; } const createFirebaseTraceSlice: StateCreator = ( set, get ) => ( { - activeTraces: {}, + activeFirebaseTraces: {}, - startTrace: async ( traceId: string, attributes: Record = {} ) => { + startFirebaseTrace: async ( traceId: string, attributes: Record = {} ) => { const trace = await perf().startTrace( traceId ); const timeoutId = setTimeout( () => { - get().stopTrace( traceId ); + get().stopFirebaseTrace( traceId ); }, TRACE_TIMEOUT ); try { @@ -43,16 +43,16 @@ const createFirebaseTraceSlice: StateCreator } set( state => ( { - activeTraces: { - ...state.activeTraces, + activeFirebaseTraces: { + ...state.activeFirebaseTraces, [traceId]: { trace, timeoutId } } } ) ); }, - stopTrace: async ( traceId: string, attributes: Record = {} ) => { - const { activeTraces } = get(); - const traceData = activeTraces[traceId]; + stopFirebaseTrace: async ( traceId: string, attributes: Record = {} ) => { + const { activeFirebaseTraces } = get(); + const traceData = activeFirebaseTraces[traceId]; if ( traceData ) { clearTimeout( traceData.timeoutId ); @@ -69,8 +69,8 @@ const createFirebaseTraceSlice: StateCreator await trace.stop(); set( state => { - const { [traceId]: _, ...remainingTraces } = state.activeTraces; - return { activeTraces: remainingTraces }; + const { [traceId]: _, ...remainingTraces } = state.activeFirebaseTraces; + return { activeFirebaseTraces: remainingTraces }; } ); } } From 30a400cf0ef43c1df7d18260730b336e3ade1db4 Mon Sep 17 00:00:00 2001 From: sepeterson <10458078+sepeterson@users.noreply.github.com> Date: Fri, 21 Nov 2025 11:25:00 -0600 Subject: [PATCH 17/32] MOB-991 correct filename for logger --- src/sharedHelpers/tracking.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/sharedHelpers/tracking.ts b/src/sharedHelpers/tracking.ts index 297362cd3..126ece311 100644 --- a/src/sharedHelpers/tracking.ts +++ b/src/sharedHelpers/tracking.ts @@ -3,7 +3,7 @@ import { } from "@react-native-firebase/analytics"; import { log } from "sharedHelpers/logger"; -const logger = log.extend( "tracing.ts" ); +const logger = log.extend( "tracking.ts" ); type FirebaseParameters = Record From 6e006ed86c366b27e43e9c1bb1dad75207de8631 Mon Sep 17 00:00:00 2001 From: sepeterson <10458078+sepeterson@users.noreply.github.com> Date: Fri, 21 Nov 2025 12:47:25 -0600 Subject: [PATCH 18/32] MOB-991 reference performance from getPerformance --- src/stores/createFirebaseTraceSlice.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/stores/createFirebaseTraceSlice.ts b/src/stores/createFirebaseTraceSlice.ts index d87a1d2cb..7cb146278 100644 --- a/src/stores/createFirebaseTraceSlice.ts +++ b/src/stores/createFirebaseTraceSlice.ts @@ -1,4 +1,4 @@ -import perf, { FirebasePerformanceTypes } from "@react-native-firebase/perf"; +import { FirebasePerformanceTypes, getPerformance } from "@react-native-firebase/perf"; import { StateCreator } from "zustand"; export enum FIREBASE_TRACES { @@ -27,7 +27,8 @@ const createFirebaseTraceSlice: StateCreator activeFirebaseTraces: {}, startFirebaseTrace: async ( traceId: string, attributes: Record = {} ) => { - const trace = await perf().startTrace( traceId ); + const perf = getPerformance(); + const trace = await perf.startTrace( traceId ); const timeoutId = setTimeout( () => { get().stopFirebaseTrace( traceId ); From 81e8bb83eed16d75f0819324d31d974bccfb4eba Mon Sep 17 00:00:00 2001 From: sepeterson <10458078+sepeterson@users.noreply.github.com> Date: Fri, 21 Nov 2025 12:58:04 -0600 Subject: [PATCH 19/32] MOB-991 log attribute error setting --- src/stores/createFirebaseTraceSlice.ts | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/src/stores/createFirebaseTraceSlice.ts b/src/stores/createFirebaseTraceSlice.ts index 7cb146278..519cc045b 100644 --- a/src/stores/createFirebaseTraceSlice.ts +++ b/src/stores/createFirebaseTraceSlice.ts @@ -1,6 +1,10 @@ import { FirebasePerformanceTypes, getPerformance } from "@react-native-firebase/perf"; import { StateCreator } from "zustand"; +import { log } from "../../react-native-logs.config"; + +const logger = log.extend( "createFirebaseTraceSlice.ts" ); + export enum FIREBASE_TRACES { AI_CAMERA_TO_MATCH = "ai_camera_to_match", } @@ -38,9 +42,8 @@ const createFirebaseTraceSlice: StateCreator Object.entries( attributes ).forEach( ( [key, value] ) => { trace.putAttribute( key, value ); } ); - } catch ( _ ) { - /* this can error for a few reasons like value being a non-string - but we still need to stop the trace */ + } catch ( error ) { + logger.error( "Error setting firebase trace attributes on start", error ); } set( state => ( { @@ -62,9 +65,8 @@ const createFirebaseTraceSlice: StateCreator Object.entries( attributes ).forEach( ( [key, value] ) => { trace.putAttribute( key, value ); } ); - } catch ( _ ) { - /* this can error for a few reasons like value being a non-string - but we still need to stop the trace */ + } catch ( error ) { + logger.error( "Error setting firebase trace attributes on stop", error ); } await trace.stop(); From 9287299aaeb7aa91af8e1d26ce615de12748b284 Mon Sep 17 00:00:00 2001 From: sepeterson <10458078+sepeterson@users.noreply.github.com> Date: Fri, 21 Nov 2025 13:05:35 -0600 Subject: [PATCH 20/32] MOB-991 reuse attribute logic, set attribute for timeouts --- src/stores/createFirebaseTraceSlice.ts | 36 ++++++++++++++------------ 1 file changed, 19 insertions(+), 17 deletions(-) diff --git a/src/stores/createFirebaseTraceSlice.ts b/src/stores/createFirebaseTraceSlice.ts index 519cc045b..40366e284 100644 --- a/src/stores/createFirebaseTraceSlice.ts +++ b/src/stores/createFirebaseTraceSlice.ts @@ -11,6 +11,7 @@ export enum FIREBASE_TRACES { export enum FIREBASE_TRACE_ATTRIBUTES { ONLINE = "online", + DID_TIMEOUT = "did_timeout", } interface TraceData { @@ -20,10 +21,23 @@ interface TraceData { const TRACE_TIMEOUT = 10000; +const applyTraceAttributes = ( + trace: FirebasePerformanceTypes.Trace, + attributes: Record +): void => { + try { + Object.entries( attributes ).forEach( ( [key, value] ) => { + trace.putAttribute( key, value ); + } ); + } catch ( error ) { + logger.error( "Error setting firebase trace attributes", error ); + } +}; + export interface FirebaseTraceSlice { activeFirebaseTraces: Record; - startFirebaseTrace: ( traceId: string ) => Promise; - stopFirebaseTrace: ( traceId: string ) => Promise; + startFirebaseTrace: ( traceId: string, attributes: Record ) => Promise; + stopFirebaseTrace: ( traceId: string, attributes: Record ) => Promise; } const createFirebaseTraceSlice: StateCreator @@ -35,16 +49,10 @@ const createFirebaseTraceSlice: StateCreator const trace = await perf.startTrace( traceId ); const timeoutId = setTimeout( () => { - get().stopFirebaseTrace( traceId ); + get().stopFirebaseTrace( traceId, { [FIREBASE_TRACE_ATTRIBUTES.DID_TIMEOUT]: "true" } ); }, TRACE_TIMEOUT ); - try { - Object.entries( attributes ).forEach( ( [key, value] ) => { - trace.putAttribute( key, value ); - } ); - } catch ( error ) { - logger.error( "Error setting firebase trace attributes on start", error ); - } + applyTraceAttributes( trace, attributes ); set( state => ( { activeFirebaseTraces: { @@ -61,13 +69,7 @@ const createFirebaseTraceSlice: StateCreator clearTimeout( traceData.timeoutId ); const { trace } = traceData; - try { - Object.entries( attributes ).forEach( ( [key, value] ) => { - trace.putAttribute( key, value ); - } ); - } catch ( error ) { - logger.error( "Error setting firebase trace attributes on stop", error ); - } + applyTraceAttributes( trace, attributes ); await trace.stop(); From 0dd99311bd14ec897d81b39d15b9469d6813e1dc Mon Sep 17 00:00:00 2001 From: sepeterson <10458078+sepeterson@users.noreply.github.com> Date: Fri, 21 Nov 2025 14:23:36 -0600 Subject: [PATCH 21/32] MOB-991 reusable firebase attributes type and enforce length limits --- src/stores/createFirebaseTraceSlice.ts | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/src/stores/createFirebaseTraceSlice.ts b/src/stores/createFirebaseTraceSlice.ts index 40366e284..d4cd6c224 100644 --- a/src/stores/createFirebaseTraceSlice.ts +++ b/src/stores/createFirebaseTraceSlice.ts @@ -19,15 +19,22 @@ interface TraceData { timeoutId: ReturnType; } +type FirebaseTraceAttributes = Partial>; + const TRACE_TIMEOUT = 10000; const applyTraceAttributes = ( trace: FirebasePerformanceTypes.Trace, - attributes: Record + attributes: FirebaseTraceAttributes ): void => { try { Object.entries( attributes ).forEach( ( [key, value] ) => { - trace.putAttribute( key, value ); + // Firebase will silently fail if we exceed these limits + if ( key.length <= 40 && value.length <= 100 ) { + trace.putAttribute( key, value ); + } else { + logger.error( `Failed to set firebase attribute (too long): ${key}=${value}` ); + } } ); } catch ( error ) { logger.error( "Error setting firebase trace attributes", error ); @@ -36,15 +43,15 @@ const applyTraceAttributes = ( export interface FirebaseTraceSlice { activeFirebaseTraces: Record; - startFirebaseTrace: ( traceId: string, attributes: Record ) => Promise; - stopFirebaseTrace: ( traceId: string, attributes: Record ) => Promise; + startFirebaseTrace: ( traceId: string, attributes: FirebaseTraceAttributes ) => Promise; + stopFirebaseTrace: ( traceId: string, attributes: FirebaseTraceAttributes ) => Promise; } const createFirebaseTraceSlice: StateCreator = ( set, get ) => ( { activeFirebaseTraces: {}, - startFirebaseTrace: async ( traceId: string, attributes: Record = {} ) => { + startFirebaseTrace: async ( traceId: string, attributes: FirebaseTraceAttributes = {} ) => { const perf = getPerformance(); const trace = await perf.startTrace( traceId ); @@ -62,7 +69,7 @@ const createFirebaseTraceSlice: StateCreator } ) ); }, - stopFirebaseTrace: async ( traceId: string, attributes: Record = {} ) => { + stopFirebaseTrace: async ( traceId: string, attributes: FirebaseTraceAttributes = {} ) => { const { activeFirebaseTraces } = get(); const traceData = activeFirebaseTraces[traceId]; if ( traceData ) { From 09309fe106ca837dac1b68e7cf566cd2bd7f77ee Mon Sep 17 00:00:00 2001 From: sepeterson <10458078+sepeterson@users.noreply.github.com> Date: Fri, 21 Nov 2025 14:33:37 -0600 Subject: [PATCH 22/32] MOB-991: log errors for firebase log events --- src/sharedHelpers/tracking.ts | 21 ++++++++++----------- 1 file changed, 10 insertions(+), 11 deletions(-) diff --git a/src/sharedHelpers/tracking.ts b/src/sharedHelpers/tracking.ts index 126ece311..025e2036c 100644 --- a/src/sharedHelpers/tracking.ts +++ b/src/sharedHelpers/tracking.ts @@ -7,26 +7,25 @@ const logger = log.extend( "tracking.ts" ); type FirebaseParameters = Record -// eslint-disable-next-line import/prefer-default-export export const logFirebaseEvent = ( eventId: string, parameters?: FirebaseParameters ) => { - // eslint-disable-next-line no-undef - if ( __DEV__ ) { - logger.info( `Firebase event: ${eventId} ${JSON.stringify( parameters )}` ); + try { + const analytics = getAnalytics(); + logEvent( analytics, eventId, parameters ); + } catch ( error ) { + logger.error( "Error logging firebase event", error ); } - const analytics = getAnalytics(); - logEvent( analytics, eventId, parameters ); }; export const logFirebaseScreenView = ( screenName: string ) => { - // eslint-disable-next-line no-undef - if ( __DEV__ ) { - logger.info( `Firebase screen view: ${screenName}` ); + try { + const analytics = getAnalytics(); + logScreenView( analytics, { screen_name: screenName, screen_class: screenName } ); + } catch ( error ) { + logger.error( "Error logging firebase screen view", error ); } - const analytics = getAnalytics(); - logScreenView( analytics, { screen_name: screenName, screen_class: screenName } ); }; From bf953bfc423c24b9e164877304ae9b582cb10807 Mon Sep 17 00:00:00 2001 From: sepeterson <10458078+sepeterson@users.noreply.github.com> Date: Mon, 24 Nov 2025 00:37:04 -0600 Subject: [PATCH 23/32] MOB-991 add metric logic --- src/stores/createFirebaseTraceSlice.ts | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/src/stores/createFirebaseTraceSlice.ts b/src/stores/createFirebaseTraceSlice.ts index d4cd6c224..03585e122 100644 --- a/src/stores/createFirebaseTraceSlice.ts +++ b/src/stores/createFirebaseTraceSlice.ts @@ -19,7 +19,7 @@ interface TraceData { timeoutId: ReturnType; } -type FirebaseTraceAttributes = Partial>; +type FirebaseTraceAttributes = Partial>; const TRACE_TIMEOUT = 10000; @@ -29,11 +29,17 @@ const applyTraceAttributes = ( ): void => { try { Object.entries( attributes ).forEach( ( [key, value] ) => { + if ( typeof value === "string" ) { // Firebase will silently fail if we exceed these limits - if ( key.length <= 40 && value.length <= 100 ) { - trace.putAttribute( key, value ); - } else { - logger.error( `Failed to set firebase attribute (too long): ${key}=${value}` ); + if ( key.length <= 40 && value.length <= 100 ) { + trace.putAttribute( key, value ); + } else { + logger.error( `Failed to set firebase attribute (too long): ${key}=${value}` ); + } + } else if ( key.length <= 32 && /^(?!_)[^\s](?:.*[^\s])?$/.test( key ) ) { + /* metric key constraint: "Must not have a leading or trailing whitespace, + no leading underscore '_' character and have a max length of 32 characters." */ + trace.putMetric( key, value ); } } ); } catch ( error ) { From 2b7dc12b0025f4752926991176b95514ae5b3248 Mon Sep 17 00:00:00 2001 From: sepeterson <10458078+sepeterson@users.noreply.github.com> Date: Mon, 24 Nov 2025 14:44:58 -0600 Subject: [PATCH 24/32] MOB-991 fix firebase trace function names :) --- src/components/Camera/AICamera/AICamera.js | 6 +++--- src/components/Match/MatchContainer.js | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/components/Camera/AICamera/AICamera.js b/src/components/Camera/AICamera/AICamera.js index 1705dcb35..f9894ab8f 100644 --- a/src/components/Camera/AICamera/AICamera.js +++ b/src/components/Camera/AICamera/AICamera.js @@ -127,7 +127,7 @@ const AICamera = ( { ? device.formats[debugFormatIndex] : undefined; - const startTrace = useStore( state => state.startTrace ); + const startFirebaseTrace = useStore( state => state.startFirebaseTrace ); const toggleLocation = () => { if ( !useLocation && !hasLocationPermissions ) { @@ -177,7 +177,7 @@ const AICamera = ( { await logStage( sentinelFileName, "take_photo_start" ); setHasTakenPhoto( true ); logFirebaseEvent( "ai_camera_shutter_tap", { hasLocationPermissions } ); - startTrace( FIREBASE_TRACES.AI_CAMERA_TO_MATCH ); + startFirebaseTrace( FIREBASE_TRACES.AI_CAMERA_TO_MATCH ); // this feels a little duplicative, but we're currently using aICameraSuggestion // to show the loading screen in Suggestions *without* setting an observation.taxon, // and we're using visionResult to populate ObsEdit *with* the taxon @@ -203,7 +203,7 @@ const AICamera = ( { takePhotoAndStoreUri, result, hasLocationPermissions, - startTrace + startFirebaseTrace ] ); useEffect( () => { diff --git a/src/components/Match/MatchContainer.js b/src/components/Match/MatchContainer.js index e0572e716..9a59281e1 100644 --- a/src/components/Match/MatchContainer.js +++ b/src/components/Match/MatchContainer.js @@ -126,7 +126,7 @@ const MatchContainer = ( ) => { shouldUseEvidenceLocation: evidenceHasLocation } ); - const stopTrace = useStore( state => state.stopTrace ); + const stopFirebaseTrace = useStore( state => state.stopFirebaseTrace ); const { scoreImageParams, @@ -435,7 +435,7 @@ const MatchContainer = ( ) => { && !suggestionsLoading ) { // This should capture a case where online and offline have had a chance to load - stopTrace( + stopFirebaseTrace( FIREBASE_TRACES.AI_CAMERA_TO_MATCH, { [FIREBASE_TRACE_ATTRIBUTES.ONLINE]: `${!usingOfflineSuggestions}` } ); @@ -443,7 +443,7 @@ const MatchContainer = ( ) => { }, [ onlineSuggestionsAttempted, suggestionsLoading, - stopTrace, + stopFirebaseTrace, usingOfflineSuggestions ] ); From f847d3ab19ab3a5f4b00e765d417db45dfc235c5 Mon Sep 17 00:00:00 2001 From: sepeterson <10458078+sepeterson@users.noreply.github.com> Date: Tue, 25 Nov 2025 07:57:13 -0600 Subject: [PATCH 25/32] MOB-991 moce trace start to CameraContainer to avoid catching permission gate --- src/components/Camera/AICamera/AICamera.js | 7 +------ src/components/Camera/CameraContainer.tsx | 11 ++++++++++- 2 files changed, 11 insertions(+), 7 deletions(-) diff --git a/src/components/Camera/AICamera/AICamera.js b/src/components/Camera/AICamera/AICamera.js index f9894ab8f..b805a50ed 100644 --- a/src/components/Camera/AICamera/AICamera.js +++ b/src/components/Camera/AICamera/AICamera.js @@ -24,7 +24,6 @@ import { useTranslation } from "sharedHooks"; import { isDebugMode } from "sharedHooks/useDebugMode"; -import { FIREBASE_TRACES } from "stores/createFirebaseTraceSlice"; import useStore from "stores/useStore"; import colors from "styles/tailwindColors"; @@ -127,8 +126,6 @@ const AICamera = ( { ? device.formats[debugFormatIndex] : undefined; - const startFirebaseTrace = useStore( state => state.startFirebaseTrace ); - const toggleLocation = () => { if ( !useLocation && !hasLocationPermissions ) { requestLocationPermissions( ); @@ -177,7 +174,6 @@ const AICamera = ( { await logStage( sentinelFileName, "take_photo_start" ); setHasTakenPhoto( true ); logFirebaseEvent( "ai_camera_shutter_tap", { hasLocationPermissions } ); - startFirebaseTrace( FIREBASE_TRACES.AI_CAMERA_TO_MATCH ); // this feels a little duplicative, but we're currently using aICameraSuggestion // to show the loading screen in Suggestions *without* setting an observation.taxon, // and we're using visionResult to populate ObsEdit *with* the taxon @@ -202,8 +198,7 @@ const AICamera = ( { sentinelFileName, takePhotoAndStoreUri, result, - hasLocationPermissions, - startFirebaseTrace + hasLocationPermissions ] ); useEffect( () => { diff --git a/src/components/Camera/CameraContainer.tsx b/src/components/Camera/CameraContainer.tsx index d8b003a02..66909ceb2 100644 --- a/src/components/Camera/CameraContainer.tsx +++ b/src/components/Camera/CameraContainer.tsx @@ -22,6 +22,7 @@ import { log } from "sharedHelpers/logger"; import { createSentinelFile, deleteSentinelFile, logStage } from "sharedHelpers/sentinelFiles"; import { useTranslation } from "sharedHooks"; import useLocationPermission from "sharedHooks/useLocationPermission"; +import { FIREBASE_TRACES } from "stores/createFirebaseTraceSlice"; import useStore from "stores/useStore"; import CameraWithDevice from "./CameraWithDevice"; @@ -67,6 +68,7 @@ const CameraContainer = ( ) => { const sentinelFileName = useStore( state => state.sentinelFileName ); const setSentinelFileName = useStore( state => state.setSentinelFileName ); const addCameraRollUris = useStore( state => state.addCameraRollUris ); + const startFirebaseTrace = useStore( state => state.startFirebaseTrace ); const { params } = useRoute( ); const cameraType = params?.camera; @@ -199,6 +201,11 @@ const CameraContainer = ( ) => { visionResult: StoredResult | null ) => { if ( !showPhotoPermissionsGate ) { + if ( cameraType === "AI" ) { + // We won't start the trace if the user hasn't granted photo permissions + // because the time it takes to request permissions would be included in the trace + startFirebaseTrace( FIREBASE_TRACES.AI_CAMERA_TO_MATCH ); + } await handleNavigation( newPhotoState, visionResult ); } else { await logStageIfAICamera( "request_save_photo_permission_start" ); @@ -208,7 +215,9 @@ const CameraContainer = ( ) => { handleNavigation, requestSavePhotoPermission, showPhotoPermissionsGate, - logStageIfAICamera + logStageIfAICamera, + startFirebaseTrace, + cameraType ] ); const toggleFlash = ( ) => { From 403be86702e158b4009664acad712f8be5060cb2 Mon Sep 17 00:00:00 2001 From: sepeterson <10458078+sepeterson@users.noreply.github.com> Date: Tue, 25 Nov 2025 08:05:31 -0600 Subject: [PATCH 26/32] MOB-991 fix perf jest mock --- tests/jest.setup.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/jest.setup.js b/tests/jest.setup.js index 6655e5312..9f6eac302 100644 --- a/tests/jest.setup.js +++ b/tests/jest.setup.js @@ -110,7 +110,8 @@ jest.mock( "@react-native-firebase/analytics", () => ( { } ) ); jest.mock( "@react-native-firebase/perf", () => ( { - startTrace: jest.fn( ) + startFirebaseTrace: jest.fn( ), + stopFirebaseTrace: jest.fn( ) } ) ); // see https://stackoverflow.com/questions/42268673/jest-test-animated-view-for-react-native-app From ab8f0bd1a53cdc6b9d915f564cde50b1d5f5dfc5 Mon Sep 17 00:00:00 2001 From: sepeterson <10458078+sepeterson@users.noreply.github.com> Date: Tue, 2 Dec 2025 09:14:03 -0600 Subject: [PATCH 27/32] MOB-991 rm firebase log from pressableWithTracking --- .../SharedComponents/Buttons/PressableWithTracking.tsx | 7 ------- 1 file changed, 7 deletions(-) diff --git a/src/components/SharedComponents/Buttons/PressableWithTracking.tsx b/src/components/SharedComponents/Buttons/PressableWithTracking.tsx index 820432fdf..0810b2ead 100644 --- a/src/components/SharedComponents/Buttons/PressableWithTracking.tsx +++ b/src/components/SharedComponents/Buttons/PressableWithTracking.tsx @@ -1,4 +1,3 @@ -import { getAnalytics, logEvent } from "@react-native-firebase/analytics"; import { getCurrentRoute } from "navigation/navigationUtils"; import React from "react"; import type { PressableProps } from "react-native"; @@ -15,12 +14,6 @@ const PressableWithTracking = React.forwardRef if ( otherProps?.testID ) { const currentRoute = getCurrentRoute( ); logger.info( `Button tap: ${otherProps?.testID}-${currentRoute?.name || "undefined"}` ); - // Basic button tap tracking with Firebase Analytics - const analytics = getAnalytics(); - logEvent( analytics, "button_tap", { - testID: otherProps?.testID, - screen: currentRoute?.name - } ); } if ( onPress ) { From 4e309e23fd396eb802c8802a73ece0579426080c Mon Sep 17 00:00:00 2001 From: sepeterson <10458078+sepeterson@users.noreply.github.com> Date: Tue, 2 Dec 2025 09:55:22 -0600 Subject: [PATCH 28/32] MOB-991 stringify error logs --- src/sharedHelpers/tracking.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/sharedHelpers/tracking.ts b/src/sharedHelpers/tracking.ts index 025e2036c..391bbe187 100644 --- a/src/sharedHelpers/tracking.ts +++ b/src/sharedHelpers/tracking.ts @@ -15,7 +15,7 @@ export const logFirebaseEvent = ( const analytics = getAnalytics(); logEvent( analytics, eventId, parameters ); } catch ( error ) { - logger.error( "Error logging firebase event", error ); + logger.error( "Error logging firebase event", JSON.stringify( error ) ); } }; @@ -26,6 +26,6 @@ export const logFirebaseScreenView = ( const analytics = getAnalytics(); logScreenView( analytics, { screen_name: screenName, screen_class: screenName } ); } catch ( error ) { - logger.error( "Error logging firebase screen view", error ); + logger.error( "Error logging firebase screen view", JSON.stringify( error ) ); } }; From 93080ea03104a1f329fb8045eb42c24b8503a6ea Mon Sep 17 00:00:00 2001 From: sepeterson <10458078+sepeterson@users.noreply.github.com> Date: Tue, 2 Dec 2025 11:02:26 -0600 Subject: [PATCH 29/32] MOB-991 move start trace to nav function and add photo permission parameter --- src/components/Camera/CameraContainer.tsx | 27 +++++++++++------------ 1 file changed, 13 insertions(+), 14 deletions(-) diff --git a/src/components/Camera/CameraContainer.tsx b/src/components/Camera/CameraContainer.tsx index 66909ceb2..55428c1d2 100644 --- a/src/components/Camera/CameraContainer.tsx +++ b/src/components/Camera/CameraContainer.tsx @@ -22,7 +22,7 @@ import { log } from "sharedHelpers/logger"; import { createSentinelFile, deleteSentinelFile, logStage } from "sharedHelpers/sentinelFiles"; import { useTranslation } from "sharedHooks"; import useLocationPermission from "sharedHooks/useLocationPermission"; -import { FIREBASE_TRACES } from "stores/createFirebaseTraceSlice"; +import { FIREBASE_TRACE_ATTRIBUTES, FIREBASE_TRACES } from "stores/createFirebaseTraceSlice"; import useStore from "stores/useStore"; import CameraWithDevice from "./CameraWithDevice"; @@ -173,8 +173,13 @@ const CameraContainer = ( ) => { // and we want to make sure Suggestions has the correct observationPhotos const handleNavigation = useCallback( async ( newPhotoState: PhotoState, - visionResult: StoredResult | null + hasSavePhotoPermission: boolean, + visionResult?: StoredResult | null ) => { + startFirebaseTrace( + FIREBASE_TRACES.AI_CAMERA_TO_MATCH, + { [FIREBASE_TRACE_ATTRIBUTES.HAS_SAVE_PHOTO_PERMISSION]: `${hasSavePhotoPermission}` } + ); // fetch accurate user location, with a fallback to a course location // at the time the user taps AI shutter or multicapture checkmark // to create an observation @@ -193,7 +198,8 @@ const CameraContainer = ( ) => { prepareStoreAndNavigate, navigationOptions, logStageIfAICamera, - deleteStageIfAICamera + deleteStageIfAICamera, + startFirebaseTrace ] ); const handleCheckmarkPress = useCallback( async ( @@ -201,12 +207,7 @@ const CameraContainer = ( ) => { visionResult: StoredResult | null ) => { if ( !showPhotoPermissionsGate ) { - if ( cameraType === "AI" ) { - // We won't start the trace if the user hasn't granted photo permissions - // because the time it takes to request permissions would be included in the trace - startFirebaseTrace( FIREBASE_TRACES.AI_CAMERA_TO_MATCH ); - } - await handleNavigation( newPhotoState, visionResult ); + await handleNavigation( newPhotoState, true, visionResult ); } else { await logStageIfAICamera( "request_save_photo_permission_start" ); requestSavePhotoPermission( ); @@ -215,9 +216,7 @@ const CameraContainer = ( ) => { handleNavigation, requestSavePhotoPermission, showPhotoPermissionsGate, - logStageIfAICamera, - startFirebaseTrace, - cameraType + logStageIfAICamera ] ); const toggleFlash = ( ) => { @@ -364,14 +363,14 @@ const CameraContainer = ( ) => { await handleNavigation( { cameraUris, evidenceToAdd - } ); + }, true ); }, onModalHide: async ( ) => { await logStageIfAICamera( "request_save_photo_permission_complete" ); await handleNavigation( { cameraUris, evidenceToAdd - } ); + }, false ); } } )} {renderLocationPermissionsGate( { From 86430591fcc38aad73510fd795975db6490b35a1 Mon Sep 17 00:00:00 2001 From: sepeterson <10458078+sepeterson@users.noreply.github.com> Date: Tue, 2 Dec 2025 11:08:28 -0600 Subject: [PATCH 30/32] MOB-991 use logEvent for screenviews to avoid deprecation --- src/sharedHelpers/tracking.ts | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/sharedHelpers/tracking.ts b/src/sharedHelpers/tracking.ts index 391bbe187..3ce03ecf8 100644 --- a/src/sharedHelpers/tracking.ts +++ b/src/sharedHelpers/tracking.ts @@ -1,5 +1,5 @@ import { - getAnalytics, logEvent, logScreenView + getAnalytics, logEvent } from "@react-native-firebase/analytics"; import { log } from "sharedHelpers/logger"; @@ -24,7 +24,10 @@ export const logFirebaseScreenView = ( ) => { try { const analytics = getAnalytics(); - logScreenView( analytics, { screen_name: screenName, screen_class: screenName } ); + logEvent( analytics, "screen_view", { + firebase_screen: screenName, + firebase_screen_class: screenName + } ); } catch ( error ) { logger.error( "Error logging firebase screen view", JSON.stringify( error ) ); } From 3e0ae32059f8c9525d769f74a166a8e4871599c4 Mon Sep 17 00:00:00 2001 From: sepeterson <10458078+sepeterson@users.noreply.github.com> Date: Tue, 2 Dec 2025 11:09:49 -0600 Subject: [PATCH 31/32] MOB-991 add photo permission attribute and stringify an error --- src/stores/createFirebaseTraceSlice.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/stores/createFirebaseTraceSlice.ts b/src/stores/createFirebaseTraceSlice.ts index 03585e122..2fb1967f2 100644 --- a/src/stores/createFirebaseTraceSlice.ts +++ b/src/stores/createFirebaseTraceSlice.ts @@ -12,6 +12,7 @@ export enum FIREBASE_TRACES { export enum FIREBASE_TRACE_ATTRIBUTES { ONLINE = "online", DID_TIMEOUT = "did_timeout", + HAS_SAVE_PHOTO_PERMISSION = "has_save_photo_permission", } interface TraceData { @@ -43,7 +44,7 @@ const applyTraceAttributes = ( } } ); } catch ( error ) { - logger.error( "Error setting firebase trace attributes", error ); + logger.error( "Error setting firebase trace attributes", JSON.stringify( error ) ); } }; From 83c06dbeacd9f34e61d0a850e3ed4959bba81131 Mon Sep 17 00:00:00 2001 From: sepeterson <10458078+sepeterson@users.noreply.github.com> Date: Thu, 4 Dec 2025 10:30:26 -0600 Subject: [PATCH 32/32] MOB-991 fix type import lint errors --- src/stores/createFirebaseTraceSlice.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/stores/createFirebaseTraceSlice.ts b/src/stores/createFirebaseTraceSlice.ts index 2fb1967f2..8e0e89fb0 100644 --- a/src/stores/createFirebaseTraceSlice.ts +++ b/src/stores/createFirebaseTraceSlice.ts @@ -1,5 +1,6 @@ -import { FirebasePerformanceTypes, getPerformance } from "@react-native-firebase/perf"; -import { StateCreator } from "zustand"; +import type { FirebasePerformanceTypes } from "@react-native-firebase/perf"; +import { getPerformance } from "@react-native-firebase/perf"; +import type { StateCreator } from "zustand"; import { log } from "../../react-native-logs.config";