From 21990acffa68ebcea2faf82bda76449f9b0cd569 Mon Sep 17 00:00:00 2001 From: Justin Wyne <1986068+wyne@users.noreply.github.com> Date: Wed, 17 Jul 2024 21:20:07 -0700 Subject: [PATCH 01/17] AppOpen game_list event --- redux/SettingsSlice.ts | 8 +++++++- redux/store.ts | 1 + src/screens/ListScreen.tsx | 28 ++++++++++++++++++---------- 3 files changed, 26 insertions(+), 11 deletions(-) diff --git a/redux/SettingsSlice.ts b/redux/SettingsSlice.ts index 4ee3d073..923ed28a 100644 --- a/redux/SettingsSlice.ts +++ b/redux/SettingsSlice.ts @@ -19,6 +19,7 @@ export interface SettingsState { lastStoreReviewPrompt: number; devMenuEnabled?: boolean; appOpens: number; + installId: string | undefined; }; const initialState: SettingsState = { @@ -34,6 +35,7 @@ const initialState: SettingsState = { interactionType: InteractionType.SwipeVertical, lastStoreReviewPrompt: 0, appOpens: 0, + installId: undefined, }; const settingsSlice = createSlice({ @@ -81,7 +83,10 @@ const settingsSlice = createSlice({ }, increaseAppOpens(state) { state.appOpens += 1; - } + }, + setInstallId(state, action: PayloadAction) { + state.installId = action.payload; + }, } }); @@ -99,6 +104,7 @@ export const { setLastStoreReviewPrompt, toggleDevMenuEnabled, increaseAppOpens, + setInstallId, } = settingsSlice.actions; export default settingsSlice.reducer; diff --git a/redux/store.ts b/redux/store.ts index 951d3d70..05bfa9d7 100644 --- a/redux/store.ts +++ b/redux/store.ts @@ -24,6 +24,7 @@ const settingsPersistConfig = { 'lastStoreReviewPrompt', 'devMenuEnabled', 'appOpens', + 'installId', ], }; diff --git a/src/screens/ListScreen.tsx b/src/screens/ListScreen.tsx index 75095eec..cfc9ba50 100644 --- a/src/screens/ListScreen.tsx +++ b/src/screens/ListScreen.tsx @@ -5,14 +5,15 @@ import { ParamListBase } from '@react-navigation/native'; import { NativeStackNavigationProp } from '@react-navigation/native-stack'; import * as Application from 'expo-application'; import { BlurView } from 'expo-blur'; +import * as Crypto from 'expo-crypto'; import { StyleSheet, Text, View } from 'react-native'; import Animated, { Easing, LinearTransition } from 'react-native-reanimated'; import { SafeAreaView } from 'react-native-safe-area-context'; -import { SemVer, parse } from 'semver'; +import { parse, SemVer } from 'semver'; import { selectGameIds } from '../../redux/GamesSlice'; import { useAppDispatch, useAppSelector } from '../../redux/hooks'; -import { increaseAppOpens, setOnboardedVersion } from '../../redux/SettingsSlice'; +import { increaseAppOpens, setInstallId, setOnboardedVersion } from '../../redux/SettingsSlice'; import GameListItem from '../components/GameListItem'; import { getPendingOnboardingSemVer } from '../components/Onboarding/Onboarding'; import logger from '../Logger'; @@ -24,24 +25,30 @@ interface Props { const ListScreen: React.FunctionComponent = ({ navigation }) => { const appOpens = useAppSelector(state => state.settings.appOpens); const devMenuEnabled = useAppSelector(state => state.settings.devMenuEnabled); + const installId = useAppSelector(state => state.settings.installId); const gameIds = useAppSelector(state => selectGameIds(state)); const dispatch = useAppDispatch(); const onboardedStr = useAppSelector(state => state.settings.onboarded); const onboardedSemVer = parse(onboardedStr); const appVersion = new SemVer(Application.nativeApplicationVersion || '0.0.0'); - - logger.info(`App Version: ${appVersion}`); - logger.info(`Dev Menu Enabled: ${devMenuEnabled}`); - logger.info(`Onboarded Version: ${onboardedSemVer}`); - const pendingOnboardingVer = getPendingOnboardingSemVer(onboardedSemVer); const onboarded = pendingOnboardingVer === undefined; - logger.info(`Pending onboard: ${pendingOnboardingVer}`); - logger.info(`Onboarded: ${onboarded}`); - useEffect(() => { + logger.info(`App Version: ${appVersion}`); + logger.info(`Install ID: ${installId}`); + logger.info(`Dev Menu Enabled: ${devMenuEnabled}`); + logger.info(`Onboarded Version: ${onboardedSemVer}`); + logger.info(`Pending onboard: ${pendingOnboardingVer}`); + logger.info(`Onboarded: ${onboarded}`); + + if (installId === undefined) { + console.log('no install id'); + const installId = Crypto.randomUUID(); + dispatch(setInstallId(installId)); + } + analytics().logEvent('game_list', { gameCount: gameIds.length, appOpens: appOpens, @@ -49,6 +56,7 @@ const ListScreen: React.FunctionComponent = ({ navigation }) => { devMenuEnabled: devMenuEnabled, onboardedVersion: onboardedSemVer?.version, pendingOnboardingVersion: pendingOnboardingVer, + installId: installId, }); logger.info('App Opens: ', appOpens); From a8a19c21fc2e3ef51bc28dc118c412d3d62b0e6b Mon Sep 17 00:00:00 2001 From: Justin Wyne <1986068+wyne@users.noreply.github.com> Date: Wed, 17 Jul 2024 22:10:24 -0700 Subject: [PATCH 02/17] Add analytics wrapper --- __mocks__/Analytics.ts | 1 + redux/GamesSlice.ts | 6 +++--- src/Analytics.ts | 17 +++++++++++++++++ src/components/AppInfo/RotatingIcon.tsx | 4 ++-- src/components/Buttons/AppInfoButton.test.tsx | 8 +++++--- src/components/Buttons/AppInfoButton.tsx | 4 ++-- src/components/Buttons/BackButton.tsx | 4 ++-- src/components/Buttons/CheckButton.test.tsx | 8 +++++--- src/components/Buttons/CheckButton.tsx | 4 ++-- src/components/Buttons/FullscreenButton.tsx | 4 ++-- src/components/Buttons/HomeButton.tsx | 6 +++--- src/components/GameListItem.tsx | 4 ++-- src/components/Headers/EditPlayerHeader.tsx | 4 ++-- src/components/Headers/GameHeader.tsx | 6 +++--- .../HalfTap/HalfTileTouchSurface.tsx | 4 ++-- src/components/PlayerListItem.tsx | 4 ++-- src/components/PopupMenu/AbstractPopupMenu.tsx | 8 ++++---- src/components/ScoreLog/RoundScoreColumn.tsx | 4 ++-- src/components/Sheets/AddendModal.tsx | 6 +++--- src/screens/AppInfoScreen.tsx | 4 ++-- src/screens/EditPlayerScreen.tsx | 4 ++-- src/screens/ListScreen.tsx | 6 +++--- src/screens/SettingsScreen.tsx | 4 ++-- src/screens/ShareScreen.tsx | 4 ++-- 24 files changed, 75 insertions(+), 53 deletions(-) create mode 100644 __mocks__/Analytics.ts create mode 100644 src/Analytics.ts diff --git a/__mocks__/Analytics.ts b/__mocks__/Analytics.ts new file mode 100644 index 00000000..4d41f932 --- /dev/null +++ b/__mocks__/Analytics.ts @@ -0,0 +1 @@ +export const logEvent = jest.fn(); diff --git a/redux/GamesSlice.ts b/redux/GamesSlice.ts index 4cc348bc..0e4c4c42 100644 --- a/redux/GamesSlice.ts +++ b/redux/GamesSlice.ts @@ -1,8 +1,8 @@ -import analytics from '@react-native-firebase/analytics'; import { PayloadAction, createAsyncThunk, createEntityAdapter, createSelector, createSlice } from '@reduxjs/toolkit'; import { getContrastRatio } from 'colorsheet'; import * as Crypto from 'expo-crypto'; +import { logEvent } from '../src/Analytics'; import { getPalette } from '../src/ColorPalette'; import { SortDirectionKey, SortSelectorKey } from '../src/components/ScoreLog/SortHelper'; import logger from '../src/Logger'; @@ -153,7 +153,7 @@ export const asyncRematchGame = createAsyncThunk( dispatch(setCurrentGameId(newGameId)); - await analytics().logEvent('rematch_game', { + await logEvent('rematch_game', { gameId: game.id, }); @@ -226,7 +226,7 @@ export const asyncCreateGame = createAsyncThunk( dispatch(setCurrentGameId(newGameId)); - await analytics().logEvent('new_game', { + await logEvent('new_game', { index: gameCount, }); diff --git a/src/Analytics.ts b/src/Analytics.ts new file mode 100644 index 00000000..4c4675bd --- /dev/null +++ b/src/Analytics.ts @@ -0,0 +1,17 @@ +import analytics from '@react-native-firebase/analytics'; + +import logger from './Logger'; + +/** + * Logs an event to Firebase Analytics with additional logging. + * @param eventName The name of the event to log. + * @param params The parameters of the event. + */ +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export const logEvent = async (eventName: string, params?: Record) => { + // Additional logging logic here (e.g., console.log for debugging) + logger.log(`Logging event: ${eventName}`, params); + + // Log the event to Firebase Analytics + await analytics().logEvent(eventName, params); +}; diff --git a/src/components/AppInfo/RotatingIcon.tsx b/src/components/AppInfo/RotatingIcon.tsx index 615bbbfa..cebcf03d 100644 --- a/src/components/AppInfo/RotatingIcon.tsx +++ b/src/components/AppInfo/RotatingIcon.tsx @@ -1,6 +1,5 @@ import React from 'react'; -import analytics from '@react-native-firebase/analytics'; import * as Haptics from 'expo-haptics'; import { Image } from 'expo-image'; import { TouchableWithoutFeedback } from 'react-native'; @@ -13,6 +12,7 @@ import Animated, { import { useAppDispatch } from '../../../redux/hooks'; import { toggleDevMenuEnabled } from '../../../redux/SettingsSlice'; +import { logEvent } from '../../Analytics'; const RotatingIcon: React.FunctionComponent = ({ }) => { const dispatch = useAppDispatch(); @@ -48,7 +48,7 @@ const RotatingIcon: React.FunctionComponent = ({ }) => { rotationCount.value = rotationCount.value + 1; rotation.value = withTiming((rotationCount.value * 90), { duration: 1000, easing: Easing.elastic(1) }); - await analytics().logEvent('app_icon'); + await logEvent('app_icon'); }}> { const navigation = useNavigationMock(); @@ -22,7 +24,7 @@ describe('AppInfoButton', () => { fireEvent.press(button); await waitFor(() => { - expect(analytics().logEvent).toHaveBeenCalledWith('app_info'); + expect(logEvent).toHaveBeenCalledWith('app_info'); }); }); }); diff --git a/src/components/Buttons/AppInfoButton.tsx b/src/components/Buttons/AppInfoButton.tsx index e9c1cb72..5e30a326 100644 --- a/src/components/Buttons/AppInfoButton.tsx +++ b/src/components/Buttons/AppInfoButton.tsx @@ -1,10 +1,10 @@ import React from 'react'; -import analytics from '@react-native-firebase/analytics'; import { ParamListBase } from '@react-navigation/native'; import { NativeStackNavigationProp } from '@react-navigation/native-stack'; import { Icon } from 'react-native-elements/dist/icons/Icon'; +import { logEvent } from '../../Analytics'; import { systemBlue } from '../../constants'; import HeaderButton from './HeaderButton'; @@ -17,7 +17,7 @@ const AppInfoButton: React.FunctionComponent = ({ navigation }) => { return ( { navigation.navigate('AppInfo'); - await analytics().logEvent('app_info'); + await logEvent('app_info'); }}> = ({ navigation }) => { return ( { navigation.goBack(); - await analytics().logEvent('menu'); + await logEvent('menu'); }}> { const navigation = useNavigationMock(); @@ -33,7 +35,7 @@ describe('CheckButton', () => { fireEvent.press(button); await waitFor(() => { - expect(analytics().logEvent).toHaveBeenCalledWith('save_game'); + expect(logEvent).toHaveBeenCalledWith('save_game'); }); }); }); diff --git a/src/components/Buttons/CheckButton.tsx b/src/components/Buttons/CheckButton.tsx index 1dc66100..a37acd32 100644 --- a/src/components/Buttons/CheckButton.tsx +++ b/src/components/Buttons/CheckButton.tsx @@ -1,10 +1,10 @@ import React from 'react'; -import analytics from '@react-native-firebase/analytics'; import { ParamListBase, RouteProp } from '@react-navigation/native'; import { NativeStackNavigationProp } from '@react-navigation/native-stack'; import { Text } from 'react-native'; +import { logEvent } from '../../Analytics'; import { systemBlue } from '../../constants'; import HeaderButton from './HeaderButton'; @@ -23,7 +23,7 @@ const CheckButton: React.FunctionComponent = ({ navigation, route }) => { return ( { - await analytics().logEvent('save_game'); + await logEvent('save_game'); if (route?.params?.source === 'list_screen') { navigation.navigate('List'); } else { diff --git a/src/components/Buttons/FullscreenButton.tsx b/src/components/Buttons/FullscreenButton.tsx index 6c483f4b..18bc4948 100644 --- a/src/components/Buttons/FullscreenButton.tsx +++ b/src/components/Buttons/FullscreenButton.tsx @@ -1,10 +1,10 @@ import React from 'react'; -import analytics from '@react-native-firebase/analytics'; import { Icon } from 'react-native-elements/dist/icons/Icon'; import { useAppDispatch, useAppSelector } from '../../../redux/hooks'; import { toggleHomeFullscreen } from '../../../redux/SettingsSlice'; +import { logEvent } from '../../Analytics'; import { systemBlue } from '../../constants'; import HeaderButton from './HeaderButton'; @@ -15,7 +15,7 @@ const FullscreenButton: React.FunctionComponent = ({ }) => { const expandHandler = async () => { dispatch(toggleHomeFullscreen()); - await analytics().logEvent('fullscreen'); + await logEvent('fullscreen'); }; return ( diff --git a/src/components/Buttons/HomeButton.tsx b/src/components/Buttons/HomeButton.tsx index b40a8430..42b2954f 100644 --- a/src/components/Buttons/HomeButton.tsx +++ b/src/components/Buttons/HomeButton.tsx @@ -1,6 +1,5 @@ import React from 'react'; -import analytics from '@react-native-firebase/analytics'; import { ParamListBase } from '@react-navigation/native'; import { NativeStackNavigationProp } from '@react-navigation/native-stack'; import * as StoreReview from 'expo-store-review'; @@ -10,6 +9,7 @@ import { Icon } from 'react-native-elements/dist/icons/Icon'; import { useAppDispatch, useAppSelector } from '../../../redux/hooks'; import { selectLastStoreReviewPrompt } from '../../../redux/selectors'; import { setLastStoreReviewPrompt } from '../../../redux/SettingsSlice'; +import { logEvent } from '../../Analytics'; import { systemBlue } from '../../constants'; import HeaderButton from './HeaderButton'; @@ -30,7 +30,7 @@ const HomeButton: React.FunctionComponent = ({ navigation }) => { if (gameCount < 3) { return; } if (daysSinceLastPrompt < 90) { return; } - await analytics().logEvent('review_prompt'); + await logEvent('review_prompt'); dispatch(setLastStoreReviewPrompt(Date.now())); @@ -49,7 +49,7 @@ const HomeButton: React.FunctionComponent = ({ navigation }) => { return ( { navigation.navigate('List'); - await analytics().logEvent('menu'); + await logEvent('menu'); storePrompt(); }}> diff --git a/src/components/GameListItem.tsx b/src/components/GameListItem.tsx index 0ee85626..d1d3b53b 100644 --- a/src/components/GameListItem.tsx +++ b/src/components/GameListItem.tsx @@ -1,6 +1,5 @@ import React, { useCallback } from 'react'; -import analytics from '@react-native-firebase/analytics'; import { ParamListBase } from '@react-navigation/native'; import { NativeStackNavigationProp } from '@react-navigation/native-stack'; import * as Haptics from 'expo-haptics'; @@ -12,6 +11,7 @@ import Animated, { FadeInUp, SlideOutLeft } from 'react-native-reanimated'; import { selectGameById } from '../../redux/GamesSlice'; import { useAppDispatch, useAppSelector } from '../../redux/hooks'; import { setCurrentGameId } from '../../redux/SettingsSlice'; +import { logEvent } from '../Analytics'; import GameListItemPlayerName from './GameListItemPlayerName'; import AbstractPopupMenu from './PopupMenu/AbstractPopupMenu'; @@ -45,7 +45,7 @@ const GameListItem: React.FunctionComponent = ({ navigation, gameId, inde setCurrentGameCallback(); navigation.navigate('Game'); - await analytics().logEvent('select_game', { + await logEvent('select_game', { index: index, game_id: gameId, player_count: playerIds.length, diff --git a/src/components/Headers/EditPlayerHeader.tsx b/src/components/Headers/EditPlayerHeader.tsx index 45dc738f..eb3f6cab 100644 --- a/src/components/Headers/EditPlayerHeader.tsx +++ b/src/components/Headers/EditPlayerHeader.tsx @@ -1,10 +1,10 @@ import React from 'react'; -import { getAnalytics } from '@react-native-firebase/analytics'; import { ParamListBase, RouteProp } from '@react-navigation/native'; import { NativeStackNavigationProp } from '@react-navigation/native-stack'; import { StyleSheet, Text } from 'react-native'; +import { logEvent } from '../../Analytics'; import { systemBlue } from '../../constants'; import HeaderButton from '../Buttons/HeaderButton'; @@ -28,7 +28,7 @@ const EditPlayerHeader: React.FunctionComponent = ({ navi headerLeft={ { navigation.goBack(); - await getAnalytics().logEvent('edit_player_back'); + await logEvent('edit_player_back'); }}> Back diff --git a/src/components/Headers/GameHeader.tsx b/src/components/Headers/GameHeader.tsx index 7b141908..4dec4d66 100644 --- a/src/components/Headers/GameHeader.tsx +++ b/src/components/Headers/GameHeader.tsx @@ -1,6 +1,5 @@ import React from 'react'; -import analytics from '@react-native-firebase/analytics'; import { ParamListBase } from '@react-navigation/native'; import { NativeStackNavigationProp } from '@react-navigation/native-stack'; import * as Haptics from 'expo-haptics'; @@ -10,6 +9,7 @@ import { Icon } from 'react-native-elements/dist/icons/Icon'; import { roundNext, roundPrevious, selectGameById } from '../../../redux/GamesSlice'; import { useAppDispatch, useAppSelector } from '../../../redux/hooks'; +import { logEvent } from '../../Analytics'; import { systemBlue } from '../../constants'; import AddendButton from '../Buttons/AddendButton'; import FullscreenButton from '../Buttons/FullscreenButton'; @@ -92,7 +92,7 @@ const GameHeader: React.FunctionComponent = ({ navigation }) => { } dispatch(roundNext(currentGame.id)); - analytics().logEvent('round_change', { + logEvent('round_change', { game_id: currentGameId, source: 'next button', round: roundCurrent, @@ -105,7 +105,7 @@ const GameHeader: React.FunctionComponent = ({ navigation }) => { Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Medium); dispatch(roundPrevious(currentGame.id)); - analytics().logEvent('round_change', { + logEvent('round_change', { game_id: currentGameId, source: 'previous button', round: roundCurrent, diff --git a/src/components/Interactions/HalfTap/HalfTileTouchSurface.tsx b/src/components/Interactions/HalfTap/HalfTileTouchSurface.tsx index 9a232a77..2317b908 100644 --- a/src/components/Interactions/HalfTap/HalfTileTouchSurface.tsx +++ b/src/components/Interactions/HalfTap/HalfTileTouchSurface.tsx @@ -1,12 +1,12 @@ import React, { useState } from 'react'; -import analytics from '@react-native-firebase/analytics'; import * as Haptics from 'expo-haptics'; import { StyleSheet, TouchableHighlight, View } from 'react-native'; import { useAppDispatch, useAppSelector } from '../../../../redux/hooks'; import { playerRoundScoreIncrement } from '../../../../redux/PlayersSlice'; import { selectCurrentGame } from '../../../../redux/selectors'; +import { logEvent } from '../../../Analytics'; import { ScoreParticle } from '../../PlayerTiles/AdditionTile/ScoreParticle'; type ScoreParticleProps = { @@ -55,7 +55,7 @@ export const HalfTileTouchSurface: React.FunctionComponent = ( addParticle(addend); } Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light); - analytics().logEvent('score_change', { + logEvent('score_change', { player_index: playerIndex, game_id: currentGameId, addend: addend, diff --git a/src/components/PlayerListItem.tsx b/src/components/PlayerListItem.tsx index 931fab8e..97b8b3ce 100644 --- a/src/components/PlayerListItem.tsx +++ b/src/components/PlayerListItem.tsx @@ -1,6 +1,5 @@ import React from 'react'; -import analytics from '@react-native-firebase/analytics'; import { ParamListBase } from '@react-navigation/native'; import { NativeStackNavigationProp } from '@react-navigation/native-stack'; import { Alert, StyleSheet, Text, TouchableOpacity, View } from 'react-native'; @@ -10,6 +9,7 @@ import { makeSelectPlayerColors, selectGameById, updateGame } from '../../redux/ import { useAppDispatch, useAppSelector } from '../../redux/hooks'; import { removePlayer, selectPlayerById } from '../../redux/PlayersSlice'; import { selectCurrentGame } from '../../redux/selectors'; +import { logEvent } from '../Analytics'; interface Props { playerId: string; @@ -62,7 +62,7 @@ const PlayerListItem: React.FunctionComponent = ({ const deleteHandler = async () => { removePlayerHandler(); - await analytics().logEvent('remove_player', { + await logEvent('remove_player', { game_id: currentGameId, player_index: index, }); diff --git a/src/components/PopupMenu/AbstractPopupMenu.tsx b/src/components/PopupMenu/AbstractPopupMenu.tsx index 7d7279cb..766a17a8 100644 --- a/src/components/PopupMenu/AbstractPopupMenu.tsx +++ b/src/components/PopupMenu/AbstractPopupMenu.tsx @@ -1,12 +1,12 @@ import React from 'react'; -import analytics from '@react-native-firebase/analytics'; import { ParamListBase } from '@react-navigation/native'; import { NativeStackNavigationProp } from '@react-navigation/native-stack'; import { Alert, Platform } from 'react-native'; import { asyncRematchGame, gameDelete, selectGameById } from '../../../redux/GamesSlice'; import { useAppDispatch, useAppSelector } from '../../../redux/hooks'; +import { logEvent } from '../../Analytics'; import AndroidPopupMenu from './AndroidPopupMenu'; import IOSPopupMenu from './IOSPopupMenu'; @@ -36,7 +36,7 @@ const AbstractPopupMenu: React.FC = (props) => { props.setCurrentGameCallback(); props.navigation.navigate('Share'); - await analytics().logEvent('menu_share', { + await logEvent('menu_share', { round_count: roundTotal, player_count: playerIds.length, }); @@ -49,7 +49,7 @@ const AbstractPopupMenu: React.FC = (props) => { props.setCurrentGameCallback(); props.navigation.navigate('Settings', { source: 'list_screen' }); - await analytics().logEvent('menu_edit', { + await logEvent('menu_edit', { round_count: roundTotal, player_count: playerIds.length, }); @@ -91,7 +91,7 @@ const AbstractPopupMenu: React.FC = (props) => { { cancelable: false }, ); - await analytics().logEvent('delete_game', { + await logEvent('delete_game', { index: props.index, round_count: roundTotal, player_count: playerIds.length, diff --git a/src/components/ScoreLog/RoundScoreColumn.tsx b/src/components/ScoreLog/RoundScoreColumn.tsx index 2a696e34..19312705 100644 --- a/src/components/ScoreLog/RoundScoreColumn.tsx +++ b/src/components/ScoreLog/RoundScoreColumn.tsx @@ -1,11 +1,11 @@ import React, { memo, useCallback } from 'react'; -import analytics from '@react-native-firebase/analytics'; import { LayoutChangeEvent, Text, TouchableWithoutFeedback, View } from 'react-native'; import { updateGame } from '../../../redux/GamesSlice'; import { useAppDispatch, useAppSelector } from '../../../redux/hooks'; import { selectCurrentGame } from '../../../redux/selectors'; +import { logEvent } from '../../Analytics'; import RoundScoreCell from './RoundScoreCell'; import { SortSelectorKey, sortSelectors } from './SortHelper'; @@ -39,7 +39,7 @@ const RoundScoreColumn: React.FunctionComponent = ({ roundCurrent: round, } })); - await analytics().logEvent('round_change', { + await logEvent('round_change', { game_id: currentGameId, source: 'direct select', }); diff --git a/src/components/Sheets/AddendModal.tsx b/src/components/Sheets/AddendModal.tsx index 143be820..4d9ad950 100644 --- a/src/components/Sheets/AddendModal.tsx +++ b/src/components/Sheets/AddendModal.tsx @@ -1,7 +1,6 @@ import React, { useCallback, useEffect, useMemo, useState } from 'react'; import { BottomSheetBackdrop, BottomSheetBackdropProps, BottomSheetModal, BottomSheetScrollView } from '@gorhom/bottom-sheet'; -import analytics from '@react-native-firebase/analytics'; import { Picker } from '@react-native-picker/picker'; import * as ScreenOrientation from 'expo-screen-orientation'; import { debounce } from 'lodash'; @@ -9,6 +8,7 @@ import { Platform, StyleSheet, Text, View } from 'react-native'; import { useAppDispatch, useAppSelector } from '../../../redux/hooks'; import { setAddendOne, setAddendTwo, setMultiplier } from '../../../redux/SettingsSlice'; +import { logEvent } from '../../Analytics'; import InteractionSelector from '../Interactions/InteractionSelector'; import { InteractionType } from '../Interactions/InteractionType'; @@ -58,7 +58,7 @@ const AddendModal: React.FunctionComponent = ({ }) => { const onTapValueChange = useCallback((itemValue: number, itemIndex: number) => { dispatch(setMultiplier(addendOptions[itemIndex])); dispatch(setAddendOne(addendOptions[itemIndex])); - analytics().logEvent('addend_two_change', { + logEvent('addend_two_change', { addendOne: itemValue, }); }, [addendOne]); @@ -67,7 +67,7 @@ const AddendModal: React.FunctionComponent = ({ }) => { const onLongTapValueChange = useCallback((itemValue: number) => { dispatch(setAddendTwo(itemValue)); - analytics().logEvent('addend_two_change', { + logEvent('addend_two_change', { addendTwo: itemValue, }); }, [addendTwo]); diff --git a/src/screens/AppInfoScreen.tsx b/src/screens/AppInfoScreen.tsx index 59d9b0ce..fde24fc8 100644 --- a/src/screens/AppInfoScreen.tsx +++ b/src/screens/AppInfoScreen.tsx @@ -1,6 +1,5 @@ import React from 'react'; -import analytics from '@react-native-firebase/analytics'; import { NativeStackNavigationProp } from '@react-navigation/native-stack'; import { ParamListBase } from '@react-navigation/routers'; import * as Application from 'expo-application'; @@ -9,6 +8,7 @@ import { Button } from 'react-native-elements'; import { useAppDispatch, useAppSelector } from '../../redux/hooks'; import { toggleShowColorPalettes, toggleShowPlayerIndex, toggleShowPointParticles } from '../../redux/SettingsSlice'; +import { logEvent } from '../Analytics'; import RotatingIcon from '../components/AppInfo/RotatingIcon'; interface Props { @@ -55,7 +55,7 @@ const AppInfoScreen: React.FunctionComponent = ({ navigation }) => { `${Platform.OS} ${Platform.Version}\n` + (process.env.EXPO_PUBLIC_FIREBASE_ANALYTICS) ); - await analytics().logEvent('view_version'); + await logEvent('view_version'); }; return ( diff --git a/src/screens/EditPlayerScreen.tsx b/src/screens/EditPlayerScreen.tsx index 91d3cfa2..cacb4461 100644 --- a/src/screens/EditPlayerScreen.tsx +++ b/src/screens/EditPlayerScreen.tsx @@ -1,6 +1,5 @@ import React, { useState } from 'react'; -import analytics from '@react-native-firebase/analytics'; import { ParamListBase, RouteProp } from '@react-navigation/native'; import { NativeStackNavigationProp } from '@react-navigation/native-stack'; import { NativeSyntheticEvent, ScrollView, StyleSheet, TextInput, TextInputEndEditingEventData, View } from 'react-native'; @@ -9,6 +8,7 @@ import { Input } from 'react-native-elements'; import { useAppDispatch, useAppSelector } from '../../redux/hooks'; import { updatePlayer } from '../../redux/PlayersSlice'; import { selectCurrentGame } from '../../redux/selectors'; +import { logEvent } from '../Analytics'; import ColorSelector from '../components/ColorPalettes/ColorSelector'; type RouteParams = { @@ -74,7 +74,7 @@ const EditPlayerScreen: React.FC = ({ } })); - analytics().logEvent('update_player', { + logEvent('update_player', { game_id: currentGame.id, player_index: index, }); diff --git a/src/screens/ListScreen.tsx b/src/screens/ListScreen.tsx index cfc9ba50..01c846d2 100644 --- a/src/screens/ListScreen.tsx +++ b/src/screens/ListScreen.tsx @@ -1,6 +1,5 @@ import React, { memo, useEffect } from 'react'; -import analytics from '@react-native-firebase/analytics'; import { ParamListBase } from '@react-navigation/native'; import { NativeStackNavigationProp } from '@react-navigation/native-stack'; import * as Application from 'expo-application'; @@ -9,11 +8,12 @@ import * as Crypto from 'expo-crypto'; import { StyleSheet, Text, View } from 'react-native'; import Animated, { Easing, LinearTransition } from 'react-native-reanimated'; import { SafeAreaView } from 'react-native-safe-area-context'; -import { parse, SemVer } from 'semver'; +import { SemVer, parse } from 'semver'; import { selectGameIds } from '../../redux/GamesSlice'; import { useAppDispatch, useAppSelector } from '../../redux/hooks'; import { increaseAppOpens, setInstallId, setOnboardedVersion } from '../../redux/SettingsSlice'; +import { logEvent } from '../Analytics'; import GameListItem from '../components/GameListItem'; import { getPendingOnboardingSemVer } from '../components/Onboarding/Onboarding'; import logger from '../Logger'; @@ -49,7 +49,7 @@ const ListScreen: React.FunctionComponent = ({ navigation }) => { dispatch(setInstallId(installId)); } - analytics().logEvent('game_list', { + logEvent('game_list', { gameCount: gameIds.length, appOpens: appOpens, appVersion: appVersion.version, diff --git a/src/screens/SettingsScreen.tsx b/src/screens/SettingsScreen.tsx index 25473ff4..2408c023 100644 --- a/src/screens/SettingsScreen.tsx +++ b/src/screens/SettingsScreen.tsx @@ -1,6 +1,5 @@ import React, { useEffect } from 'react'; -import analytics from '@react-native-firebase/analytics'; import { ParamListBase, RouteProp } from '@react-navigation/native'; import { NativeStackNavigationProp } from '@react-navigation/native-stack'; import { StyleSheet, Text, TouchableOpacity, View } from 'react-native'; @@ -10,6 +9,7 @@ import { Button, Icon } from 'react-native-elements'; import { addPlayer, reorderPlayers } from '../../redux/GamesSlice'; import { useAppDispatch, useAppSelector } from '../../redux/hooks'; import { selectCurrentGame } from '../../redux/selectors'; +import { logEvent } from '../Analytics'; import EditGame from '../components/EditGame'; import PlayerListItem from '../components/PlayerListItem'; import { MAX_PLAYERS, systemBlue } from '../constants'; @@ -45,7 +45,7 @@ const SettingsScreen: React.FunctionComponent = ({ navigation }) => { playerName: `Player ${playerIds.length + 1}`, })); - await analytics().logEvent('add_player', { + await logEvent('add_player', { game_id: currentGameId, player_count: playerIds.length + 1, }); diff --git a/src/screens/ShareScreen.tsx b/src/screens/ShareScreen.tsx index 82e0757c..3d6cbbcc 100644 --- a/src/screens/ShareScreen.tsx +++ b/src/screens/ShareScreen.tsx @@ -1,6 +1,5 @@ import React, { useRef } from 'react'; -import analytics from '@react-native-firebase/analytics'; import { ParamListBase } from '@react-navigation/native'; import { NativeStackNavigationProp } from '@react-navigation/native-stack'; import * as Sharing from 'expo-sharing'; @@ -12,6 +11,7 @@ import { captureRef } from 'react-native-view-shot'; import { selectGameById } from '../../redux/GamesSlice'; import { useAppSelector } from '../../redux/hooks'; import { selectCurrentGame } from '../../redux/selectors'; +import { logEvent } from '../Analytics'; import PlayerNameColumn from '../components/ScoreLog/PlayerNameColumn'; import RoundScoreColumn from '../components/ScoreLog/RoundScoreColumn'; import TotalScoreColumn from '../components/ScoreLog/TotalScoreColumn'; @@ -50,7 +50,7 @@ const ShareScreen: React.FunctionComponent = ({ navigation }) => { }); }); - await analytics().logEvent('share_image'); + await logEvent('share_image'); }; return ( From 59117a698aac57019eef34b0ea897edd901c9bfd Mon Sep 17 00:00:00 2001 From: Justin Wyne <1986068+wyne@users.noreply.github.com> Date: Thu, 18 Jul 2024 01:02:42 -0700 Subject: [PATCH 03/17] Log analytics events in color --- src/Analytics.ts | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/Analytics.ts b/src/Analytics.ts index 4c4675bd..3da97e34 100644 --- a/src/Analytics.ts +++ b/src/Analytics.ts @@ -10,7 +10,13 @@ import logger from './Logger'; // eslint-disable-next-line @typescript-eslint/no-explicit-any export const logEvent = async (eventName: string, params?: Record) => { // Additional logging logic here (e.g., console.log for debugging) - logger.log(`Logging event: ${eventName}`, params); + logger.info( + '\x1b[34m', // Set the color to blue + 'EVENT', + eventName, + JSON.stringify(params, null, 2), + '\x1b[0m' // Reset the color + ); // Log the event to Firebase Analytics await analytics().logEvent(eventName, params); From ecc7a7980cd262f567d98dd3fe125e99f53255a4 Mon Sep 17 00:00:00 2001 From: Justin Wyne <1986068+wyne@users.noreply.github.com> Date: Thu, 18 Jul 2024 01:03:03 -0700 Subject: [PATCH 04/17] Log onboarding events --- src/screens/ListScreen.tsx | 6 +---- src/screens/OnboardingScreen.tsx | 43 +++++++++++++++++++++++++++++--- 2 files changed, 41 insertions(+), 8 deletions(-) diff --git a/src/screens/ListScreen.tsx b/src/screens/ListScreen.tsx index 01c846d2..11a4e88b 100644 --- a/src/screens/ListScreen.tsx +++ b/src/screens/ListScreen.tsx @@ -36,11 +36,6 @@ const ListScreen: React.FunctionComponent = ({ navigation }) => { const onboarded = pendingOnboardingVer === undefined; useEffect(() => { - logger.info(`App Version: ${appVersion}`); - logger.info(`Install ID: ${installId}`); - logger.info(`Dev Menu Enabled: ${devMenuEnabled}`); - logger.info(`Onboarded Version: ${onboardedSemVer}`); - logger.info(`Pending onboard: ${pendingOnboardingVer}`); logger.info(`Onboarded: ${onboarded}`); if (installId === undefined) { @@ -50,6 +45,7 @@ const ListScreen: React.FunctionComponent = ({ navigation }) => { } logEvent('game_list', { + onboarded: onboarded, gameCount: gameIds.length, appOpens: appOpens, appVersion: appVersion.version, diff --git a/src/screens/OnboardingScreen.tsx b/src/screens/OnboardingScreen.tsx index ae2bfed8..4ca59b73 100644 --- a/src/screens/OnboardingScreen.tsx +++ b/src/screens/OnboardingScreen.tsx @@ -1,6 +1,6 @@ -import React from 'react'; +import React, { useCallback, useEffect, useRef } from 'react'; -import { RouteProp } from '@react-navigation/native'; +import { RouteProp, useFocusEffect } from '@react-navigation/native'; import { NativeStackNavigationProp } from '@react-navigation/native-stack'; import { NativeScrollEvent, @@ -13,6 +13,7 @@ import { ExpandingDot } from 'react-native-animated-pagination-dots'; import Animated, { FadeIn } from 'react-native-reanimated'; import { SemVer } from 'semver'; +import { logEvent } from '../Analytics'; import { getOnboardingScreens, OnboardingScreenItem } from '../components/Onboarding/Onboarding'; import OnboardingPage from '../components/Onboarding/OnboardingPage'; import SkipButton from '../components/Onboarding/SkipButton'; @@ -40,9 +41,17 @@ const OnboardingScreen: React.FunctionComponent = ({ navigation, route }) const [activeIndex, setActiveIndex] = React.useState(0); const closeOnboarding = React.useCallback(() => { + const end = activeIndex == onboardingScreens.length - 1; + const eventName = end ? 'onboarding_complete' : 'onboarding_skip'; + logEvent(eventName, { + onboarding: onboarding, + index: activeIndex, + end: end + }); + if (onboarding) navigation.navigate('List'); else navigation.goBack(); - }, []); + }, [activeIndex]); // Flatlist props that calculates current item index const onScrollEnd = (event: NativeSyntheticEvent) => { @@ -52,6 +61,34 @@ const OnboardingScreen: React.FunctionComponent = ({ navigation, route }) const viewConfigRef = React.useRef({ viewAreaCoveragePercentThreshold: 50 }); + useEffect(() => { + logEvent('onboarding_page', { + onboarding: onboarding, + index: activeIndex, + end: activeIndex == onboardingScreens.length - 1 + }); + }, [activeIndex]); + + const activeIndexRef = useRef(activeIndex); // Create a ref to store the current activeIndex + + useEffect(() => { + activeIndexRef.current = activeIndex; // Update the ref's value whenever activeIndex changes + }, [activeIndex]); + + useFocusEffect( + useCallback(() => { + // This function is called when the screen comes into focus + return () => { + // This function is called when the screen goes out of focus + logEvent('onboarding_close', { + onboarding: onboarding, + index: activeIndexRef.current, + end: activeIndexRef.current == onboardingScreens.length - 1 + }); + }; + }, [onboarding]) + ); + return ( { From 7124b4abdfd6de2a2bc35ddb5f58afe4415201f0 Mon Sep 17 00:00:00 2001 From: Justin Wyne <1986068+wyne@users.noreply.github.com> Date: Thu, 18 Jul 2024 01:07:18 -0700 Subject: [PATCH 05/17] Log DevMenu event --- src/components/AppInfo/RotatingIcon.tsx | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/components/AppInfo/RotatingIcon.tsx b/src/components/AppInfo/RotatingIcon.tsx index cebcf03d..a8b37405 100644 --- a/src/components/AppInfo/RotatingIcon.tsx +++ b/src/components/AppInfo/RotatingIcon.tsx @@ -10,13 +10,15 @@ import Animated, { withTiming } from 'react-native-reanimated'; -import { useAppDispatch } from '../../../redux/hooks'; +import { useAppDispatch, useAppSelector } from '../../../redux/hooks'; import { toggleDevMenuEnabled } from '../../../redux/SettingsSlice'; import { logEvent } from '../../Analytics'; const RotatingIcon: React.FunctionComponent = ({ }) => { const dispatch = useAppDispatch(); + const installId = useAppSelector(state => state.settings.installId); + const rotation = useSharedValue(0); const rotationCount = useSharedValue(1); const animatedStyles = useAnimatedStyle(() => { @@ -33,6 +35,9 @@ const RotatingIcon: React.FunctionComponent = ({ }) => { holdCallback = setTimeout(() => { dispatch(toggleDevMenuEnabled()); Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Heavy); + logEvent('dev_menu', { + installId: installId, + }); }, 5000); }; From 209a14175748e03f70e5a9ff39568d4a30c0dd03 Mon Sep 17 00:00:00 2001 From: Justin Wyne <1986068+wyne@users.noreply.github.com> Date: Thu, 18 Jul 2024 01:10:32 -0700 Subject: [PATCH 06/17] Game events --- redux/GamesSlice.ts | 1 + src/screens/SettingsScreen.tsx | 6 ++++-- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/redux/GamesSlice.ts b/redux/GamesSlice.ts index 0e4c4c42..72e7510d 100644 --- a/redux/GamesSlice.ts +++ b/redux/GamesSlice.ts @@ -228,6 +228,7 @@ export const asyncCreateGame = createAsyncThunk( await logEvent('new_game', { index: gameCount, + playerCount: playerCount, }); return newGameId; diff --git a/src/screens/SettingsScreen.tsx b/src/screens/SettingsScreen.tsx index 2408c023..7e909410 100644 --- a/src/screens/SettingsScreen.tsx +++ b/src/screens/SettingsScreen.tsx @@ -13,7 +13,6 @@ import { logEvent } from '../Analytics'; import EditGame from '../components/EditGame'; import PlayerListItem from '../components/PlayerListItem'; import { MAX_PLAYERS, systemBlue } from '../constants'; -import logger from '../Logger'; type RouteParams = { Settings: { @@ -112,7 +111,10 @@ const SettingsScreen: React.FunctionComponent = ({ navigation }) => { }) ); - logger.info('Reorder players'); + logEvent('reorder_players', { + game_id: currentGameId, + playerCount: data.length, + }); }} /> From 3ebab93e412eded91f51d47c0c1a6be4fcbc0e98 Mon Sep 17 00:00:00 2001 From: Justin Wyne <1986068+wyne@users.noreply.github.com> Date: Thu, 18 Jul 2024 01:18:20 -0700 Subject: [PATCH 07/17] More game events: lock, reset, edit --- src/components/Sheets/GameSheet.tsx | 34 +++++++++++++++++++++-------- 1 file changed, 25 insertions(+), 9 deletions(-) diff --git a/src/components/Sheets/GameSheet.tsx b/src/components/Sheets/GameSheet.tsx index 424091b3..a560fcc8 100644 --- a/src/components/Sheets/GameSheet.tsx +++ b/src/components/Sheets/GameSheet.tsx @@ -11,6 +11,7 @@ import { SafeAreaView } from 'react-native-safe-area-context'; import { asyncRematchGame, selectGameById, updateGame } from '../../../redux/GamesSlice'; import { useAppDispatch, useAppSelector } from '../../../redux/hooks'; import { updatePlayer } from '../../../redux/PlayersSlice'; +import { logEvent } from '../../Analytics'; import { systemBlue } from '../../constants'; import BigButton from '../BigButtons/BigButton'; import RematchIcon from '../Icons/RematchIcon'; @@ -50,14 +51,20 @@ const GameSheet: React.FunctionComponent = ({ navigation, containerHeight /** * Lock the game */ - const setLock = () => dispatch( - updateGame({ - id: currentGameId, - changes: { - locked: !gameLocked, - } - }) - ); + const setLock = () => { + dispatch( + updateGame({ + id: currentGameId, + changes: { + locked: !gameLocked, + } + }) + ); + logEvent('lock_game', { + game_id: currentGameId, + locked: !gameLocked + }); + }; /** * Reset the game, but keep the players @@ -94,6 +101,8 @@ const GameSheet: React.FunctionComponent = ({ navigation, containerHeight } })); navigation.navigate('Game'); + + logEvent('reset_game', { game_id: currentGameId }); } } ] @@ -243,7 +252,14 @@ const GameSheet: React.FunctionComponent = ({ navigation, containerHeight margin: 5, marginTop: 15, backgroundColor: 'rgba(0,0,0,.2)', borderRadius: 10 }} - onPress={() => navigation.navigate('Settings')} + onPress={() => { + + logEvent('edit_game', { + game_id: currentGameId + }); + navigation.navigate('Settings'); + } + } /> } From 3547d5b89f6512f426e916b0b02e91d98eb263c2 Mon Sep 17 00:00:00 2001 From: Justin Wyne <1986068+wyne@users.noreply.github.com> Date: Thu, 18 Jul 2024 01:23:08 -0700 Subject: [PATCH 08/17] Log sorting events --- src/components/Rounds.tsx | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/components/Rounds.tsx b/src/components/Rounds.tsx index e65448b1..8e22ee74 100644 --- a/src/components/Rounds.tsx +++ b/src/components/Rounds.tsx @@ -7,6 +7,7 @@ import { TouchableOpacity } from 'react-native-gesture-handler'; import { selectGameById, setSortSelector } from '../../redux/GamesSlice'; import { useAppDispatch, useAppSelector } from '../../redux/hooks'; +import { logEvent } from '../Analytics'; import PlayerNameColumn from './ScoreLog/PlayerNameColumn'; import RoundScoreColumn from './ScoreLog/RoundScoreColumn'; @@ -63,10 +64,12 @@ const Rounds: React.FunctionComponent = ({ }) => { const sortByPlayerIndex = () => { dispatch(setSortSelector({ gameId: currentGameId, sortSelector: SortSelectorKey.ByIndex })); + logEvent('sort_by_index', { gameId: currentGameId }); }; const sortByTotalScore = () => { dispatch(setSortSelector({ gameId: currentGameId, sortSelector: SortSelectorKey.ByScore })); + logEvent('sort_by_score', { gameId: currentGameId }); }; return ( From 5df8b12c7458c65b30705a6cd35fc658213e80a4 Mon Sep 17 00:00:00 2001 From: Justin Wyne <1986068+wyne@users.noreply.github.com> Date: Thu, 18 Jul 2024 01:27:57 -0700 Subject: [PATCH 09/17] Log fullscreen and more review data --- src/components/Buttons/FullscreenButton.tsx | 4 +++- src/components/Buttons/HomeButton.tsx | 7 ++++++- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/src/components/Buttons/FullscreenButton.tsx b/src/components/Buttons/FullscreenButton.tsx index 18bc4948..b224e84e 100644 --- a/src/components/Buttons/FullscreenButton.tsx +++ b/src/components/Buttons/FullscreenButton.tsx @@ -14,8 +14,10 @@ const FullscreenButton: React.FunctionComponent = ({ }) => { const fullscreen = useAppSelector(state => state.settings.home_fullscreen); const expandHandler = async () => { + await logEvent('fullscreen', { + fullscreen: !fullscreen + }); dispatch(toggleHomeFullscreen()); - await logEvent('fullscreen'); }; return ( diff --git a/src/components/Buttons/HomeButton.tsx b/src/components/Buttons/HomeButton.tsx index 42b2954f..2fab7489 100644 --- a/src/components/Buttons/HomeButton.tsx +++ b/src/components/Buttons/HomeButton.tsx @@ -20,6 +20,7 @@ interface Props { const HomeButton: React.FunctionComponent = ({ navigation }) => { const gameCount = useAppSelector((state) => state.games.ids.length); + const installId = useAppSelector((state) => state.settings.installId); const lastStoreReviewPrompt = useAppSelector(selectLastStoreReviewPrompt); const dispatch = useAppDispatch(); @@ -30,7 +31,11 @@ const HomeButton: React.FunctionComponent = ({ navigation }) => { if (gameCount < 3) { return; } if (daysSinceLastPrompt < 90) { return; } - await logEvent('review_prompt'); + await logEvent('review_prompt', { + daysSinceLastPrompt: daysSinceLastPrompt, + gameCount: gameCount, + installId: installId + }); dispatch(setLastStoreReviewPrompt(Date.now())); From d649e6eca53cc2323838f0399495ac886a65c971 Mon Sep 17 00:00:00 2001 From: Justin Wyne <1986068+wyne@users.noreply.github.com> Date: Thu, 18 Jul 2024 01:36:39 -0700 Subject: [PATCH 10/17] Log player edit button --- redux/GamesSlice.ts | 2 +- src/components/Headers/GameHeader.tsx | 3 +++ src/screens/SettingsScreen.tsx | 10 ++++++++-- 3 files changed, 12 insertions(+), 3 deletions(-) diff --git a/redux/GamesSlice.ts b/redux/GamesSlice.ts index 72e7510d..69fad963 100644 --- a/redux/GamesSlice.ts +++ b/redux/GamesSlice.ts @@ -228,7 +228,7 @@ export const asyncCreateGame = createAsyncThunk( await logEvent('new_game', { index: gameCount, - playerCount: playerCount, + player_count: playerCount, }); return newGameId; diff --git a/src/components/Headers/GameHeader.tsx b/src/components/Headers/GameHeader.tsx index 4dec4d66..a3d9c714 100644 --- a/src/components/Headers/GameHeader.tsx +++ b/src/components/Headers/GameHeader.tsx @@ -96,6 +96,8 @@ const GameHeader: React.FunctionComponent = ({ navigation }) => { game_id: currentGameId, source: 'next button', round: roundCurrent, + next_round: roundCurrent + 1, + new_round: isLastRound }); }; @@ -109,6 +111,7 @@ const GameHeader: React.FunctionComponent = ({ navigation }) => { game_id: currentGameId, source: 'previous button', round: roundCurrent, + next_round: roundCurrent - 1, }); }; diff --git a/src/screens/SettingsScreen.tsx b/src/screens/SettingsScreen.tsx index 7e909410..9a815ae1 100644 --- a/src/screens/SettingsScreen.tsx +++ b/src/screens/SettingsScreen.tsx @@ -78,7 +78,13 @@ const SettingsScreen: React.FunctionComponent = ({ navigation }) => { Players {playerIds.length > 1 && - setEdit(!edit)}> + { + setEdit(!edit); + logEvent('edit_players', { + game_id: currentGameId, + player_count: playerIds.length, + }); + }}> {edit ? 'Done' : 'Edit'} } @@ -113,7 +119,7 @@ const SettingsScreen: React.FunctionComponent = ({ navigation }) => { logEvent('reorder_players', { game_id: currentGameId, - playerCount: data.length, + player_count: data.length, }); }} /> From 731fff1019e94369f7142a5e087e25f4c34d427c Mon Sep 17 00:00:00 2001 From: Justin Wyne <1986068+wyne@users.noreply.github.com> Date: Thu, 18 Jul 2024 01:43:07 -0700 Subject: [PATCH 11/17] cleanup --- src/components/AppInfo/RotatingIcon.tsx | 2 +- src/components/Buttons/HomeButton.tsx | 6 +++--- src/screens/ListScreen.tsx | 8 ++++---- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/components/AppInfo/RotatingIcon.tsx b/src/components/AppInfo/RotatingIcon.tsx index a8b37405..4e49a07e 100644 --- a/src/components/AppInfo/RotatingIcon.tsx +++ b/src/components/AppInfo/RotatingIcon.tsx @@ -36,7 +36,7 @@ const RotatingIcon: React.FunctionComponent = ({ }) => { dispatch(toggleDevMenuEnabled()); Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Heavy); logEvent('dev_menu', { - installId: installId, + installId, }); }, 5000); }; diff --git a/src/components/Buttons/HomeButton.tsx b/src/components/Buttons/HomeButton.tsx index 2fab7489..3ad297a8 100644 --- a/src/components/Buttons/HomeButton.tsx +++ b/src/components/Buttons/HomeButton.tsx @@ -32,9 +32,9 @@ const HomeButton: React.FunctionComponent = ({ navigation }) => { if (daysSinceLastPrompt < 90) { return; } await logEvent('review_prompt', { - daysSinceLastPrompt: daysSinceLastPrompt, - gameCount: gameCount, - installId: installId + daysSinceLastPrompt, + gameCount, + installId }); dispatch(setLastStoreReviewPrompt(Date.now())); diff --git a/src/screens/ListScreen.tsx b/src/screens/ListScreen.tsx index 11a4e88b..a8c11590 100644 --- a/src/screens/ListScreen.tsx +++ b/src/screens/ListScreen.tsx @@ -45,14 +45,14 @@ const ListScreen: React.FunctionComponent = ({ navigation }) => { } logEvent('game_list', { - onboarded: onboarded, + onboarded, gameCount: gameIds.length, - appOpens: appOpens, + appOpens, appVersion: appVersion.version, - devMenuEnabled: devMenuEnabled, + devMenuEnabled, onboardedVersion: onboardedSemVer?.version, pendingOnboardingVersion: pendingOnboardingVer, - installId: installId, + installId, }); logger.info('App Opens: ', appOpens); From 1e20fe9887c7cf087f8416df46915d28479ccc39 Mon Sep 17 00:00:00 2001 From: Justin Wyne <1986068+wyne@users.noreply.github.com> Date: Thu, 18 Jul 2024 01:56:39 -0700 Subject: [PATCH 12/17] Rolling game counter --- redux/GamesSlice.ts | 3 ++- redux/SettingsSlice.ts | 6 ++++++ redux/store.ts | 1 + src/screens/ListScreen.tsx | 5 ++--- 4 files changed, 11 insertions(+), 4 deletions(-) diff --git a/redux/GamesSlice.ts b/redux/GamesSlice.ts index 69fad963..152114ef 100644 --- a/redux/GamesSlice.ts +++ b/redux/GamesSlice.ts @@ -8,7 +8,7 @@ import { SortDirectionKey, SortSelectorKey } from '../src/components/ScoreLog/So import logger from '../src/Logger'; import { playerAdd, selectPlayerById, updatePlayer } from './PlayersSlice'; -import { setCurrentGameId } from './SettingsSlice'; +import { incrementRollingGameCounter, setCurrentGameId } from './SettingsSlice'; import { RootState } from './store'; export interface GameState { @@ -225,6 +225,7 @@ export const asyncCreateGame = createAsyncThunk( })); dispatch(setCurrentGameId(newGameId)); + dispatch(incrementRollingGameCounter()); await logEvent('new_game', { index: gameCount, diff --git a/redux/SettingsSlice.ts b/redux/SettingsSlice.ts index 923ed28a..db175b2a 100644 --- a/redux/SettingsSlice.ts +++ b/redux/SettingsSlice.ts @@ -20,6 +20,7 @@ export interface SettingsState { devMenuEnabled?: boolean; appOpens: number; installId: string | undefined; + rollingGameCounter?: number; }; const initialState: SettingsState = { @@ -36,6 +37,7 @@ const initialState: SettingsState = { lastStoreReviewPrompt: 0, appOpens: 0, installId: undefined, + rollingGameCounter: 0, }; const settingsSlice = createSlice({ @@ -87,6 +89,9 @@ const settingsSlice = createSlice({ setInstallId(state, action: PayloadAction) { state.installId = action.payload; }, + incrementRollingGameCounter(state) { + state.rollingGameCounter = (state.rollingGameCounter ?? 0) + 1; + }, } }); @@ -105,6 +110,7 @@ export const { toggleDevMenuEnabled, increaseAppOpens, setInstallId, + incrementRollingGameCounter, } = settingsSlice.actions; export default settingsSlice.reducer; diff --git a/redux/store.ts b/redux/store.ts index 05bfa9d7..c2944842 100644 --- a/redux/store.ts +++ b/redux/store.ts @@ -25,6 +25,7 @@ const settingsPersistConfig = { 'devMenuEnabled', 'appOpens', 'installId', + 'rollingGameCounter', ], }; diff --git a/src/screens/ListScreen.tsx b/src/screens/ListScreen.tsx index a8c11590..17dc4665 100644 --- a/src/screens/ListScreen.tsx +++ b/src/screens/ListScreen.tsx @@ -34,10 +34,9 @@ const ListScreen: React.FunctionComponent = ({ navigation }) => { const appVersion = new SemVer(Application.nativeApplicationVersion || '0.0.0'); const pendingOnboardingVer = getPendingOnboardingSemVer(onboardedSemVer); const onboarded = pendingOnboardingVer === undefined; + const rollingGameCounter = useAppSelector(state => state.settings.rollingGameCounter); useEffect(() => { - logger.info(`Onboarded: ${onboarded}`); - if (installId === undefined) { console.log('no install id'); const installId = Crypto.randomUUID(); @@ -53,9 +52,9 @@ const ListScreen: React.FunctionComponent = ({ navigation }) => { onboardedVersion: onboardedSemVer?.version, pendingOnboardingVersion: pendingOnboardingVer, installId, + rollingGameCounter, }); - logger.info('App Opens: ', appOpens); dispatch(increaseAppOpens()); }, []); From a18f91b2e9e8d5da147a30540bd6a816ada7211b Mon Sep 17 00:00:00 2001 From: Justin Wyne <1986068+wyne@users.noreply.github.com> Date: Thu, 18 Jul 2024 17:25:18 -0700 Subject: [PATCH 13/17] Adjust powerHold timing, update notch value in logEvent --- src/components/Interactions/Swipe/Swipe.tsx | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/components/Interactions/Swipe/Swipe.tsx b/src/components/Interactions/Swipe/Swipe.tsx index 261a9492..a5287ece 100644 --- a/src/components/Interactions/Swipe/Swipe.tsx +++ b/src/components/Interactions/Swipe/Swipe.tsx @@ -1,6 +1,5 @@ import React, { useEffect, useRef, useState } from 'react'; -import analytics from '@react-native-firebase/analytics'; import * as Haptics from 'expo-haptics'; import { Animated, StyleSheet, View } from 'react-native'; import { PanGestureHandler, PanGestureHandlerStateChangeEvent, State } from 'react-native-gesture-handler'; @@ -9,6 +8,7 @@ import { runOnJS, useAnimatedReaction, useSharedValue } from 'react-native-reani import { useAppDispatch, useAppSelector } from '../../../../redux/hooks'; import { playerRoundScoreIncrement } from '../../../../redux/PlayersSlice'; import { selectCurrentGame } from '../../../../redux/selectors'; +import { logEvent } from '../../../Analytics'; interface HalfTapProps { children: React.ReactNode; @@ -39,7 +39,7 @@ const SwipeVertical: React.FC = ({ //#region Power Hold - const powerHoldTime = 400; + const powerHoldTime = 500; const holdDuration = useRef(new Animated.Value(0)).current; let powerHoldTimer: NodeJS.Timeout; const [powerHold, setPowerHold] = useState(false); @@ -55,8 +55,8 @@ const SwipeVertical: React.FC = ({ }, []); const scale = holdDuration.interpolate({ - inputRange: [0, powerHoldTime * .9, powerHoldTime], - outputRange: [1, 1.1, 1.05], + inputRange: [0, powerHoldTime * .2, powerHoldTime * .9, powerHoldTime], + outputRange: [1, 1, 1.1, 1.05], extrapolate: 'clamp', }); @@ -159,14 +159,14 @@ const SwipeVertical: React.FC = ({ // Handle the end of the gesture totalOffset.value = null; - analytics().logEvent('score_change', { + logEvent('score_change', { player_index: index, game_id: currentGameId, addend: powerHold ? addendOne : addendTwo, round: roundCurrent, type: event.nativeEvent.translationY > 0 ? 'decrement' : 'increment', power_hold: powerHold, - notches: Math.round((event.nativeEvent.translationY || 0) / notchSize), + notches: -Math.round((event.nativeEvent.translationY || 0) / notchSize), interaction: 'swipe-vertical', }); From 4c39a408edc3f5f3814aa166d7eae8f13366a4b0 Mon Sep 17 00:00:00 2001 From: Justin Wyne <1986068+wyne@users.noreply.github.com> Date: Thu, 18 Jul 2024 17:19:24 -0700 Subject: [PATCH 14/17] Log list_menu_open event --- src/components/GameListItem.tsx | 5 ++++- src/components/Sheets/GameSheet.tsx | 21 +++++++++++++++++++-- src/screens/AppInfoScreen.tsx | 28 +++++++++++++++++++++++++--- src/screens/ListScreen.tsx | 2 +- 4 files changed, 49 insertions(+), 7 deletions(-) diff --git a/src/components/GameListItem.tsx b/src/components/GameListItem.tsx index d1d3b53b..c6673d8a 100644 --- a/src/components/GameListItem.tsx +++ b/src/components/GameListItem.tsx @@ -64,7 +64,10 @@ const GameListItem: React.FunctionComponent = ({ navigation, gameId, inde index={index} > Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Heavy)} + onLongPress={() => { + Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Heavy); + logEvent('list_menu_open'); + }} onPress={ // Only select game if iOS because Android is handled by PopupMenu Platform.OS == 'ios' ? chooseGameHandler : undefined diff --git a/src/components/Sheets/GameSheet.tsx b/src/components/Sheets/GameSheet.tsx index a560fcc8..109d09df 100644 --- a/src/components/Sheets/GameSheet.tsx +++ b/src/components/Sheets/GameSheet.tsx @@ -1,4 +1,4 @@ -import React, { useCallback, useMemo, useState } from 'react'; +import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import BottomSheet, { BottomSheetBackdrop, BottomSheetBackdropProps, BottomSheetScrollView } from '@gorhom/bottom-sheet'; import { ParamListBase, useIsFocused } from '@react-navigation/native'; @@ -138,7 +138,24 @@ const GameSheet: React.FunctionComponent = ({ navigation, containerHeight }; // State variable for the current snap point index - const [, setSnapPointIndex] = useState(0); + const [snapPointIndex, setSnapPointIndex] = useState(0); + const hasMountedRef = useRef(false); + + useEffect(() => { + if (hasMountedRef.current) { + if (snapPointIndex == 0) { + logEvent('game_sheet_close'); + } else { + logEvent('game_sheet_snap', { + snapPointIndex: snapPointIndex, + }); + + } + } else { + // Skip the effect on the first render + hasMountedRef.current = true; + } + }, [snapPointIndex]); /** * Function to handle changes in the bottom sheet diff --git a/src/screens/AppInfoScreen.tsx b/src/screens/AppInfoScreen.tsx index fde24fc8..8cf96429 100644 --- a/src/screens/AppInfoScreen.tsx +++ b/src/screens/AppInfoScreen.tsx @@ -43,11 +43,33 @@ const AppInfoScreen: React.FunctionComponent = ({ navigation }) => { const showPlayerIndex = useAppSelector(state => state.settings.showPlayerIndex); const showColorPalettes = useAppSelector(state => state.settings.showColorPalettes); const devMenuEnabled = useAppSelector(state => state.settings.devMenuEnabled); + const installId = useAppSelector(state => state.settings.installId); const dispatch = useAppDispatch(); - const toggleParticleSwitch = () => { dispatch(toggleShowPointParticles()); }; - const togglePlayerIndexSwitch = () => { dispatch(toggleShowPlayerIndex()); }; - const toggleColorPalettesSwitch = () => { dispatch(toggleShowColorPalettes()); }; + const toggleParticleSwitch = () => { + dispatch(toggleShowPointParticles()); + logEvent('toggle_feature', { + feature: 'point_particles', + value: !showPointParticles, + installId + }); + }; + const togglePlayerIndexSwitch = () => { + dispatch(toggleShowPlayerIndex()); + logEvent('toggle_feature', { + feature: 'player_index', + value: !showPlayerIndex, + installId + }); + }; + const toggleColorPalettesSwitch = () => { + dispatch(toggleShowColorPalettes()); + logEvent('toggle_feature', { + feature: 'color_palettes', + value: !showColorPalettes, + installId + }); + }; const alertWithVersion = async () => { Alert.alert('ScorePad with Rounds\n' + diff --git a/src/screens/ListScreen.tsx b/src/screens/ListScreen.tsx index 17dc4665..5328c432 100644 --- a/src/screens/ListScreen.tsx +++ b/src/screens/ListScreen.tsx @@ -38,7 +38,7 @@ const ListScreen: React.FunctionComponent = ({ navigation }) => { useEffect(() => { if (installId === undefined) { - console.log('no install id'); + logger.log('No install id'); const installId = Crypto.randomUUID(); dispatch(setInstallId(installId)); } From 13de5702062df5fc91f7a1dd0271c8f47627a412 Mon Sep 17 00:00:00 2001 From: Justin Wyne <1986068+wyne@users.noreply.github.com> Date: Thu, 18 Jul 2024 17:22:08 -0700 Subject: [PATCH 15/17] Log addend_sheet event --- src/components/Buttons/AddendButton.tsx | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/components/Buttons/AddendButton.tsx b/src/components/Buttons/AddendButton.tsx index 62ad94c3..6d1a675f 100644 --- a/src/components/Buttons/AddendButton.tsx +++ b/src/components/Buttons/AddendButton.tsx @@ -3,6 +3,7 @@ import React from 'react'; import { StyleSheet, Text, TouchableHighlight, View } from 'react-native'; import { useAppSelector } from '../../../redux/hooks'; +import { logEvent } from '../../Analytics'; import { systemBlue } from '../../constants'; import { InteractionType } from '../Interactions/InteractionType'; import { useAddendModalContext } from '../Sheets/AddendModalContext'; @@ -15,6 +16,7 @@ const AddendButton: React.FunctionComponent = ({ }) => { const addendOne = useAppSelector(state => state.settings.addendOne); const addendTwo = useAppSelector(state => state.settings.addendTwo); const interactionType = useAppSelector(state => state.settings.interactionType); + const installId = useAppSelector(state => state.settings.installId); const adddendModalRef = useAddendModalContext(); const gameSheetRef = useGameSheetContext(); @@ -26,6 +28,10 @@ const AddendButton: React.FunctionComponent = ({ }) => { gameSheetRef?.current?.snapToIndex(0); adddendModalRef.current?.present(); + + logEvent('addend_sheet', { + installId + }); }; const gestureIcons: { [key: string]: React.FunctionComponent; } = { From 73dc56529f75a13c002ffd2da8ca44bfb4dfe2d9 Mon Sep 17 00:00:00 2001 From: Justin Wyne <1986068+wyne@users.noreply.github.com> Date: Thu, 18 Jul 2024 17:22:26 -0700 Subject: [PATCH 16/17] Log interaction_type events --- src/components/Interactions/InteractionSelector.tsx | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/components/Interactions/InteractionSelector.tsx b/src/components/Interactions/InteractionSelector.tsx index c335520e..ccadfa34 100644 --- a/src/components/Interactions/InteractionSelector.tsx +++ b/src/components/Interactions/InteractionSelector.tsx @@ -4,6 +4,7 @@ import { Text, View } from 'react-native'; import { useAppDispatch, useAppSelector } from '../../../redux/hooks'; import { setInteractionType } from '../../../redux/SettingsSlice'; +import { logEvent } from '../../Analytics'; import BigButton from '../BigButtons/BigButton'; import SwipeGestureIcon from '../Buttons/SwipeGestureIcon'; import TapGestureIcon from '../Buttons/TapGestureIcon'; @@ -18,6 +19,7 @@ const InteractionSelector: React.FunctionComponent = ( const dispatch = useAppDispatch(); const interactionType = useAppSelector(state => state.settings.interactionType); + const gameId = useAppSelector(state => state.settings.currentGameId); const description = (() => { switch (interactionType) { @@ -37,6 +39,10 @@ const InteractionSelector: React.FunctionComponent = ( animated={false} onPress={() => { dispatch(setInteractionType(InteractionType.HalfTap)); + logEvent('interaction_type', { + interactionType: 'half_tap', + gameId, + }); }} text="Tap" icon={} @@ -48,6 +54,10 @@ const InteractionSelector: React.FunctionComponent = ( animated={false} onPress={() => { dispatch(setInteractionType(InteractionType.SwipeVertical)); + logEvent('interaction_type', { + interactionType: 'swipe_vertical', + gameId, + }); }} text="Swipe" icon={} From 8935847eba8f4ec1422d000ff1fdd4f92dc04b6e Mon Sep 17 00:00:00 2001 From: Justin Wyne <1986068+wyne@users.noreply.github.com> Date: Thu, 18 Jul 2024 17:36:43 -0700 Subject: [PATCH 17/17] Fix warning about animated opacity --- src/components/PlayerTiles/AdditionTile/ScoreParticle.tsx | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/components/PlayerTiles/AdditionTile/ScoreParticle.tsx b/src/components/PlayerTiles/AdditionTile/ScoreParticle.tsx index 5dd23f1a..d026c1eb 100644 --- a/src/components/PlayerTiles/AdditionTile/ScoreParticle.tsx +++ b/src/components/PlayerTiles/AdditionTile/ScoreParticle.tsx @@ -1,6 +1,6 @@ import React from 'react'; -import { Text , DimensionValue } from 'react-native'; +import { Text, DimensionValue } from 'react-native'; import Animated, { FadeOut } from 'react-native-reanimated'; type Props = { @@ -38,7 +38,6 @@ export const ScoreParticle: React.FunctionComponent = React.memo(({ id, v right: right, transform: [{ rotate: randomRotation }], backgroundColor: 'white', - opacity: 0.7, borderRadius: 100, borderWidth: 2, borderColor: 'black',