Skip to content

Commit

Permalink
feat: [IOCOM-1110] Home messages analytics events (#6115)
Browse files Browse the repository at this point in the history
## Short description
This PR add the Analytics event for the new messages home and search.

## List of changes proposed in this pull request
- For the event list, refer to the document linked on IOCOM-1110

## How to test
Using the io-dev-api-server, generate some messages and check that all
analytics events are triggered with proper parameters.
  • Loading branch information
Vangaorth authored Aug 26, 2024
1 parent 10738b2 commit ec09215
Show file tree
Hide file tree
Showing 25 changed files with 474 additions and 135 deletions.
12 changes: 8 additions & 4 deletions ts/features/messages/__mocks__/messages.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,8 @@ import { UIMessageId } from "../types";

export const defaultRequestPayload = {
pageSize: 12,
filter: { getArchived: false }
filter: { getArchived: false },
fromUserAction: false
};

export const defaultRequestError = {
Expand Down Expand Up @@ -149,7 +150,8 @@ export const successReloadMessagesPayload: ReloadMessagesPayload = {
previous: successPayloadMessages[0].id,
next: successPayloadMessages[2].id
},
filter: defaultRequestPayload.filter
filter: defaultRequestPayload.filter,
fromUserAction: false
};

export const successLoadNextPageMessagesPayload: NextPageMessagesSuccessPayload =
Expand All @@ -158,7 +160,8 @@ export const successLoadNextPageMessagesPayload: NextPageMessagesSuccessPayload
pagination: {
next: successPayloadMessages[2].id
},
filter: defaultRequestPayload.filter
filter: defaultRequestPayload.filter,
fromUserAction: false
};

export const successLoadPreviousPageMessagesPayload: PreviousPageMessagesSuccessPayload =
Expand All @@ -167,5 +170,6 @@ export const successLoadPreviousPageMessagesPayload: PreviousPageMessagesSuccess
pagination: {
previous: successPayloadMessages[0].id
},
filter: defaultRequestPayload.filter
filter: defaultRequestPayload.filter,
fromUserAction: false
};
118 changes: 118 additions & 0 deletions ts/features/messages/analytics/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,27 @@ import * as O from "fp-ts/lib/Option";
import * as S from "fp-ts/lib/string";
import { pipe } from "fp-ts/lib/function";
import { NonEmptyString } from "@pagopa/ts-commons/lib/strings";
import { getType } from "typesafe-actions";
import { ServiceId } from "../../../../definitions/backend/ServiceId";
import { MessageCategory } from "../../../../definitions/backend/MessageCategory";
import { mixpanelTrack } from "../../../mixpanel";
import { readablePrivacyReport } from "../../../utils/reporters";
import { UIMessageId } from "../types";
import { booleanToYesNo, buildEventProperties } from "../../../utils/analytics";
import { MessageGetStatusFailurePhaseType } from "../store/reducers/messageGetStatus";
import { MessageListCategory } from "../types/messageListCategory";
import { Action } from "../../../store/actions/types";
import { GlobalState } from "../../../store/reducers/types";
import {
loadNextPageMessages,
loadPreviousPageMessages,
reloadAllMessages
} from "../store/actions";
import {
messageCountForCategorySelector,
shownMessageCategorySelector
} from "../store/reducers/allPaginated";
import { pageSize } from "../../../config";

export function trackOpenMessage(
organizationName: string,
Expand Down Expand Up @@ -321,3 +335,107 @@ export function trackRemoteContentInfo() {
buildEventProperties("UX", "action")
);
}

export const trackMessagesActionsPostDispatch = (
action: Action,
state: GlobalState
) => {
switch (action.type) {
case getType(reloadAllMessages.success):
case getType(loadPreviousPageMessages.success):
case getType(loadNextPageMessages.success):
const shownCategory = shownMessageCategorySelector(state);
const messageCount = messageCountForCategorySelector(
state,
shownCategory
);
trackMessagesPage(
shownCategory,
messageCount,
pageSize,
action.payload.fromUserAction
);
break;
}
};

