Merge pull request #3230 from inaturalist/mob-991-firebase-record-whether-or-not-the-user-has-given-location

MOB-991 MOB-993 MOB-989 add firebase performance monitoring and a couple events
This commit is contained in:
Seth Peterson
2025-12-04 11:28:16 -06:00
committed by GitHub
15 changed files with 285 additions and 26 deletions

View File

@@ -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

View File

@@ -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")
}
}

View File

@@ -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
@@ -2741,6 +2782,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):
@@ -3340,6 +3385,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`)"
@@ -3359,11 +3405,19 @@ SPEC REPOS:
trunk:
- AppAuth
- Firebase
- FirebaseABTesting
- FirebaseAnalytics
- FirebaseCore
- FirebaseCoreExtension
- FirebaseCoreInternal
- FirebaseInstallations
- FirebasePerformance
- FirebaseRemoteConfig
- FirebaseRemoteConfigInterop
- FirebaseSessions
- FirebaseSharedSwift
- GoogleAppMeasurement
- GoogleDataTransport
- GoogleSignIn
- GoogleUtilities
- GTMAppAuth
@@ -3371,6 +3425,7 @@ SPEC REPOS:
- Mute
- nanopb
- PromisesObjC
- PromisesSwift
- React-Codegen
- SocketRocket
@@ -3586,6 +3641,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:
@@ -3622,13 +3679,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
@@ -3637,6 +3702,7 @@ SPEC CHECKSUMS:
Mute: 20135a96076f140cc82bfc8b810e2d6150d8ec7e
nanopb: fad817b59e0457d11a5dfbde799381cd727c1275
PromisesObjC: f5707f49cb48b9636751c5b2e7d227e43fba9f47
PromisesSwift: 9d77319bbe72ebf6d872900551f7eeba9bce2851
RCT-Folly: 846fda9475e61ec7bcbf8a3fe81edfcaeb090669
RCTDeprecation: 300c5eb91114d4339b0bb39505d0f4824d7299b7
RCTRequired: e0446b01093475b7082fbeee5d1ef4ad1fe20ac4
@@ -3734,6 +3800,7 @@ SPEC CHECKSUMS:
RNDeviceInfo: d863506092aef7e7af3a1c350c913d867d795047
RNFBAnalytics: 2c4927af02d587595a7888a8275b7e3a2ccc1842
RNFBApp: 0211ae65fadcb017cc787b2bd539847330687b93
RNFBPerf: 3e7b00d193c37044790f92053fc013c12f385c75
RNFS: 89de7d7f4c0f6bafa05343c578f61118c8282ed8
RNGestureHandler: a52292df4f8fc7daab354ad2c37be401d4c3bed8
RNGoogleSignin: 65a7b18dd8fd9f279068ede103db4840f6782d99

16
package-lock.json generated
View File

@@ -24,6 +24,7 @@
"@react-native-community/slider": "^5.1.1",
"@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",
@@ -5495,6 +5496,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",

View File

@@ -60,6 +60,7 @@
"@react-native-community/slider": "^5.1.1",
"@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",

View File

@@ -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( "ai_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( () => {

View File

@@ -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_TRACE_ATTRIBUTES, 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;
@@ -171,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
@@ -191,7 +198,8 @@ const CameraContainer = ( ) => {
prepareStoreAndNavigate,
navigationOptions,
logStageIfAICamera,
deleteStageIfAICamera
deleteStageIfAICamera,
startFirebaseTrace
] );
const handleCheckmarkPress = useCallback( async (
@@ -199,7 +207,7 @@ const CameraContainer = ( ) => {
visionResult: StoredResult | null
) => {
if ( !showPhotoPermissionsGate ) {
await handleNavigation( newPhotoState, visionResult );
await handleNavigation( newPhotoState, true, visionResult );
} else {
await logStageIfAICamera( "request_save_photo_permission_start" );
requestSavePhotoPermission( );
@@ -355,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( {

View File

@@ -28,6 +28,7 @@ import {
useExitObservationFlow, useLocationPermission, useSuggestions, useWatchPosition
} from "sharedHooks";
import { isDebugMode } from "sharedHooks/useDebugMode";
import { FIREBASE_TRACE_ATTRIBUTES, FIREBASE_TRACES } from "stores/createFirebaseTraceSlice";
import useStore from "stores/useStore";
import tryToReplaceWithLocalTaxon from "./helpers/tryToReplaceWithLocalTaxon";
@@ -125,6 +126,8 @@ const MatchContainer = ( ) => {
shouldUseEvidenceLocation: evidenceHasLocation
} );
const stopFirebaseTrace = useStore( state => state.stopFirebaseTrace );
const {
scoreImageParams,
onlineFetchStatus,
@@ -426,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
stopFirebaseTrace(
FIREBASE_TRACES.AI_CAMERA_TO_MATCH,
{ [FIREBASE_TRACE_ATTRIBUTES.ONLINE]: `${!usingOfflineSuggestions}` }
);
}
}, [
onlineSuggestionsAttempted,
suggestionsLoading,
stopFirebaseTrace,
usingOfflineSuggestions
] );
// Remove the top suggestion from the list of other suggestions
const otherSuggestions = orderedSuggestions
.filter( suggestion => suggestion.taxon.id !== taxonId );

View File

@@ -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", { current_slide: currentIndex } );
setOnboardingShown( true );
};
const paginationColor = colors.white;
const backgroundAnimation1 = useAnimatedStyle( () => {
@@ -314,10 +318,19 @@ 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_continue_pressed",
{ current_slide: currentIndex }
);
const isLastSlide = carouselRef.current?.getCurrentIndex()
>= ONBOARDING_SLIDES.length - 1;
if ( isLastSlide ) {
closeModal();
} else {
carouselRef.current?.scrollTo( { count: 1, animated: true } );
}
}}
/>
</View>
</View>

View File

@@ -1,4 +1,3 @@
import { getAnalytics, logEvent } from "@react-native-firebase/analytics";
import { getCurrentRoute } from "navigation/navigationUtils";
import React from "react";
import type { GestureResponderEvent, PressableProps } from "react-native";
@@ -15,12 +14,6 @@ const PressableWithTracking = React.forwardRef<typeof Pressable, PressableProps>
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 ) {

View File

@@ -1,11 +1,11 @@
import {
useNetInfo
} from "@react-native-community/netinfo";
import { getAnalytics, logEvent } from "@react-native-firebase/analytics";
import { NavigationContainer } from "@react-navigation/native";
import type { PropsWithChildren } from "react";
import React, { useRef } from "react";
import { Alert } from "react-native";
import { logFirebaseScreenView } from "sharedHelpers/tracking";
import { useTranslation } from "sharedHooks";
import { navigationRef } from "./navigationUtils";
@@ -22,13 +22,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

View File

@@ -0,0 +1,34 @@
import {
getAnalytics, logEvent
} from "@react-native-firebase/analytics";
import { log } from "sharedHelpers/logger";
const logger = log.extend( "tracking.ts" );
type FirebaseParameters = Record<string, string | number | string[]>
export const logFirebaseEvent = (
eventId: string,
parameters?: FirebaseParameters
) => {
try {
const analytics = getAnalytics();
logEvent( analytics, eventId, parameters );
} catch ( error ) {
logger.error( "Error logging firebase event", JSON.stringify( error ) );
}
};
export const logFirebaseScreenView = (
screenName: string
) => {
try {
const analytics = getAnalytics();
logEvent( analytics, "screen_view", {
firebase_screen: screenName,
firebase_screen_class: screenName
} );
} catch ( error ) {
logger.error( "Error logging firebase screen view", JSON.stringify( error ) );
}
};

View File

@@ -0,0 +1,99 @@
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";
const logger = log.extend( "createFirebaseTraceSlice.ts" );
export enum FIREBASE_TRACES {
AI_CAMERA_TO_MATCH = "ai_camera_to_match",
}
export enum FIREBASE_TRACE_ATTRIBUTES {
ONLINE = "online",
DID_TIMEOUT = "did_timeout",
HAS_SAVE_PHOTO_PERMISSION = "has_save_photo_permission",
}
interface TraceData {
trace: FirebasePerformanceTypes.Trace;
timeoutId: ReturnType<typeof setTimeout>;
}
type FirebaseTraceAttributes = Partial<Record<FIREBASE_TRACE_ATTRIBUTES, string | number>>;
const TRACE_TIMEOUT = 10000;
const applyTraceAttributes = (
trace: FirebasePerformanceTypes.Trace,
attributes: FirebaseTraceAttributes
): 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}` );
}
} 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 ) {
logger.error( "Error setting firebase trace attributes", JSON.stringify( error ) );
}
};
export interface FirebaseTraceSlice {
activeFirebaseTraces: Record<string, TraceData>;
startFirebaseTrace: ( traceId: string, attributes: FirebaseTraceAttributes ) => Promise<void>;
stopFirebaseTrace: ( traceId: string, attributes: FirebaseTraceAttributes ) => Promise<void>;
}
const createFirebaseTraceSlice: StateCreator<FirebaseTraceSlice>
= ( set, get ) => ( {
activeFirebaseTraces: {},
startFirebaseTrace: async ( traceId: string, attributes: FirebaseTraceAttributes = {} ) => {
const perf = getPerformance();
const trace = await perf.startTrace( traceId );
const timeoutId = setTimeout( () => {
get().stopFirebaseTrace( traceId, { [FIREBASE_TRACE_ATTRIBUTES.DID_TIMEOUT]: "true" } );
}, TRACE_TIMEOUT );
applyTraceAttributes( trace, attributes );
set( state => ( {
activeFirebaseTraces: {
...state.activeFirebaseTraces,
[traceId]: { trace, timeoutId }
}
} ) );
},
stopFirebaseTrace: async ( traceId: string, attributes: FirebaseTraceAttributes = {} ) => {
const { activeFirebaseTraces } = get();
const traceData = activeFirebaseTraces[traceId];
if ( traceData ) {
clearTimeout( traceData.timeoutId );
const { trace } = traceData;
applyTraceAttributes( trace, attributes );
await trace.stop();
set( state => {
const { [traceId]: _, ...remainingTraces } = state.activeFirebaseTraces;
return { activeFirebaseTraces: remainingTraces };
} );
}
}
} );
export default createFirebaseTraceSlice;

View File

@@ -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 ),

View File

@@ -109,6 +109,11 @@ jest.mock( "@react-native-firebase/analytics", () => ( {
logEvent: jest.fn( )
} ) );
jest.mock( "@react-native-firebase/perf", () => ( {
startFirebaseTrace: jest.fn( ),
stopFirebaseTrace: 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