Explorar o código

Segment events (#4349)

* segment manual events

---------

Co-authored-by: Artem <Artem Slugin>
attemka hai 1 ano
pai
achega
5c178929a1

+ 12 - 2
packages/atlas/src/MainLayout.tsx

@@ -1,11 +1,12 @@
 import loadable from '@loadable/component'
 import { FC, useEffect, useRef, useState } from 'react'
-import { Route, Routes, useLocation, useNavigationType } from 'react-router-dom'
+import { Route, Routes, useLocation, useNavigationType, useSearchParams } from 'react-router-dom'
 
 import { StudioLoading } from '@/components/_loaders/StudioLoading'
 import { CookiePopover } from '@/components/_overlays/CookiePopover'
 import { atlasConfig } from '@/config'
 import { BASE_PATHS, absoluteRoutes } from '@/config/routes'
+import { useSegmentAnalytics } from '@/hooks/useSegmentAnalytics'
 import { transitions } from '@/styles'
 import { RoutingState } from '@/types/routing'
 import { isBrowserOutdated } from '@/utils/browser'
@@ -40,6 +41,7 @@ const LoadablePlaygroundLayout = loadable(() => import('./views/playground/Playg
 export const MainLayout: FC = () => {
   const scrollPosition = useRef<number>(0)
   const location = useLocation()
+  const [searchParams] = useSearchParams()
   const navigationType = useNavigationType()
   const [cachedLocation, setCachedLocation] = useState(location)
   const locationState = location.state as RoutingState
@@ -54,11 +56,13 @@ export const MainLayout: FC = () => {
     },
     onExitClick: () => closeDialog(),
   })
+  const { trackPageView } = useSegmentAnalytics()
 
   useEffect(() => {
     if (!atlasConfig.analytics.sentry?.dsn) {
       return
     }
+
     const stopReplay = async () => await SentryLogger.replay?.stop()
 
     if (location.pathname === absoluteRoutes.viewer.ypp()) {
@@ -70,7 +74,13 @@ export const MainLayout: FC = () => {
         stopReplay()
       }
     }
-  }, [location.pathname])
+
+    trackPageView(
+      location.pathname,
+      '',
+      (location.pathname === absoluteRoutes.viewer.ypp() && searchParams.get('referrer')) || undefined
+    )
+  }, [location.pathname, trackPageView, searchParams])
 
   const { clearOverlays } = useOverlayManager()
 

+ 7 - 3
packages/atlas/src/api/hooks/channel.ts

@@ -23,6 +23,7 @@ import {
   useGetTop10ChannelsQuery,
   useUnfollowChannelMutation,
 } from '@/api/queries/__generated__/channels.generated'
+import { useSegmentAnalytics } from '@/hooks/useSegmentAnalytics'
 
 export const useBasicChannel = (
   id: string,
@@ -82,9 +83,11 @@ export const useBasicChannels = (
 
 export const useFollowChannel = (opts?: MutationHookOptions<FollowChannelMutation>) => {
   const [followChannel, rest] = useFollowChannelMutation()
+  const { trackChannelFollow } = useSegmentAnalytics()
   return {
-    followChannel: (id: string) =>
-      followChannel({
+    followChannel: (id: string) => {
+      trackChannelFollow(id)
+      return followChannel({
         ...opts,
         variables: {
           channelId: id,
@@ -100,7 +103,8 @@ export const useFollowChannel = (opts?: MutationHookOptions<FollowChannelMutatio
             },
           })
         },
-      }),
+      })
+    },
     ...rest,
   }
 }

+ 1 - 0
packages/atlas/src/components/_auth/SignInModal/SignInModal.tsx

@@ -114,6 +114,7 @@ export const SignInModal: FC = () => {
         captchaToken: data.captchaToken,
       }
       const response = await faucetMutation(body)
+
       return response.data
     },
     [avatarMutation, faucetMutation]

+ 132 - 7
packages/atlas/src/hooks/useSegmentAnalytics.ts

@@ -2,19 +2,144 @@ import { useCallback } from 'react'
 
 import useSegmentAnalyticsContext from '@/providers/segmentAnalytics/useSegmentAnalyticsContext'
 