export const trackMessagesPage = (
category: MessageListCategory,
messageCount: number,
inputPageSize: number,
fromUserAction: boolean
) => {
const eventName = `MESSAGES_${
category === "ARCHIVE" ? "ARCHIVE" : "INBOX"
}_PAGE`;
const props = buildEventProperties("UX", "screen_view", {
page: Math.max(1, Math.ceil(messageCount / inputPageSize)),
count_messages: messageCount,
fromUserAction
});
void mixpanelTrack(eventName, props);
};

export const trackArchivedRestoredMessages = (
archived: boolean,
messageCount: number
) => {
const eventName = `MESSAGES_${archived ? "ARCHIVED" : "RESTORED"}`;
const props = buildEventProperties("UX", "action", {
[`count_messages_${archived ? "archived" : "restored"}`]: messageCount
});
void mixpanelTrack(eventName, props);
};

export const trackMessageListEndReached = (
category: MessageListCategory,
willLoadNextMessagePage: boolean
) => {
const eventName = `MESSAGES_${category === "ARCHIVE" ? "ARCHIVE" : "INBOX"}_${
willLoadNextMessagePage ? "SCROLL" : "ENDLIST"
}`;
const props = buildEventProperties("UX", "action");
void mixpanelTrack(eventName, props);
};

export const trackPullToRefresh = (category: MessageListCategory) => {
const eventName = `MESSAGES_${
category === "ARCHIVE" ? "ARCHIVE" : "INBOX"
}_REFRESH`;
const props = buildEventProperties("UX", "action");
void mixpanelTrack(eventName, props);
};

export const trackAutoRefresh = (category: MessageListCategory) => {
const eventName = `MESSAGES_${
category === "ARCHIVE" ? "ARCHIVE" : "INBOX"
}_AUTO_REFRESH`;
const props = buildEventProperties("TECH", undefined);
void mixpanelTrack(eventName, props);
};

export const trackMessageSearchPage = () => {
const eventName = `MESSAGES_SEARCH_PAGE`;
const props = buildEventProperties("UX", "screen_view");
void mixpanelTrack(eventName, props);
};

export const trackMessageSearchResult = (resultCount: number) => {
const eventName = `MESSAGES_SEARCH_RESULT_PAGE`;
const props = buildEventProperties("UX", "screen_view", {
count_result_returned: resultCount
});
void mixpanelTrack(eventName, props);
};

export const trackMessageSearchSelection = () => {
const eventName = `MESSAGES_SEARCH_RESULT_SELECTED`;
const props = buildEventProperties("UX", "action");
void mixpanelTrack(eventName, props);
};

