mirror of
https://github.com/inaturalist/iNaturalistReactNative.git
synced 2025-12-23 22:18:36 -05:00
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:
@@ -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
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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
16
package-lock.json
generated
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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( () => {
|
||||
|
||||
@@ -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( {
|
||||
|
||||
@@ -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 );
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 ) {
|
||||
|
||||
@@ -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
|
||||
|
||||
34
src/sharedHelpers/tracking.ts
Normal file
34
src/sharedHelpers/tracking.ts
Normal 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 ) );
|
||||
}
|
||||
};
|
||||
99
src/stores/createFirebaseTraceSlice.ts
Normal file
99
src/stores/createFirebaseTraceSlice.ts
Normal 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;
|
||||
@@ -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 ),
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user