diff --git a/packages/common/src/context/appContext.ts b/packages/common/src/context/appContext.ts new file mode 100644 index 0000000000..d3ab4711ac --- /dev/null +++ b/packages/common/src/context/appContext.ts @@ -0,0 +1,26 @@ +import { createContext, useContext } from 'react' + +import { AnalyticsEvent, AllTrackingEvents } from 'models/Analytics' + +type AppContextType = { + analytics: { + track: (event: AnalyticsEvent, callback?: () => void) => Promise + make: ( + event: T + ) => { + eventName: string + properties: any + } + } +} + +export const AppContext = createContext(null) + +export const useAppContext = () => { + const context = useContext(AppContext) + if (!context) { + throw new Error('useAppContext has to be used within ') + } + + return context +} diff --git a/packages/common/src/context/index.ts b/packages/common/src/context/index.ts new file mode 100644 index 0000000000..0b4bd5f5ea --- /dev/null +++ b/packages/common/src/context/index.ts @@ -0,0 +1 @@ +export * from './appContext' diff --git a/packages/common/src/hooks/chats/useCanSendMessage.ts b/packages/common/src/hooks/chats/useCanSendMessage.ts index 72de4b87eb..5f9d286928 100644 --- a/packages/common/src/hooks/chats/useCanSendMessage.ts +++ b/packages/common/src/hooks/chats/useCanSendMessage.ts @@ -1,7 +1,7 @@ import { useSelector } from 'react-redux' import { User } from 'models/User' -import { CommonState } from 'store/index' +import { ChatPermissionAction, CommonState } from 'store/index' import { getCanSendMessage, getOtherChatUsers @@ -12,14 +12,20 @@ import { useProxySelector } from '..' /** * Returns whether or not the current user can send messages to the current chat */ -export const useCanSendMessage = (currentChatId?: string) => { +export const useCanSendMessage = ( + currentChatId?: string +): { + canSendMessage: boolean + callToAction: ChatPermissionAction + // Explicitly define type as undefinable since users could be empty + firstOtherUser: User | undefined +} => { const users = useProxySelector( (state) => getOtherChatUsers(state, currentChatId), [currentChatId] ) - // Explicitly define type as undefinable since users could be empty - const firstOtherUser: User | undefined = users[0] + const firstOtherUser = users[0] const { canSendMessage, callToAction } = useSelector((state: CommonState) => getCanSendMessage(state, { diff --git a/packages/common/src/hooks/chats/useSetInboxPermissions.ts b/packages/common/src/hooks/chats/useSetInboxPermissions.ts index d0bb0788c3..c95019fc0f 100644 --- a/packages/common/src/hooks/chats/useSetInboxPermissions.ts +++ b/packages/common/src/hooks/chats/useSetInboxPermissions.ts @@ -4,7 +4,9 @@ import type { AudiusSdk } from '@audius/sdk' import { ChatPermission } from '@audius/sdk' import { useDispatch, useSelector } from 'react-redux' +import { Name } from 'models/Analytics' import { Status } from 'models/Status' +import { useAppContext } from 'src/context/appContext' import { accountSelectors } from 'store/account' import { chatActions, chatSelectors } from 'store/pages' @@ -22,6 +24,9 @@ export const useSetInboxPermissions = ({ audiusSdk }: useSetInboxPermissionsProps) => { const dispatch = useDispatch() + const { + analytics: { track, make } + } = useAppContext() const permissions = useSelector(getUserChatPermissions) const userId = useSelector(getUserId) const currentPermission = permissions?.permits @@ -57,13 +62,25 @@ export const useSetInboxPermissions = ({ const sdk = await audiusSdk() await sdk.chats.permit({ permit: permission }) setPermissionStatus(Status.SUCCESS) + track( + make({ + eventName: Name.CHANGE_INBOX_SETTINGS_SUCCESS, + permission + }) + ) } catch (e) { console.error('Error saving chat permissions:', e) setPermissionStatus(Status.ERROR) + track( + make({ + eventName: Name.CHANGE_INBOX_SETTINGS_FAILURE, + permission + }) + ) } } }, - [audiusSdk, permissionStatus] + [audiusSdk, track, make, permissionStatus] ) // Save local permission state to backend. Useful in scenarios where we diff --git a/packages/common/src/index.ts b/packages/common/src/index.ts index 22fd3651f4..b6b8ebea66 100644 --- a/packages/common/src/index.ts +++ b/packages/common/src/index.ts @@ -1,5 +1,6 @@ export * from './api' export * from './audius-query' +export * from './context' export * from './models' export * from './utils' export * from './services' diff --git a/packages/common/src/models/Analytics.ts b/packages/common/src/models/Analytics.ts index 135dbc6181..48c20ef73e 100644 --- a/packages/common/src/models/Analytics.ts +++ b/packages/common/src/models/Analytics.ts @@ -1,3 +1,5 @@ +import { ChatPermission } from '@audius/sdk' + import { FeedFilter } from 'models/FeedFilter' import { ID, PlayableType } from 'models/Identifiers' import { MonitorPayload, ServiceMonitorType } from 'models/Services' @@ -319,7 +321,24 @@ export enum Name { CONNECT_WALLET_NEW_WALLET_CONNECTED = 'Connect Wallet: New Wallet Connected', CONNECT_WALLET_ALREADY_ASSOCIATED = 'Connect Wallet: Already Associated', CONNECT_WALLET_ASSOCIATION_ERROR = 'Connect Wallet: Association Error', - CONNECT_WALLET_ERROR = 'Connect Wallet: Error' + CONNECT_WALLET_ERROR = 'Connect Wallet: Error', + + // Chat + CREATE_CHAT_SUCCESS = 'Create Chat: Success', + CREATE_CHAT_FAILURE = 'Create Chat: Failure', + SEND_MESSAGE_SUCCESS = 'Send Message: Success', + SEND_MESSAGE_FAILURE = 'Send Message: Failure', + DELETE_CHAT_SUCCESS = 'Delete Chat: Success', + DELETE_CHAT_FAILURE = 'Delete Chat: Failure', + BLOCK_USER_SUCCESS = 'Block User: Success', + BLOCK_USER_FAILURE = 'Block User: Failure', + CHANGE_INBOX_SETTINGS_SUCCESS = 'Change Inbox Settings: Success', + CHANGE_INBOX_SETTINGS_FAILURE = 'Change Inbox Settings: Failure', + SEND_MESSAGE_REACTION_SUCCESS = 'Send Message Reaction: Success', + SEND_MESSAGE_REACTION_FAILURE = 'Send Message Reaction: Failure', + MESSAGE_UNFURL_TRACK = 'Message Unfurl: Track', + MESSAGE_UNFURL_PLAYLIST = 'Message Unfurl: Playlist', + TIP_UNLOCKED_CHAT = 'Unlocked Chat: Tip' } type PageView = { @@ -1512,6 +1531,73 @@ type ConnectWalletError = { error: string } +type CreateChatSuccess = { + eventName: Name.CREATE_CHAT_SUCCESS +} + +type CreateChatFailure = { + eventName: Name.CREATE_CHAT_FAILURE +} + +type SendMessageSuccess = { + eventName: Name.SEND_MESSAGE_SUCCESS +} + +type SendMessageFailure = { + eventName: Name.SEND_MESSAGE_FAILURE +} + +type DeleteChatSuccess = { + eventName: Name.DELETE_CHAT_SUCCESS +} + +type DeleteChatFailure = { + eventName: Name.DELETE_CHAT_FAILURE +} + +type BlockUserSuccess = { + eventName: Name.BLOCK_USER_SUCCESS + blockedUserId: ID +} + +type BlockUserFailure = { + eventName: Name.BLOCK_USER_FAILURE + blockedUserId: ID +} + +type ChangeInboxSettingsSuccess = { + eventName: Name.CHANGE_INBOX_SETTINGS_SUCCESS + permission: ChatPermission +} + +type ChangeInboxSettingsFailure = { + eventName: Name.CHANGE_INBOX_SETTINGS_FAILURE + permission: ChatPermission +} + +type SendMessageReactionSuccess = { + eventName: Name.SEND_MESSAGE_REACTION_SUCCESS + reaction: string | null +} + +type SendMessageReactionFailure = { + eventName: Name.SEND_MESSAGE_REACTION_FAILURE + reaction: string | null +} + +type MessageUnfurlTrack = { + eventName: Name.MESSAGE_UNFURL_TRACK +} + +type MessageUnfurlPlaylist = { + eventName: Name.MESSAGE_UNFURL_PLAYLIST +} + +type TipUnlockedChat = { + eventName: Name.TIP_UNLOCKED_CHAT + recipientUserId: ID +} + export type BaseAnalyticsEvent = { type: typeof ANALYTICS_TRACK_EVENT } export type AllTrackingEvents = @@ -1714,3 +1800,19 @@ export type AllTrackingEvents = | ConnectWalletAlreadyAssociated | ConnectWalletAssociationError | ConnectWalletError + | SendMessageSuccess + | CreateChatSuccess + | CreateChatFailure + | SendMessageSuccess + | SendMessageFailure + | DeleteChatSuccess + | DeleteChatFailure + | BlockUserSuccess + | BlockUserFailure + | ChangeInboxSettingsSuccess + | ChangeInboxSettingsFailure + | SendMessageReactionSuccess + | SendMessageReactionFailure + | MessageUnfurlTrack + | MessageUnfurlPlaylist + | TipUnlockedChat diff --git a/packages/common/src/store/pages/chat/sagas.ts b/packages/common/src/store/pages/chat/sagas.ts index f953e8889f..24ccff0401 100644 --- a/packages/common/src/store/pages/chat/sagas.ts +++ b/packages/common/src/store/pages/chat/sagas.ts @@ -15,6 +15,8 @@ import { } from 'typed-redux-saga' import { ulid } from 'ulid' +import { Name } from 'models/Analytics' +import { ErrorLevel } from 'models/ErrorReporting' import { ID } from 'models/Identifiers' import { Status } from 'models/Status' import { getAccountUser, getUserId } from 'store/account/selectors' @@ -101,6 +103,11 @@ function* doFetchUnreadMessagesCount() { } catch (e) { console.error('fetchUnreadMessagesCountFailed', e) yield* put(fetchUnreadMessagesCountFailed()) + const reportToSentry = yield* getContext('reportToSentry') + reportToSentry({ + level: ErrorLevel.Error, + error: e as Error + }) } } @@ -119,6 +126,11 @@ function* doFetchMoreChats() { } catch (e) { console.error('fetchMoreChatsFailed', e) yield* put(fetchMoreChatsFailed()) + const reportToSentry = yield* getContext('reportToSentry') + reportToSentry({ + level: ErrorLevel.Error, + error: e as Error + }) } } @@ -169,11 +181,20 @@ function* doFetchMoreMessages(action: ReturnType) { } catch (e) { console.error('fetchNewChatMessagesFailed', e) yield* put(fetchMoreMessagesFailed({ chatId })) + const reportToSentry = yield* getContext('reportToSentry') + reportToSentry({ + level: ErrorLevel.Error, + error: e as Error, + additionalInfo: { + chatId + } + }) } } function* doSetMessageReaction(action: ReturnType) { const { chatId, messageId, reaction, userId } = action.payload + const { track, make } = yield* getContext('analytics') try { const audiusSdk = yield* getContext('audiusSdk') const sdk = yield* call(audiusSdk) @@ -198,14 +219,40 @@ function* doSetMessageReaction(action: ReturnType) { reaction: reactionResponse }) ) + yield* call( + track, + make({ + eventName: Name.SEND_MESSAGE_REACTION_SUCCESS, + reaction + }) + ) } catch (e) { console.error('setMessageReactionFailed', e) yield* put(setMessageReactionFailed(action.payload)) + const reportToSentry = yield* getContext('reportToSentry') + reportToSentry({ + level: ErrorLevel.Error, + error: e as Error, + additionalInfo: { + chatId, + messageId, + reaction, + userId + } + }) + yield* call( + track, + make({ + eventName: Name.SEND_MESSAGE_REACTION_FAILURE, + reaction + }) + ) } } function* doCreateChat(action: ReturnType) { const { userIds, skipNavigation } = action.payload + const { track, make } = yield* getContext('analytics') try { const audiusSdk = yield* getContext('audiusSdk') const sdk = yield* call(audiusSdk) @@ -238,6 +285,7 @@ function* doCreateChat(action: ReturnType) { throw new Error("Chat couldn't be found after creating") } yield* put(createChatSucceeded({ chat })) + yield* call(track, make({ eventName: Name.CREATE_CHAT_SUCCESS })) } } catch (e) { console.error('createChatFailed', e) @@ -247,6 +295,15 @@ function* doCreateChat(action: ReturnType) { content: 'Something went wrong. Failed to create chat.' }) ) + const reportToSentry = yield* getContext('reportToSentry') + reportToSentry({ + level: ErrorLevel.Error, + error: e as Error, + additionalInfo: { + userIds + } + }) + yield* call(track, make({ eventName: Name.CREATE_CHAT_FAILURE })) } } @@ -273,11 +330,20 @@ function* doMarkChatAsRead(action: ReturnType) { } catch (e) { console.error('markChatAsReadFailed', e) yield* put(markChatAsReadFailed({ chatId })) + const reportToSentry = yield* getContext('reportToSentry') + reportToSentry({ + level: ErrorLevel.Error, + error: e as Error, + additionalInfo: { + chatId + } + }) } } function* doSendMessage(action: ReturnType) { const { chatId, message, resendMessageId } = action.payload + const { track, make } = yield* getContext('analytics') const messageIdToUse = resendMessageId ?? ulid() const userId = yield* select(getUserId) try { @@ -309,6 +375,7 @@ function* doSendMessage(action: ReturnType) { messageId: messageIdToUse, message }) + yield* call(track, make({ eventName: Name.SEND_MESSAGE_SUCCESS })) } catch (e) { console.error('sendMessageFailed', e) yield* put(sendMessageFailed({ chatId, messageId: messageIdToUse })) @@ -329,6 +396,16 @@ function* doSendMessage(action: ReturnType) { }) ) } + const reportToSentry = yield* getContext('reportToSentry') + reportToSentry({ + level: ErrorLevel.Error, + error: e as Error, + additionalInfo: { + chatId, + messageId: messageIdToUse + } + }) + yield* call(track, make({ eventName: Name.SEND_MESSAGE_FAILURE })) } } @@ -364,6 +441,11 @@ function* doFetchBlockees() { ) } catch (e) { console.error('fetchBlockeesFailed', e) + const reportToSentry = yield* getContext('reportToSentry') + reportToSentry({ + level: ErrorLevel.Error, + error: e as Error + }) } } @@ -381,19 +463,39 @@ function* doFetchBlockers() { ) } catch (e) { console.error('fetchBlockersFailed', e) + const reportToSentry = yield* getContext('reportToSentry') + reportToSentry({ + level: ErrorLevel.Error, + error: e as Error + }) } } function* doBlockUser(action: ReturnType) { + const { userId } = action.payload + const { track, make } = yield* getContext('analytics') try { const audiusSdk = yield* getContext('audiusSdk') const sdk = yield* call(audiusSdk) yield* call([sdk.chats, sdk.chats.block], { - userId: encodeHashId(action.payload.userId) + userId: encodeHashId(userId) }) yield* put(fetchBlockees()) + yield* call( + track, + make({ eventName: Name.BLOCK_USER_SUCCESS, blockedUserId: userId }) + ) } catch (e) { console.error('blockUserFailed', e) + const reportToSentry = yield* getContext('reportToSentry') + reportToSentry({ + level: ErrorLevel.Error, + error: e as Error + }) + yield* call( + track, + make({ eventName: Name.BLOCK_USER_FAILURE, blockedUserId: userId }) + ) } } @@ -407,6 +509,11 @@ function* doUnblockUser(action: ReturnType) { yield* put(fetchBlockees()) } catch (e) { console.error('unblockUserFailed', e) + const reportToSentry = yield* getContext('reportToSentry') + reportToSentry({ + level: ErrorLevel.Error, + error: e as Error + }) } } @@ -434,6 +541,11 @@ function* doFetchPermissions(action: ReturnType) { ) } catch (e) { console.error('fetchPermissionsFailed', e) + const reportToSentry = yield* getContext('reportToSentry') + reportToSentry({ + level: ErrorLevel.Error, + error: e as Error + }) } } @@ -457,12 +569,23 @@ function* doFetchLinkUnfurlMetadata( fetchLinkUnfurlSucceeded({ chatId, messageId, unfurlMetadata: data[0] }) ) } catch (e) { - console.error('fetchPermissionsFailed', e) + console.error('fetchUnfurlFailed', e) + const reportToSentry = yield* getContext('reportToSentry') + reportToSentry({ + level: ErrorLevel.Error, + error: e as Error, + additionalInfo: { + chatId, + messageId, + href + } + }) } } function* doDeleteChat(action: ReturnType) { const { chatId } = action.payload + const { track, make } = yield* getContext('analytics') try { const audiusSdk = yield* getContext('audiusSdk') const sdk = yield* call(audiusSdk) @@ -475,8 +598,18 @@ function* doDeleteChat(action: ReturnType) { yield* delay(1) // NOW delete the chat - otherwise we refetch it right away yield* put(deleteChatSucceeded({ chatId })) + yield* call(track, make({ eventName: Name.DELETE_CHAT_SUCCESS })) } catch (e) { console.error('deleteChat failed', e, { chatId }) + const reportToSentry = yield* getContext('reportToSentry') + reportToSentry({ + level: ErrorLevel.Error, + error: e as Error, + additionalInfo: { + chatId + } + }) + yield* call(track, make({ eventName: Name.DELETE_CHAT_FAILURE })) } } diff --git a/packages/common/src/store/pages/chat/selectors.ts b/packages/common/src/store/pages/chat/selectors.ts index 0affc15a0f..ef902af61c 100644 --- a/packages/common/src/store/pages/chat/selectors.ts +++ b/packages/common/src/store/pages/chat/selectors.ts @@ -3,6 +3,7 @@ import { createSelector } from 'reselect' import { ID } from 'models/Identifiers' import { Status } from 'models/Status' +import { User } from 'models/User' import { accountSelectors } from 'store/account' import { cacheUsersSelectors } from 'store/cache' import { CommonState } from 'store/reducers' @@ -160,7 +161,10 @@ export const getOtherChatUsers = (state: CommonState, chatId?: string) => { return getOtherChatUsersFromChat(state, chat) } -export const getSingleOtherChatUser = (state: CommonState, chatId?: string) => { +export const getSingleOtherChatUser = ( + state: CommonState, + chatId?: string +): User | undefined => { return getOtherChatUsers(state, chatId)[0] } diff --git a/packages/common/src/store/storeContext.ts b/packages/common/src/store/storeContext.ts index 8f12f2c545..7fb6302e3f 100644 --- a/packages/common/src/store/storeContext.ts +++ b/packages/common/src/store/storeContext.ts @@ -1,6 +1,12 @@ import type { AudiusSdk } from '@audius/sdk' -import { AnalyticsEvent, LineupState, Track } from '../models' +import { + AllTrackingEvents, + AnalyticsEvent, + LineupState, + ReportToSentryArgs, + Track +} from '../models' import { AudioPlayer } from '../services/audio-player' import { AudiusAPIClient } from '../services/audius-api-client' import { AudiusBackend } from '../services/audius-backend' @@ -33,6 +39,12 @@ export type CommonStoreContext = { options?: Record, callback?: () => void ) => Promise + make: ( + event: T + ) => { + eventName: string + properties: any + } } remoteConfigInstance: RemoteConfigInstance audiusBackendInstance: AudiusBackend @@ -53,6 +65,7 @@ export type CommonStoreContext = { setTag: (key: string, value: string) => void configureScope: (fn: (scope: { setUser: any }) => void) => void } + reportToSentry: (args: ReportToSentryArgs) => void trackDownload: TrackDownload instagramAppId?: string instagramRedirectUrl?: string diff --git a/packages/mobile/src/app/App.tsx b/packages/mobile/src/app/App.tsx index 0bc4042bbf..618cea7dc5 100644 --- a/packages/mobile/src/app/App.tsx +++ b/packages/mobile/src/app/App.tsx @@ -1,4 +1,4 @@ -import { AudiusQueryContext } from '@audius/common' +import { AudiusQueryContext, AppContext } from '@audius/common' import { PortalProvider, PortalHost } from '@gorhom/portal' import * as Sentry from '@sentry/react-native' import { Platform, UIManager } from 'react-native' @@ -23,6 +23,7 @@ import { useEnterForeground } from 'app/hooks/useAppState' import { incrementSessionCount } from 'app/hooks/useSessionCount' import { RootScreen } from 'app/screens/root-screen' import { WalletConnectProvider } from 'app/screens/wallet-connect' +import * as analytics from 'app/services/analytics' import { apiClient } from 'app/services/audius-api-client' import { audiusBackendInstance } from 'app/services/audius-backend-instance' import { audiusSdk } from 'app/services/audius-sdk' @@ -78,41 +79,47 @@ const App = () => { }) return ( - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + ) } diff --git a/packages/mobile/src/screens/chat-screen/ChatMessagePlaylist.tsx b/packages/mobile/src/screens/chat-screen/ChatMessagePlaylist.tsx index b69a6071fe..14e55599d0 100644 --- a/packages/mobile/src/screens/chat-screen/ChatMessagePlaylist.tsx +++ b/packages/mobile/src/screens/chat-screen/ChatMessagePlaylist.tsx @@ -2,6 +2,7 @@ import { useCallback, useMemo, useEffect } from 'react' import type { ChatMessageTileProps, ID, TrackPlayback } from '@audius/common' import { + Name, Kind, PlaybackSource, QueueSource, @@ -150,6 +151,11 @@ export const ChatMessagePlaylist = ({ useEffect(() => { if (collection && uid) { + trackEvent( + make({ + eventName: Name.MESSAGE_UNFURL_PLAYLIST + }) + ) onSuccess?.() } else { onEmpty?.() diff --git a/packages/mobile/src/screens/chat-screen/ChatMessageTrack.tsx b/packages/mobile/src/screens/chat-screen/ChatMessageTrack.tsx index ebd99952ad..dc1223d370 100644 --- a/packages/mobile/src/screens/chat-screen/ChatMessageTrack.tsx +++ b/packages/mobile/src/screens/chat-screen/ChatMessageTrack.tsx @@ -2,6 +2,7 @@ import { useCallback, useEffect, useMemo } from 'react' import type { ChatMessageTileProps, ID, TrackPlayback } from '@audius/common' import { + Name, Kind, PlaybackSource, QueueSource, @@ -65,6 +66,11 @@ export const ChatMessageTrack = ({ useEffect(() => { if (track && user && uid) { + trackEvent( + make({ + eventName: Name.MESSAGE_UNFURL_TRACK + }) + ) onSuccess?.() } else { onEmpty?.() diff --git a/packages/mobile/src/screens/chat-screen/ChatUnavailable.tsx b/packages/mobile/src/screens/chat-screen/ChatUnavailable.tsx index f28bb44e66..32b2f08d30 100644 --- a/packages/mobile/src/screens/chat-screen/ChatUnavailable.tsx +++ b/packages/mobile/src/screens/chat-screen/ChatUnavailable.tsx @@ -9,7 +9,7 @@ import { setVisibility } from 'app/store/drawers/slice' import { makeStyles } from 'app/styles' const messages = { - noAction: (userName: string) => `You can't send messages to ${userName}. `, + noAction: (userName?: string) => `You can't send messages to ${userName}. `, tip1: 'You must send ', tip2: ' a tip before you can send them messages.', blockee: 'You cannot send messages to users you have blocked. ', @@ -53,13 +53,15 @@ export const ChatUnavailable = ({ chatId }: ChatUnavailableProps) => { const handleLearnMorePress = useCallback(() => {}, []) const handleUnblockPress = useCallback(() => { - dispatch( - setVisibility({ - drawer: 'BlockMessages', - visible: true, - data: { userId: otherUser.user_id } - }) - ) + if (otherUser) { + dispatch( + setVisibility({ + drawer: 'BlockMessages', + visible: true, + data: { userId: otherUser.user_id } + }) + ) + } }, [dispatch, otherUser]) const mapChatPermissionActionToContent = useMemo(() => { @@ -67,7 +69,7 @@ export const ChatUnavailable = ({ chatId }: ChatUnavailableProps) => { [ChatPermissionAction.NONE]: () => ( <> - {messages.noAction(otherUser.name)} + {messages.noAction(otherUser?.name)} { {messages.tip1} - navigation.navigate('Profile', { id: otherUser.user_id }) + navigation.navigate('Profile', { id: otherUser?.user_id }) } > - {otherUser.name} + {otherUser?.name} {messages.tip2} diff --git a/packages/mobile/src/services/analytics.ts b/packages/mobile/src/services/analytics.ts index 0a33ac4a5a..ad15549f83 100644 --- a/packages/mobile/src/services/analytics.ts +++ b/packages/mobile/src/services/analytics.ts @@ -15,6 +15,7 @@ let analyticsSetupStatus: 'ready' | 'pending' | 'error' = 'pending' const AmplitudeWriteKey = Config.AMPLITUDE_WRITE_KEY const AmplitudeProxy = Config.AMPLITUDE_PROXY const amplitudeInstance = Amplitude.getInstance() +const IS_PRODUCTION_BUILD = process.env.NODE_ENV === 'production' export const init = async () => { try { @@ -83,7 +84,10 @@ export const track = async ({ eventName, properties }: Track) => { mobileClientVersion: version, mobileClientVersionInclOTA: versionInfo ?? 'unknown' } - amplitudeInstance.logEvent(eventName, propertiesWithContext) + if (!IS_PRODUCTION_BUILD) { + console.info('Amplitude | track', eventName, properties) + } + await amplitudeInstance.logEvent(eventName, propertiesWithContext) } // Screen Event diff --git a/packages/mobile/src/store/storeContext.ts b/packages/mobile/src/store/storeContext.ts index d4f7986aff..e402c2d190 100644 --- a/packages/mobile/src/store/storeContext.ts +++ b/packages/mobile/src/store/storeContext.ts @@ -20,6 +20,7 @@ import { import { trackDownload } from 'app/services/track-download' import { walletClient } from 'app/services/wallet-client' import { createPlaylistArtwork } from 'app/utils/createPlaylistArtwork' +import { reportToSentry } from 'app/utils/reportToSentry' import share from 'app/utils/share' export const storeContext: CommonStoreContext = { @@ -43,6 +44,7 @@ export const storeContext: CommonStoreContext = { metadataProgramId: Config.METADATA_PROGRAM_ID }), sentry: Sentry, + reportToSentry, // Shim in main, but defined in native-reloaded branch audioPlayer, trackDownload, diff --git a/packages/web/src/app.tsx b/packages/web/src/app.tsx index cc7693d790..8a927def4f 100644 --- a/packages/web/src/app.tsx +++ b/packages/web/src/app.tsx @@ -8,8 +8,8 @@ import { LastLocationProvider } from 'react-router-last-location' import { CoinbasePayButtonProvider } from 'components/coinbase-pay-button' import App from 'pages/App' -import AppContext from 'pages/AppContext' import { AppErrorBoundary } from 'pages/AppErrorBoundary' +import AppProviders from 'pages/AppProviders' import { MainContentContext } from 'pages/MainContentContext' import { OAuthLoginPage } from 'pages/oauth-login-page/OAuthLoginPage' import { SomethingWrong } from 'pages/something-wrong/SomethingWrong' @@ -31,7 +31,7 @@ const AudiusApp = () => { > - + {({ mainContentRef }) => ( @@ -53,7 +53,7 @@ const AudiusApp = () => { )} - + diff --git a/packages/web/src/common/store/tipping/sagas.ts b/packages/web/src/common/store/tipping/sagas.ts index f647a790b6..89ed2e8fc6 100644 --- a/packages/web/src/common/store/tipping/sagas.ts +++ b/packages/web/src/common/store/tipping/sagas.ts @@ -392,6 +392,11 @@ function* sendTipAsync() { skipNavigation: true }) ) + yield* put( + make(Name.TIP_UNLOCKED_CHAT, { + recipientUserId: recipient.user_id + }) + ) } }) diff --git a/packages/web/src/pages/AppContext.tsx b/packages/web/src/pages/AppContext.tsx deleted file mode 100644 index 5189af3d3a..0000000000 --- a/packages/web/src/pages/AppContext.tsx +++ /dev/null @@ -1,28 +0,0 @@ -import { RouterContextProvider } from 'components/animated-switch/RouterContextProvider' -import { HeaderContextProvider } from 'components/header/mobile/HeaderContextProvider' -import { NavProvider } from 'components/nav/store/context' -import { ScrollProvider } from 'components/scroll-provider/ScrollProvider' -import { ToastContextProvider } from 'components/toast/ToastContext' -import { MainContentContextProvider } from 'pages/MainContentContext' - -type AppContextProps = { - children: JSX.Element -} - -const AppContext = ({ children }: AppContextProps) => { - return ( - - - - - - {children} - - - - - - ) -} - -export default AppContext diff --git a/packages/web/src/pages/AppProviders.tsx b/packages/web/src/pages/AppProviders.tsx new file mode 100644 index 0000000000..023b2cc1c9 --- /dev/null +++ b/packages/web/src/pages/AppProviders.tsx @@ -0,0 +1,33 @@ +import { AppContext } from '@audius/common' + +import { RouterContextProvider } from 'components/animated-switch/RouterContextProvider' +import { HeaderContextProvider } from 'components/header/mobile/HeaderContextProvider' +import { NavProvider } from 'components/nav/store/context' +import { ScrollProvider } from 'components/scroll-provider/ScrollProvider' +import { ToastContextProvider } from 'components/toast/ToastContext' +import { MainContentContextProvider } from 'pages/MainContentContext' +import * as analytics from 'services/analytics' + +type AppContextProps = { + children: JSX.Element +} + +const AppProviders = ({ children }: AppContextProps) => { + return ( + + + + + + + {children} + + + + + + + ) +} + +export default AppProviders diff --git a/packages/web/src/pages/chat-page/components/ChatMessagePlaylist.tsx b/packages/web/src/pages/chat-page/components/ChatMessagePlaylist.tsx index 048a51f8a5..b477a108df 100644 --- a/packages/web/src/pages/chat-page/components/ChatMessagePlaylist.tsx +++ b/packages/web/src/pages/chat-page/components/ChatMessagePlaylist.tsx @@ -18,10 +18,12 @@ import { SquareSizes, cacheCollectionsActions, cacheCollectionsSelectors, - CommonState + CommonState, + Name } from '@audius/common' import { useDispatch, useSelector } from 'react-redux' +import { make } from 'common/store/analytics/actions' import MobilePlaylistTile from 'components/track/mobile/ConnectedPlaylistTile' const { getUserId } = accountSelectors @@ -122,11 +124,12 @@ export const ChatMessagePlaylist = ({ useEffect(() => { if (collection && uid) { + dispatch(make(Name.MESSAGE_UNFURL_PLAYLIST, {})) onSuccess?.() } else { onEmpty?.() } - }, [collection, uid, onSuccess, onEmpty]) + }, [collection, uid, onSuccess, onEmpty, dispatch]) return collection && uid ? ( // You may wonder why we use the mobile web playlist tile here. diff --git a/packages/web/src/pages/chat-page/components/ChatMessageTrack.tsx b/packages/web/src/pages/chat-page/components/ChatMessageTrack.tsx index 2e948cb227..f2c55810f8 100644 --- a/packages/web/src/pages/chat-page/components/ChatMessageTrack.tsx +++ b/packages/web/src/pages/chat-page/components/ChatMessageTrack.tsx @@ -14,7 +14,8 @@ import { TrackPlayback, ChatMessageTileProps, cacheTracksActions, - SquareSizes + SquareSizes, + Name } from '@audius/common' import { useDispatch, useSelector } from 'react-redux' @@ -76,11 +77,12 @@ export const ChatMessageTrack = ({ useEffect(() => { if (track && uid) { + dispatch(make(Name.MESSAGE_UNFURL_TRACK, {})) onSuccess?.() } else { onEmpty?.() } - }, [track, uid, onSuccess, onEmpty]) + }, [track, uid, onSuccess, onEmpty, dispatch]) return track && uid ? ( // You may wonder why we use the mobile web track tile here. diff --git a/packages/web/src/services/analytics/index.ts b/packages/web/src/services/analytics/index.ts index b12bad201b..a952cb174c 100644 --- a/packages/web/src/services/analytics/index.ts +++ b/packages/web/src/services/analytics/index.ts @@ -1,4 +1,9 @@ -import { AnalyticsEvent, Nullable, BooleanKeys } from '@audius/common' +import { + AnalyticsEvent, + Nullable, + BooleanKeys, + AllTrackingEvents +} from '@audius/common' import { remoteConfigInstance } from 'services/remote-config/remote-config-instance' @@ -105,3 +110,17 @@ export const identify = async ( console.error(err) } } + +/** + * NOTE: Do not use as an action creator. This is to be in parity with mobile for sagas in common + * Use: + * `import { make } from 'common/store/analytics/actions'` + * to dispatch actions + */ +export const make = (event: AllTrackingEvents) => { + const { eventName, ...props } = event + return { + eventName, + properties: props as any + } +} diff --git a/packages/web/src/store/storeContext.ts b/packages/web/src/store/storeContext.ts index e059455763..ccbb5aa1b6 100644 --- a/packages/web/src/store/storeContext.ts +++ b/packages/web/src/store/storeContext.ts @@ -23,6 +23,7 @@ import { isElectron } from 'utils/clientUtil' import { createPlaylistArtwork } from 'utils/imageProcessingUtil' import { share } from 'utils/share' +import { reportToSentry } from './errors/reportToSentry' import { getLineupSelectorForRoute } from './lineup/lineupForRoute' export const storeContext: CommonStoreContext = { @@ -56,6 +57,7 @@ export const storeContext: CommonStoreContext = { metadataProgramId: process.env.REACT_APP_METADATA_PROGRAM_ID }), sentry: Sentry, + reportToSentry, trackDownload, instagramAppId: process.env.REACT_APP_INSTAGRAM_APP_ID, instagramRedirectUrl: process.env.REACT_APP_INSTAGRAM_REDIRECT_URL,