Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 5 additions & 3 deletions components/AvalancheCenterSelector.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import {avalancheCenterList, AvalancheCenters} from 'components/avalancheCenterL
import {AvalancheCenterList} from 'components/content/AvalancheCenterList';
import {incompleteQueryState, QueryState} from 'components/content/QueryState';
import {useAllAvalancheCenterMetadata} from 'hooks/useAllAvalancheCenterMetadata';
import {useAnalytics} from 'hooks/useAnalytics';
import {CenterSwitchOrigin, useAnalytics} from 'hooks/useAnalytics';
import {useAvalancheCenterCapabilities} from 'hooks/useAvalancheCenterCapabilities';
import {useSafeAreaInsets} from 'react-native-safe-area-context';
import {MainStackNavigationProps, MainStackParamList} from 'routes';
Expand All @@ -24,15 +24,17 @@ export const AvalancheCenterSelector: React.FunctionComponent<{
const capabilities = capabilitiesResult.data;
const whichCenters = route.params.debugMode ? AvalancheCenters.AllCenters : AvalancheCenters.SupportedCenters;
const metadataResults = useAllAvalancheCenterMetadata(capabilities, whichCenters);
const analytics = useAnalytics();

const setAvalancheCenterWrapper = React.useCallback(
(center: AvalancheCenterID) => {
analytics.captureCenterSwitch(currentCenterId, center, CenterSwitchOrigin.CenterSelectorView);
setAvalancheCenter(center);

navigation.goBack();
},
[setAvalancheCenter, navigation],
[setAvalancheCenter, currentCenterId, navigation, analytics],
);
const analytics = useAnalytics();

const recordAnalytics = useCallback(() => {
analytics.screen('centerSelector');
Expand Down
7 changes: 6 additions & 1 deletion components/content/navigation/NavigationHeader.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import {NativeStackHeaderProps} from '@react-navigation/native-stack';
import {HStack, View} from 'components/core';
import {GenerateObservationShareLink} from 'components/observations/ObservationUrlMapping';
import {Title3Black} from 'components/text';
import {getPresentedFromForAnalytics, useAnalytics} from 'hooks/useAnalytics';
import {logger} from 'logger';
import {usePreferences} from 'Preferences';
import React, {useCallback} from 'react';
Expand All @@ -16,6 +17,8 @@ export const NavigationHeader: React.FunctionComponent<NativeStackHeaderProps> =
const {preferences} = usePreferences();
const centerId = preferences.center;

const analytics = useAnalytics();

let share: boolean = false;
let firstOpen: boolean = false;
let shareCenterId: AvalancheCenterID = centerId;
Expand Down Expand Up @@ -69,10 +72,12 @@ export const NavigationHeader: React.FunctionComponent<NativeStackHeaderProps> =
return;
}

analytics.capture('shareButtonPressed', {presentedFrom: getPresentedFromForAnalytics(navigation)});

Share.share({
message: shareUrl,
}).catch((error: object) => logger.error(error, 'share button failed'));
}, [shareUrl]);
}, [shareUrl, analytics, navigation]);

return (
// Setting the top padding to insets.top correctly aligns the view underneath the notches on iPhone. Trying to set the padding ourselves could lead to unexpected behavior
Expand Down
5 changes: 4 additions & 1 deletion components/map/AvalancheForecastMapView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import {defaultMapRegionForGeometries, insetViewportBounds, regionBoundsVisible}
import {AvalancheForecastZoneCards} from 'components/map/AvalancheForecastZoneCards';
import {TopElementMeasurments} from 'components/map/AvalancheForecastZoneMap';
import {Position} from 'geojson';
import {CenterSwitchOrigin, useAnalytics} from 'hooks/useAnalytics';

interface AvalancheForecastMapViewProps {
preferredCenterId: AvalancheCenterID;
Expand All @@ -43,6 +44,7 @@ export const AvalancheForecastMapView: React.FunctionComponent<AvalancheForecast
const {logger} = React.useContext<LoggerProps>(LoggerContext);

const {setPreferences} = usePreferences();
const analytics = useAnalytics();
const {isInNoCenterExperience, setIsInNoCenterExperience, initialMapCamera, saveMapCamera} = useMapPersistence();

const navigation = useNavigation<MainStackNavigationProps>();
Expand Down Expand Up @@ -72,6 +74,7 @@ export const AvalancheForecastMapView: React.FunctionComponent<AvalancheForecast
setSelectedZoneId(zone.zone_id);

if (selectedZoneCenter !== preferredCenterId) {
analytics.captureCenterSwitch(preferredCenterId, selectedZoneCenter, CenterSwitchOrigin.Map);
setPreferences({center: selectedZoneCenter});
}

Expand All @@ -83,7 +86,7 @@ export const AvalancheForecastMapView: React.FunctionComponent<AvalancheForecast
}
}
},
[navigation, selectedZoneId, preferredCenterId, requestedTime, setSelectedZoneId, setPreferences, setIsInNoCenterExperience],
[navigation, analytics, selectedZoneId, preferredCenterId, requestedTime, setSelectedZoneId, setPreferences, setIsInNoCenterExperience],
);