-const useAnalytics = () => {
+export const useSegmentAnalytics = () => {
   const { analytics } = useSegmentAnalyticsContext()
 
-  const pageViewed = useCallback(
-    (name: string, category = 'App') => {
-      analytics.page(category, name)
+  const trackPageView = useCallback(
+    (name: string, category = 'App', referrer = 'no data') => {
+      analytics.page(category, name, {
+        referrer,
+      })
+    },
+    [analytics]
+  )
+
+  const trackYppOptIn = useCallback(
+    (handle: string, email: string, category: string, subscribersCount: string) => {
+      analytics.track('ypp opt-in', {
+        handle,
+        email,
+        category,
+        subscribersCount,
+      })
+    },
+    [analytics]
+  )
+
+  const trackAccountCreation = useCallback(
+    (handle: string, email: string) => {
+      analytics.track('account created', {
+        handle,
+        email,
+      })
+    },
+    [analytics]
+  )
+
+  const trackChannelCreation = useCallback(
+    (channelId: string, channelTitle: string, language: string) => {
+      analytics.track('account created', {
+        channelId,
+        channelTitle,
+        language,
+      })
+    },
+    [analytics]
+  )
+
+  const trackVideoView = useCallback(
+    (videoId: string, channelId: string, channelTitle: string, description: string, isNft: boolean) => {
+      analytics.track('video viewed', {
+        channelId,
+        channelTitle,
+        description,
+        isNft,
+      })
+    },
+    [analytics]
+  )
+
+  const trackVideoUpload = useCallback(
+    (title: string, channelId: string) => {
+      analytics.track('video uploaded', {
+        channelId,
+        title,
+      })
+    },
+    [analytics]
+  )
+
+  const trackNftMint = useCallback(
+    (title: string, channelId: string) => {
+      analytics.track('nft minted', {
+        title,
+        channelId,
+      })
+    },
+    [analytics]
+  )
+
+  const trackNftSale = useCallback(
+    (saleType: string, price: string) => {
+      analytics.track('nft put on sale', {
+        saleType,
+        price,
+      })
+    },
+    [analytics]
+  )
+
+  const trackCommentAdded = useCallback(
+    (commentBody: string, videoId: string) => {
+      analytics.track('comment added', {
+        commentBody,
+        videoId,
+      })
+    },
+    [analytics]
+  )
+
+  const trackLikeAdded = useCallback(
+    (videoId: string, memberId: string) => {
+      analytics.track('like added', {
+        memberId,
+        videoId,
+      })
+    },
+    [analytics]
+  )
+
+  const trackDislikeAdded = useCallback(
+    (videoId: string, memberId: string) => {
+      analytics.track('dislike added', {
+        memberId,
+        videoId,
+      })
+    },
+    [analytics]
+  )
+
+  const trackChannelFollow = useCallback(
+    (channelId: string) => {
+      analytics.track('channel followed', {
+        channelId,
+      })
     },
     [analytics]
   )
 
   return {
-    pageViewed,
+    trackPageView,
+    trackYppOptIn,
+    trackAccountCreation,
+    trackChannelCreation,
+    trackVideoView,
+    trackVideoUpload,
+    trackNftMint,
+    trackNftSale,
+    trackCommentAdded,
+    trackLikeAdded,
+    trackDislikeAdded,
+    trackChannelFollow,
   }
 }
-
-export default useAnalytics

+ 5 - 1
packages/atlas/src/providers/uploads/uploads.manager.tsx

@@ -6,6 +6,7 @@ import shallow from 'zustand/shallow'
 import { useDataObjectsAvailabilityLazy } from '@/api/hooks/dataObject'
 import { atlasConfig } from '@/config'
 import { absoluteRoutes } from '@/config/routes'
+import { useSegmentAnalytics } from '@/hooks/useSegmentAnalytics'
 import { fetchMissingAssets } from '@/providers/uploads/uploads.utils'
 import { useUser } from '@/providers/user/user.hooks'
 import { AssetUploadStatus } from '@/types/storage'
@@ -26,6 +27,7 @@ export const UploadsManager: FC = () => {
   const { channelId } = useUser()
   const [cachedChannelId, setCachedChannelId] = useState<string | null>(null)
   const videoAssetsRef = useRef<VideoAssets[]>([])
+  const { trackVideoUpload } = useSegmentAnalytics()
 
   const { displaySnackbar } = useSnackbar()
   const { assetsFiles, channelUploads, uploadStatuses, isSyncing, processingAssets, newChannelsIds } = useUploadsStore(
@@ -72,10 +74,12 @@ export const UploadsManager: FC = () => {
             onActionClick: () => openInNewTab(absoluteRoutes.viewer.video(video.parentObject.id), true),
           })
         }
+
+        trackVideoUpload(video.parentObject?.title ?? 'no data', channelId ?? 'no data')
       })
       videoAssetsRef.current = videoAssets
     }