export const trackMessageSearchClosing = () => {
const eventName = `MESSAGES_SEARCH_CLOSE`;
const props = buildEventProperties("UX", "action");
void mixpanelTrack(eventName, props);
};
3 changes: 2 additions & 1 deletion ts/features/messages/components/Home/EmptyList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,8 @@ export const EmptyList = ({ category }: EmptyListProps) => {
pipe(
{
pageSize,
filter: { getArchived: category === "ARCHIVE" }
filter: { getArchived: category === "ARCHIVE" },
fromUserAction: true
},
reloadAllMessages.request,
dispatch
Expand Down
12 changes: 10 additions & 2 deletions ts/features/messages/components/Home/MessageList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,11 +18,13 @@ import {
} from "../../store/reducers/allPaginated";
import { UIMessage } from "../../types";
import { ItwDiscoveryBanner } from "../../../itwallet/common/components/ItwDiscoveryBanner";
import { trackPullToRefresh } from "../../analytics";
import {
generateMessageListLayoutInfo,
getLoadNextPageMessagesActionIfAllowed,
getReloadAllMessagesActionForRefreshIfAllowed,
LayoutInfo
LayoutInfo,
trackMessageListEndReachedIfAllowed
} from "./homeUtils";
import { WrappedMessageListItem } from "./WrappedMessageListItem";
import {
Expand Down Expand Up @@ -85,6 +87,7 @@ export const MessageList = React.forwardRef<FlatList, MessageListProps>(
);

const onRefreshCallback = useCallback(() => {
trackPullToRefresh(category);
const state = store.getState();
const reloadAllMessagesAction =
getReloadAllMessagesActionForRefreshIfAllowed(state, category);
Expand All @@ -99,6 +102,11 @@ export const MessageList = React.forwardRef<FlatList, MessageListProps>(
category,
new Date()
);
trackMessageListEndReachedIfAllowed(
category,
!!loadNextPageMessages,
state
);
if (loadNextPageMessages) {
dispatch(loadNextPageMessages);
}
Expand Down Expand Up @@ -126,9 +134,9 @@ export const MessageList = React.forwardRef<FlatList, MessageListProps>(
} else {
return (
<WrappedMessageListItem
archiveRestoreSourceCategory={category}
index={index}
message={item}
source={category}
/>
);
}
Expand Down
90 changes: 56 additions & 34 deletions ts/features/messages/components/Home/PagerViewContainer.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { pipe } from "fp-ts/lib/function";
import React, { useCallback, useEffect, useRef } from "react";
import React, { useCallback, useRef } from "react";
import { FlatList, NativeSyntheticEvent } from "react-native";
import { useFocusEffect } from "@react-navigation/native";
import PagerView from "react-native-pager-view";
Expand All @@ -9,15 +9,21 @@ import { useIODispatch, useIOStore } from "../../../../store/hooks";
import { setShownMessageCategoryAction } from "../../store/actions";
import { GlobalState } from "../../../../store/reducers/types";
import { useTabItemPressWhenScreenActive } from "../../../../hooks/useTabItemPressWhenScreenActive";
import { shownMessageCategorySelector } from "../../store/reducers/allPaginated";
import {
messageCountForCategorySelector,
shownMessageCategorySelector
} from "../../store/reducers/allPaginated";
import { foldK as foldMessageListCategory } from "../../types/messageListCategory";
import SectionStatusComponent from "../../../../components/SectionStatus";
import { trackAutoRefresh, trackMessagesPage } from "../../analytics";
import { pageSize } from "../../../../config";
import { MessageList } from "./MessageList";
import {
getInitialReloadAllMessagesActionIfNeeded,
getLoadPreviousPageMessagesActionIfAllowed,
getMessagesViewPagerInitialPageIndex,
messageViewPageIndexToListCategory
messageViewPageIndexToListCategory,
trackMessagePageOnFocusEventIfAllowed
} from "./homeUtils";
import { ArchiveRestoreBar } from "./ArchiveRestoreBar";

Expand Down Expand Up @@ -50,6 +56,8 @@ export const PagerViewContainer = React.forwardRef<PagerView>((_, ref) => {
const loadPreviousPageAction =
getLoadPreviousPageMessagesActionIfAllowed(state);
if (loadPreviousPageAction) {
const shownCategory = shownMessageCategorySelector(state);
trackAutoRefresh(shownCategory);
dispatch(loadPreviousPageAction);
}
}, [dispatch, store]);
Expand All @@ -65,38 +73,48 @@ export const PagerViewContainer = React.forwardRef<PagerView>((_, ref) => {
);
const onPagerViewPageSelected = useCallback(
(selectionEvent: NativeSyntheticEvent<OnPageSelectedEventData>) => {
// Be aware that this callback is triggered when the user swipes
// horizontally but also when the TabNavigationContainer uses the
// PagerView's ref to switch page
// Be aware that this callback is triggered:
// - upon first PagerView rendering;
// - when the user completes a full horizontal swipe;
// - when the TabNavigationContainer uses the PagerView's ref to switch page.

// Also note that this method is called only on an effective page
// change so if there is none (i.e., the user swipe is not wide
// enough to move to a new page and the pager view scrolls back
// to the current displayed page), this callback is not invoked,
// thus allowing us not to check for a changed category/page-index
// thus allowing us not to check for a changed category/page-index.

const selectedTabIndex = selectionEvent.nativeEvent.position;
const selectedShownCategory =
messageViewPageIndexToListCategory(selectedTabIndex);
dispatch(setShownMessageCategoryAction(selectedShownCategory));

// Be aware that the store.state must not be extracted outside of
// this useEffect hook, otherwise it will re-run the hook every
// time the state changes. Be also aware that
// 'setShownMessageCategoryAction(selectedShownCategory)' above
// must be called before 'reloadAllMessage.request' below, otherwise
// the store will not have the proper 'shownCategory' value
// this useEffect hook, otherwise it will re-run the callback on
// every state change.
const state = store.getState();

// Make sure that the above call to
// 'setShownMessageCategoryAction(selectedShownCategory)'
// has been done before all the following code below, otherwise
// the store will not have the proper 'shownCategory' value

// Track message category change
const messageCount = messageCountForCategorySelector(
state,
selectedShownCategory
);
trackMessagesPage(selectedShownCategory, messageCount, pageSize, true);

// Handle inizial message loading (if needed)
dispatchReloadAllMessagesIfNeeded(state);

// As before, in order for the following call to work propertly,
// 'setShownMessageCategoryAction(selectedShownCategory)' has to be
// called before it (otherwise the 'shownMessageCategory' will be
// wrong). It has an internal logic by which it does not dispatch
// anything if the previous `dispatchReloadAllMessagesIfNeeded` has
// already requested a 'reloadAllMessages.request'
// It is called here to refresh the message list when not changing
// the screen but only switching between tabs
// The following onvoked method has an internal logic by
// which it does not dispatch anything if the previous
// `dispatchReloadAllMessagesIfNeeded` has already requested
// a 'reloadAllMessages.request'. It is called here to refresh
// the message list when not changing the screen but only
// switching between tabs.
loadNewlyReceivedMessagesIfNeededCallback();
},
[
Expand All @@ -109,24 +127,28 @@ export const PagerViewContainer = React.forwardRef<PagerView>((_, ref) => {
useTabItemPressWhenScreenActive(onTabPressedCallback, false);
useFocusEffect(
useCallback(() => {
// This is called to automatically refresh after coming back
// to this screen from another one. The timeout is needed to avoid
// a glitch with the FlatList that does not update the pull-to-refresh
// margins after the check has completed (what happens is that the
// pull-to-refresh control disappears but the list keeps its blank
// view placeholder visible)
// This hook has two use-cases:
// - to send an analytics event on first landing, when there
// is a change-back using the messages bottom tab and when
// the user navigates back from a message details;
// - to check if there are new messages (on the server) when
// there is a change-back using the messages bottom tab and
// when the user navigates back from a message details.
// The timeout is needed:
// - during onboarding, where a screen is mounted on top of the
// main navigation tab, thus avoiding a momentary focus of
// the selected tab (which is normally the messages one);
// - to avoid a glitch with the FlatList that does not update
// the pull-to-refresh margins after the check has completed
// (what happens is that the pull-to-refresh control disappears
// but the list keeps its blank view placeholder visible).
setTimeout(() => {
const state = store.getState();
trackMessagePageOnFocusEventIfAllowed(state);
loadNewlyReceivedMessagesIfNeededCallback();
}, 100);
}, [loadNewlyReceivedMessagesIfNeededCallback])
}, [loadNewlyReceivedMessagesIfNeededCallback, store])
);
useEffect(() => {
// Upon first component rendering, the PagerView's onPageSelected
// callback is not called, so we must dispatch the reload action
// for the initial shown message list
const state = store.getState();
dispatchReloadAllMessagesIfNeeded(state);
}, [dispatchReloadAllMessagesIfNeeded, store]);

return (
<>
Expand Down
Loading

0 comments on commit ec09215

Please sign in to comment.