const preferredCenterZones = useMemo(() => zones.filter(zone => zone.center_id === preferredCenterId), [zones, preferredCenterId]);
Expand Down
6 changes: 4 additions & 2 deletions components/observations/ObservationForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ import {UploaderState, getUploader} from 'components/observations/uploader/Obser
import {TaskStatus} from 'components/observations/uploader/Task';
import {Body, BodySemibold, Title3Semibold} from 'components/text';
import helpStrings from 'content/helpStrings';
import {useAnalytics} from 'hooks/useAnalytics';
import {getPresentedFromForAnalytics, useAnalytics} from 'hooks/useAnalytics';
import {useAvalancheCenterCapabilities} from 'hooks/useAvalancheCenterCapabilities';
import {useAvalancheCenterMetadata} from 'hooks/useAvalancheCenterMetadata';
import {useKeyboardBehavior} from 'hooks/useKeyboardBehavior';
Expand Down Expand Up @@ -240,12 +240,14 @@ export const ObservationForm: React.FC<{
if (!formContext.getValues('instability.avalanches_observed') && formContext.getValues('avalanches').length > 0) {
formContext.setValue('avalanches', []);
}

analytics.capture('submitObsButtonPressed', {presentedFrom: getPresentedFromForAnalytics(navigation)});
// Force validation errors to show up on fields that haven't been visited yet
await formContext.trigger();
// Then try to submit the form
void formContext.handleSubmit(onSubmitHandler, onSubmitErrorHandler)();
})();
}, [formContext, onSubmitHandler, onSubmitErrorHandler]);
}, [formContext, analytics, navigation, onSubmitHandler, onSubmitErrorHandler]);

const onCloseHandler = useCallback(() => {
formContext.reset();
Expand Down
29 changes: 29 additions & 0 deletions hooks/useAnalytics.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,43 @@
import {useContext, useMemo} from 'react';

import {NavigationState} from '@react-navigation/native';
import {Logger} from 'browser-bunyan';
import PostHog, {usePostHog} from 'posthog-react-native';

import {LoggerContext} from 'loggerContext';
import {AvalancheCenterID} from 'types/nationalAvalancheCenter';
import {fireAndForget} from 'utils/fireAndForget';

// Where a center switch was initiated from. Constrains the `eventOrigin` of the `centerSwitch` event
// to a known set of values so the data stays clean across call sites.
export enum CenterSwitchOrigin {
Map = 'Map',
CenterSelectorView = 'CenterSelectorView',
}

// Any navigation object (from screen props or from useNavigation) exposes getState().
interface NavigationWithState {
getState: () => NavigationState;
}

/**
* Returns the name of the route directly beneath the current one in the navigator — i.e. the screen
* the current screen was presented from. Intended for use as an analytics property to distinguish
* where a screen/action was reached from. Returns null (a valid analytics value, unlike undefined)
* when the current screen is the first in its navigator (no presenter).
*/
export const getPresentedFromForAnalytics = (navigation: NavigationWithState): string | null => {
const state = navigation.getState();
return state.routes[state.index - 1]?.name ?? null;
};

// A thin wrapper around the PostHog client. Several PostHog v4 methods return a Promise; this wraps the
// fire-and-forget calls (screen, register) so rejections are logged instead of silently swallowed.
// `reloadFeatureFlags` returns its promise so callers that want the resolved flags can await it.
export interface Analytics {
screen(...args: Parameters<PostHog['screen']>): void;
capture(...args: Parameters<PostHog['capture']>): void;
captureCenterSwitch(switchedFrom: AvalancheCenterID, switchedTo: AvalancheCenterID, origin: CenterSwitchOrigin): void;
identify(...args: Parameters<PostHog['identify']>): void;
register(...args: Parameters<PostHog['register']>): void;
reloadFeatureFlags(...args: Parameters<PostHog['reloadFeatureFlagsAsync']>): ReturnType<PostHog['reloadFeatureFlagsAsync']> | undefined;
Expand All @@ -23,6 +49,9 @@ export const createAnalytics = (postHog: PostHog | undefined, logger: Logger): A
capture: (...args) => {
postHog?.capture(...args);
},
captureCenterSwitch: (switchedFrom, switchedTo, origin) => {
postHog?.capture('centerSwitch', {switchedFrom: switchedFrom, switchedTo: switchedTo, eventOrigin: origin});
},
identify: (...args) => {
postHog?.identify(...args);
},
Expand Down
Loading