-  }, [assetsFiles, displaySnackbar, navigate, videoAssets])
+  }, [assetsFiles, displaySnackbar, navigate, videoAssets, channelId, trackVideoUpload])
 
   const initialRender = useRef(true)
   useEffect(() => {

+ 15 - 1
packages/atlas/src/views/global/NftSaleBottomDrawer/NftSaleBottomDrawer.tsx

@@ -5,6 +5,7 @@ import { GetNftDocument, GetNftQuery, GetNftQueryVariables } from '@/api/queries
 import { ActionBarProps } from '@/components/ActionBar'
 import { BottomDrawer } from '@/components/_overlays/BottomDrawer'
 import { absoluteRoutes } from '@/config/routes'
+import { useSegmentAnalytics } from '@/hooks/useSegmentAnalytics'
 import { useConfirmationModal } from '@/providers/confirmationModal'
 import { useJoystream } from '@/providers/joystream/joystream.hooks'
 import { useNftActions } from '@/providers/nftActions/nftActions.hooks'
@@ -20,6 +21,7 @@ const SUCCESS_SNACKBAR_TIMEOUT = 6000
 
 export const NftSaleBottomDrawer: FC = () => {
   const { currentAction, currentNftId, closeNftAction } = useNftActions()
+
   const [formStatus, setFormStatus] = useState<NftFormStatus | null>(null)
   const [openPuttingOnSaleDialog, closeCancelPuttingOnSaleDialog] = useConfirmationModal({
     type: 'warning',
@@ -44,6 +46,7 @@ export const NftSaleBottomDrawer: FC = () => {
   const handleTransaction = useTransaction()
   const client = useApolloClient()
   const { displaySnackbar } = useSnackbar()
+  const { trackNftSale } = useSegmentAnalytics()
 
   const isOpen = currentAction === 'putOnSale'
 
@@ -73,6 +76,7 @@ export const NftSaleBottomDrawer: FC = () => {
         onTxSync: refetchData,
       })
       if (completed) {
+        trackNftSale(data.type, data.type === 'buyNow' ? data.buyNowPrice : data.startingPrice)
         displaySnackbar({
           customId: currentNftId,
           title: 'NFT put on sale successfully',
@@ -84,7 +88,17 @@ export const NftSaleBottomDrawer: FC = () => {
         closeNftAction()
       }
     },
-    [memberId, client, closeNftAction, currentNftId, displaySnackbar, handleTransaction, joystream, proxyCallback]
+    [
+      memberId,
+      client,
+      closeNftAction,
+      currentNftId,
+      displaySnackbar,
+      handleTransaction,
+      joystream,
+      proxyCallback,
+      trackNftSale,
+    ]
   )
 
   const handleCancel = useCallback(() => {

+ 12 - 0
packages/atlas/src/views/global/YppLandingView/YppAuthorizationModal/YppAuthorizationModal.tsx

@@ -17,6 +17,7 @@ import { DialogModal } from '@/components/_overlays/DialogModal'
 import { atlasConfig } from '@/config'
 import { absoluteRoutes } from '@/config/routes'
 import { useMediaMatch } from '@/hooks/useMediaMatch'
+import { useSegmentAnalytics } from '@/hooks/useSegmentAnalytics'
 import { useChannelsStorageBucketsCount } from '@/providers/assets/assets.hooks'
 import { useBloatFeesAndPerMbFees, useJoystream } from '@/providers/joystream/joystream.hooks'
 import { useOverlayManager } from '@/providers/overlayManager'
@@ -119,6 +120,7 @@ export const YppAuthorizationModal: FC<YppAuthorizationModalProps> = ({
 
   const handleTransaction = useTransaction()
   const { displaySnackbar } = useSnackbar()
+  const { trackYppOptIn } = useSegmentAnalytics()
 
   const {
     handleAuthorizeClick,
@@ -204,6 +206,13 @@ export const YppAuthorizationModal: FC<YppAuthorizationModalProps> = ({
       })
       if (completed) {
         setTimeout(() => {
+          const { email, videoCategoryId } = finalFormData || {}
+          trackYppOptIn(
+            yppCurrentChannel?.title || '',
+            email ?? '',
+            videoCategoryId ?? '',
+            yppCurrentChannel?.subscribersCount.toString() ?? '0'
+          )
           onChangeStep('summary')
         }, 2000)
       }
@@ -234,6 +243,9 @@ export const YppAuthorizationModal: FC<YppAuthorizationModalProps> = ({
     proxyCallback,
     onChangeStep,
     displaySnackbar,
+    yppCurrentChannel?.subscribersCount,
+    yppCurrentChannel?.title,
+    trackYppOptIn,
   ])
 
   useEffect(() => {

+ 3 - 0
packages/atlas/src/views/studio/CreateEditChannelView/CreateEditChannelView.tsx

@@ -26,6 +26,7 @@ import { atlasConfig } from '@/config'
 import { absoluteRoutes } from '@/config/routes'
 import { useHeadTags } from '@/hooks/useHeadTags'
 import { useMediaMatch } from '@/hooks/useMediaMatch'
+import { useSegmentAnalytics } from '@/hooks/useSegmentAnalytics'
 import { ChannelInputAssets, ChannelInputMetadata } from '@/joystream-lib/types'
 import { useChannelsStorageBucketsCount } from '@/providers/assets/assets.hooks'
 import { useConnectionStatusStore } from '@/providers/connectionStatus'
@@ -76,6 +77,7 @@ export const CreateEditChannelView: FC<CreateEditChannelViewProps> = ({ newChann
   const { ref: actionBarRef, height: actionBarBoundsHeight = 0 } = useResizeObserver({ box: 'border-box' })
   const handleChannelSubmit = useCreateEditChannelSubmit()
   const smMatch = useMediaMatch('sm')
+  const { trackChannelCreation } = useSegmentAnalytics()
 
   const [showConnectToYtDialog, setShowConnectToYtDialog] = useState(false)
   const setShouldContinueYppFlow = useYppStore((store) => store.actions.setShouldContinueYppFlow)
@@ -310,6 +312,7 @@ export const CreateEditChannelView: FC<CreateEditChannelViewProps> = ({ newChann
         atlasConfig.features.ypp.youtubeSyncApiUrl &&
         setTimeout(() => setShowConnectToYtDialog(true), 2000)
     )
+    trackChannelCreation(channel?.id ?? 'no data', channel?.title ?? 'no data', channel?.language ?? 'no data')
   })
 
   const handleCoverChange: ImageCropModalProps['onConfirm'] = (

+ 5 - 0
packages/atlas/src/views/studio/VideoWorkspace/VideoWorkspace.hooks.ts

@@ -9,6 +9,7 @@ import {
   GetFullVideosConnectionQueryVariables,
 } from '@/api/queries/__generated__/videos.generated'
 import { atlasConfig } from '@/config'
+import { useSegmentAnalytics } from '@/hooks/useSegmentAnalytics'
 import { VideoExtrinsicResult, VideoInputAssets } from '@/joystream-lib/types'
 import { useChannelsStorageBucketsCount } from '@/providers/assets/assets.hooks'
 import { useDraftStore } from '@/providers/drafts'
@@ -42,6 +43,7 @@ export const useHandleVideoWorkspaceSubmit = () => {
   const { tabData } = useVideoWorkspaceData()
   const channelBucketsCount = useChannelsStorageBucketsCount(channelId)
   const { videoStateBloatBondValue, dataObjectStateBloatBondValue } = useBloatFeesAndPerMbFees()
+  const { trackNftMint } = useSegmentAnalytics()
 
   const rawMetadataProcessor = useAppActionMetadataProcessor(channelId, AppActionActionType.CreateVideo)
 
@@ -242,6 +244,8 @@ export const useHandleVideoWorkspaceSubmit = () => {
       })
 
       if (completed) {
+        !!data.nftMetadata && trackNftMint(data.metadata.title ?? 'no data', channelId)
+
         assetsToBeRemoved?.forEach((asset) => {
           removeAssetFromUploads(asset)
         })
@@ -275,6 +279,7 @@ export const useHandleVideoWorkspaceSubmit = () => {
       isNftMintDismissed,
       removeAssetFromUploads,
       setShowFistMintDialog,
+      trackNftMint,
     ]
   )
 }

+ 5 - 0
packages/atlas/src/views/viewer/VideoView/CommentsSection.tsx

@@ -18,6 +18,7 @@ import { useDisplaySignInDialog } from '@/hooks/useDisplaySignInDialog'
 import { useMediaMatch } from '@/hooks/useMediaMatch'
 import { useReactionTransactions } from '@/hooks/useReactionTransactions'
 import { useRouterQuery } from '@/hooks/useRouterQuery'
+import { useSegmentAnalytics } from '@/hooks/useSegmentAnalytics'
 import { getMemberAvatar } from '@/providers/assets/assets.helpers'
 import { useFee } from '@/providers/joystream/joystream.hooks'
 import { useUser } from '@/providers/user/user.hooks'
@@ -55,6 +56,7 @@ export const CommentsSection: FC<CommentsSectionProps> = ({ disabled, video, vid
   const { memberId, signIn, activeMembership, isLoggedIn } = useUser()
   const { openSignInDialog } = useDisplaySignInDialog({ interaction: true })
   const { isLoadingAsset: isMemberAvatarLoading, url: memberAvatarUrl } = getMemberAvatar(activeMembership)
+  const { trackCommentAdded } = useSegmentAnalytics()
 
   const { fullFee: fee, loading: feeLoading } = useFee(
     'createVideoCommentTx',
@@ -147,6 +149,9 @@ export const CommentsSection: FC<CommentsSectionProps> = ({ disabled, video, vid
       videoTitle: video?.title,
     })
     setCommentInputIsProcessing(false)
+
+    trackCommentAdded(commentInputText, video?.id ?? 'no data')
+
     if (newCommentId) {
       setCommentInputText('')
       setHighlightedCommentId(newCommentId || null)

+ 28 - 2
packages/atlas/src/views/viewer/VideoView/VideoView.tsx

@@ -28,6 +28,7 @@ import { useHeadTags } from '@/hooks/useHeadTags'
 import { useMediaMatch } from '@/hooks/useMediaMatch'
 import { useNftTransactions } from '@/hooks/useNftTransactions'
 import { useReactionTransactions } from '@/hooks/useReactionTransactions'
+import { useSegmentAnalytics } from '@/hooks/useSegmentAnalytics'
 import { useVideoStartTimestamp } from '@/hooks/useVideoStartTimestamp'
 import { VideoReaction } from '@/joystream-lib/types'
 import { useFee } from '@/providers/joystream/joystream.hooks'
@@ -93,6 +94,7 @@ export const VideoView: FC = () => {
   const nftWidgetProps = useNftWidget(video)
   const { likeOrDislikeVideo } = useReactionTransactions()
   const { withdrawBid } = useNftTransactions()
+  const { trackVideoView, trackLikeAdded, trackDislikeAdded } = useSegmentAnalytics()
 
   const mdMatch = useMediaMatch('md')
   const { addVideoView } = useAddVideoView()
@@ -149,6 +151,7 @@ export const VideoView: FC = () => {
   const channelId = video?.channel?.id
   const channelName = video?.channel?.title
   const videoId = video?.id
+  const videoDescription = video?.description
   const numberOfLikes = video?.reactions.filter(({ reaction }) => reaction === 'LIKE').length
   const numberOfDislikes = video?.reactions.filter(({ reaction }) => reaction === 'UNLIKE').length
   const videoNotAvailable = !loading && !video
@@ -208,12 +211,27 @@ export const VideoView: FC = () => {
         setVideoReactionProcessing(true)
         const fee = reactionFee || (await getReactionFee([memberId || '', video?.id, reaction]))
         const reacted = await likeOrDislikeVideo(video.id, reaction, video.title, fee)
+        reaction === 'like'
+          ? trackLikeAdded(video.id, memberId ?? 'no data')
+          : trackDislikeAdded(video.id, memberId ?? 'no data')
         setVideoReactionProcessing(false)
         return reacted
       }
       return false
     },
-    [getReactionFee, isLoggedIn, likeOrDislikeVideo, memberId, openSignInDialog, reactionFee, signIn, video]
+    [
+      getReactionFee,
+      isLoggedIn,
+      likeOrDislikeVideo,
+      memberId,
+      openSignInDialog,
+      reactionFee,
+      signIn,
+      trackLikeAdded,
+      trackDislikeAdded,
+      video?.id,
+      video?.title,
+    ]
   )
 
   // use Media Session API to provide rich metadata to the browser
@@ -252,7 +270,15 @@ export const VideoView: FC = () => {
     }).catch((error) => {
       SentryLogger.error('Failed to increase video views', 'VideoView', error)
     })
-  }, [addVideoView, channelId, videoId])
+
+    trackVideoView(
+      videoId ?? 'no data',
+      channelId ?? 'no data',
+      channelName ?? 'no data',
+      videoDescription ?? 'no data',
+      !!nftWidgetProps
+    )
+  }, [addVideoView, channelId, videoId, channelName, videoDescription, nftWidgetProps, trackVideoView])
 
   if (error) {
     return <ViewErrorFallback />