Bluesky app fork with some witchin' additions 💫
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

[Statsig] Track likes, reposts, follows (#3195)

* [Statsig] Track likes

* Move tracking to intent

* Track repost/unrepost

* Track profile follows/unfollows

* Less copy paste

* Reorder

authored by

dan and committed by
GitHub
7eaa573b db79c918

+125 -30
+31 -1
src/lib/statsig/events.ts
··· 1 - export type Events = { 1 + export type LogEvents = { 2 2 init: { 3 3 initMs: number 4 + } 5 + 'post:like': { 6 + logContext: 'FeedItem' | 'PostThreadItem' | 'Post' 7 + } 8 + 'post:repost': { 9 + logContext: 'FeedItem' | 'PostThreadItem' | 'Post' 10 + } 11 + 'post:unlike': { 12 + logContext: 'FeedItem' | 'PostThreadItem' | 'Post' 13 + } 14 + 'post:unrepost': { 15 + logContext: 'FeedItem' | 'PostThreadItem' | 'Post' 16 + } 17 + 'profile:follow': { 18 + logContext: 19 + | 'RecommendedFollowsItem' 20 + | 'PostThreadItem' 21 + | 'ProfileCard' 22 + | 'ProfileHeader' 23 + | 'ProfileHeaderSuggestedFollows' 24 + | 'ProfileMenu' 25 + } 26 + 'profile:unfollow': { 27 + logContext: 28 + | 'RecommendedFollowsItem' 29 + | 'PostThreadItem' 30 + | 'ProfileCard' 31 + | 'ProfileHeader' 32 + | 'ProfileHeaderSuggestedFollows' 33 + | 'ProfileMenu' 4 34 } 5 35 }
+5 -3
src/lib/statsig/statsig.tsx
··· 6 6 } from 'statsig-react-native-expo' 7 7 import {useSession} from '../../state/session' 8 8 import {sha256} from 'js-sha256' 9 - import {Events} from './events' 9 + import {LogEvents} from './events' 10 + 11 + export type {LogEvents} 10 12 11 13 const statsigOptions = { 12 14 environment: { ··· 31 33 getCurrentRouteName = getRouteName 32 34 } 33 35 34 - export function logEvent<E extends keyof Events>( 36 + export function logEvent<E extends keyof LogEvents>( 35 37 eventName: E & string, 36 - rawMetadata?: Events[E] & FlatJSONRecord, 38 + rawMetadata: LogEvents[E] & FlatJSONRecord, 37 39 ) { 38 40 const fullMetadata = { 39 41 ...rawMetadata,
+35 -12
src/state/queries/post.ts
··· 5 5 import {getAgent} from '#/state/session' 6 6 import {updatePostShadow} from '#/state/cache/post-shadow' 7 7 import {track} from '#/lib/analytics/analytics' 8 + import {logEvent, LogEvents} from '#/lib/statsig/statsig' 8 9 import {useToggleMutationQueue} from '#/lib/hooks/useToggleMutationQueue' 9 10 10 11 export const RQKEY = (postUri: string) => ['post', postUri] ··· 58 59 59 60 export function usePostLikeMutationQueue( 60 61 post: Shadow<AppBskyFeedDefs.PostView>, 62 + logContext: LogEvents['post:like']['logContext'] & 63 + LogEvents['post:unlike']['logContext'], 61 64 ) { 62 65 const postUri = post.uri 63 66 const postCid = post.cid 64 67 const initialLikeUri = post.viewer?.like 65 - const likeMutation = usePostLikeMutation() 66 - const unlikeMutation = usePostUnlikeMutation() 68 + const likeMutation = usePostLikeMutation(logContext) 69 + const unlikeMutation = usePostUnlikeMutation(logContext) 67 70 68 71 const queueToggle = useToggleMutationQueue({ 69 72 initialState: initialLikeUri, ··· 111 114 return [queueLike, queueUnlike] 112 115 } 113 116 114 - function usePostLikeMutation() { 117 + function usePostLikeMutation(logContext: LogEvents['post:like']['logContext']) { 115 118 return useMutation< 116 119 {uri: string}, // responds with the uri of the like 117 120 Error, 118 121 {uri: string; cid: string} // the post's uri and cid 119 122 >({ 120 - mutationFn: post => getAgent().like(post.uri, post.cid), 123 + mutationFn: post => { 124 + logEvent('post:like', {logContext}) 125 + return getAgent().like(post.uri, post.cid) 126 + }, 121 127 onSuccess() { 122 128 track('Post:Like') 123 129 }, 124 130 }) 125 131 } 126 132 127 - function usePostUnlikeMutation() { 133 + function usePostUnlikeMutation( 134 + logContext: LogEvents['post:unlike']['logContext'], 135 + ) { 128 136 return useMutation<void, Error, {postUri: string; likeUri: string}>({ 129 - mutationFn: ({likeUri}) => getAgent().deleteLike(likeUri), 137 + mutationFn: ({likeUri}) => { 138 + logEvent('post:unlike', {logContext}) 139 + return getAgent().deleteLike(likeUri) 140 + }, 130 141 onSuccess() { 131 142 track('Post:Unlike') 132 143 }, ··· 135 146 136 147 export function usePostRepostMutationQueue( 137 148 post: Shadow<AppBskyFeedDefs.PostView>, 149 + logContext: LogEvents['post:repost']['logContext'] & 150 + LogEvents['post:unrepost']['logContext'], 138 151 ) { 139 152 const postUri = post.uri 140 153 const postCid = post.cid 141 154 const initialRepostUri = post.viewer?.repost 142 - const repostMutation = usePostRepostMutation() 143 - const unrepostMutation = usePostUnrepostMutation() 155 + const repostMutation = usePostRepostMutation(logContext) 156 + const unrepostMutation = usePostUnrepostMutation(logContext) 144 157 145 158 const queueToggle = useToggleMutationQueue({ 146 159 initialState: initialRepostUri, ··· 188 201 return [queueRepost, queueUnrepost] 189 202 } 190 203 191 - function usePostRepostMutation() { 204 + function usePostRepostMutation( 205 + logContext: LogEvents['post:repost']['logContext'], 206 + ) { 192 207 return useMutation< 193 208 {uri: string}, // responds with the uri of the repost 194 209 Error, 195 210 {uri: string; cid: string} // the post's uri and cid 196 211 >({ 197 - mutationFn: post => getAgent().repost(post.uri, post.cid), 212 + mutationFn: post => { 213 + logEvent('post:repost', {logContext}) 214 + return getAgent().repost(post.uri, post.cid) 215 + }, 198 216 onSuccess() { 199 217 track('Post:Repost') 200 218 }, 201 219 }) 202 220 } 203 221 204 - function usePostUnrepostMutation() { 222 + function usePostUnrepostMutation( 223 + logContext: LogEvents['post:unrepost']['logContext'], 224 + ) { 205 225 return useMutation<void, Error, {postUri: string; repostUri: string}>({ 206 - mutationFn: ({repostUri}) => getAgent().deleteRepost(repostUri), 226 + mutationFn: ({repostUri}) => { 227 + logEvent('post:unrepost', {logContext}) 228 + return getAgent().deleteRepost(repostUri) 229 + }, 207 230 onSuccess() { 208 231 track('Post:Unrepost') 209 232 },
+13 -4
src/state/queries/profile.ts
··· 26 26 import {RQKEY as RQKEY_MY_BLOCKED} from './my-blocked-accounts' 27 27 import {STALE} from '#/state/queries' 28 28 import {track} from '#/lib/analytics/analytics' 29 + import {logEvent, LogEvents} from '#/lib/statsig/statsig' 29 30 import {ThreadNode} from './post-thread' 30 31 31 32 export const RQKEY = (did: string) => ['profile', did] ··· 186 187 187 188 export function useProfileFollowMutationQueue( 188 189 profile: Shadow<AppBskyActorDefs.ProfileViewDetailed>, 190 + logContext: LogEvents['profile:follow']['logContext'] & 191 + LogEvents['profile:unfollow']['logContext'], 189 192 ) { 190 193 const did = profile.did 191 194 const initialFollowingUri = profile.viewer?.following 192 - const followMutation = useProfileFollowMutation() 193 - const unfollowMutation = useProfileUnfollowMutation() 195 + const followMutation = useProfileFollowMutation(logContext) 196 + const unfollowMutation = useProfileUnfollowMutation(logContext) 194 197 195 198 const queueToggle = useToggleMutationQueue({ 196 199 initialState: initialFollowingUri, ··· 237 240 return [queueFollow, queueUnfollow] 238 241 } 239 242 240 - function useProfileFollowMutation() { 243 + function useProfileFollowMutation( 244 + logContext: LogEvents['profile:follow']['logContext'], 245 + ) { 241 246 return useMutation<{uri: string; cid: string}, Error, {did: string}>({ 242 247 mutationFn: async ({did}) => { 248 + logEvent('profile:follow', {logContext}) 243 249 return await getAgent().follow(did) 244 250 }, 245 251 onSuccess(data, variables) { ··· 248 254 }) 249 255 } 250 256 251 - function useProfileUnfollowMutation() { 257 + function useProfileUnfollowMutation( 258 + logContext: LogEvents['profile:unfollow']['logContext'], 259 + ) { 252 260 return useMutation<void, Error, {did: string; followUri: string}>({ 253 261 mutationFn: async ({followUri}) => { 262 + logEvent('profile:unfollow', {logContext}) 254 263 track('Profile:Unfollow', {username: followUri}) 255 264 return await getAgent().deleteFollow(followUri) 256 265 },
+5 -2
src/view/com/auth/onboarding/RecommendedFollowsItem.tsx
··· 56 56 ) 57 57 } 58 58 59 - export function ProfileCard({ 59 + function ProfileCard({ 60 60 profile, 61 61 onFollowStateChange, 62 62 moderation, ··· 72 72 const pal = usePalette('default') 73 73 const [addingMoreSuggestions, setAddingMoreSuggestions] = 74 74 React.useState(false) 75 - const [queueFollow, queueUnfollow] = useProfileFollowMutationQueue(profile) 75 + const [queueFollow, queueUnfollow] = useProfileFollowMutationQueue( 76 + profile, 77 + 'RecommendedFollowsItem', 78 + ) 76 79 77 80 const onToggleFollow = React.useCallback(async () => { 78 81 try {
+4 -1
src/view/com/post-thread/PostThreadFollowBtn.tsx
··· 42 42 const {isTabletOrDesktop} = useWebMediaQueries() 43 43 const profile: Shadow<AppBskyActorDefs.ProfileViewBasic> = 44 44 useProfileShadow(profileUnshadowed) 45 - const [queueFollow, queueUnfollow] = useProfileFollowMutationQueue(profile) 45 + const [queueFollow, queueUnfollow] = useProfileFollowMutationQueue( 46 + profile, 47 + 'PostThreadItem', 48 + ) 46 49 const requireAuth = useRequireAuth() 47 50 48 51 const isFollowing = !!profile.viewer?.following
+2
src/view/com/post-thread/PostThreadItem.tsx
··· 407 407 record={record} 408 408 richText={richText} 409 409 onPressReply={onPressReply} 410 + logContext="PostThreadItem" 410 411 /> 411 412 </View> 412 413 </View> ··· 560 561 record={record} 561 562 richText={richText} 562 563 onPressReply={onPressReply} 564 + logContext="PostThreadItem" 563 565 /> 564 566 </View> 565 567 </View>
+1
src/view/com/post/Post.tsx
··· 220 220 record={record} 221 221 richText={richText} 222 222 onPressReply={onPressReply} 223 + logContext="Post" 223 224 /> 224 225 </View> 225 226 </View>
+1
src/view/com/posts/FeedItem.tsx
··· 310 310 showAppealLabelItem={ 311 311 post.author.did === currentAccount?.did && isModeratedPost 312 312 } 313 + logContext="FeedItem" 313 314 /> 314 315 </View> 315 316 </View>
+6 -1
src/view/com/profile/FollowButton.tsx
··· 13 13 followedType = 'default', 14 14 profile, 15 15 labelStyle, 16 + logContext, 16 17 }: { 17 18 unfollowedType?: ButtonType 18 19 followedType?: ButtonType 19 20 profile: Shadow<AppBskyActorDefs.ProfileViewBasic> 20 21 labelStyle?: StyleProp<TextStyle> 22 + logContext: 'ProfileCard' 21 23 }) { 22 - const [queueFollow, queueUnfollow] = useProfileFollowMutationQueue(profile) 24 + const [queueFollow, queueUnfollow] = useProfileFollowMutationQueue( 25 + profile, 26 + logContext, 27 + ) 23 28 const {_} = useLingui() 24 29 25 30 const onPressFollow = async () => {
+3 -1
src/view/com/profile/ProfileCard.tsx
··· 230 230 renderButton={ 231 231 isMe 232 232 ? undefined 233 - : profileShadow => <FollowButton profile={profileShadow} /> 233 + : profileShadow => ( 234 + <FollowButton profile={profileShadow} logContext="ProfileCard" /> 235 + ) 234 236 } 235 237 /> 236 238 )
+4 -1
src/view/com/profile/ProfileHeader.tsx
··· 103 103 const invalidHandle = isInvalidHandle(profile.handle) 104 104 const {isDesktop} = useWebMediaQueries() 105 105 const [showSuggestedFollows, setShowSuggestedFollows] = React.useState(false) 106 - const [queueFollow, queueUnfollow] = useProfileFollowMutationQueue(profile) 106 + const [queueFollow, queueUnfollow] = useProfileFollowMutationQueue( 107 + profile, 108 + 'ProfileHeader', 109 + ) 107 110 const [__, queueUnblock] = useProfileBlockMutationQueue(profile) 108 111 const unblockPromptControl = Prompt.usePromptControl() 109 112 const moderation = useMemo(
+4 -1
src/view/com/profile/ProfileHeaderSuggestedFollows.tsx
··· 170 170 const pal = usePalette('default') 171 171 const moderationOpts = useModerationOpts() 172 172 const profile = useProfileShadow(profileUnshadowed) 173 - const [queueFollow, queueUnfollow] = useProfileFollowMutationQueue(profile) 173 + const [queueFollow, queueUnfollow] = useProfileFollowMutationQueue( 174 + profile, 175 + 'ProfileHeaderSuggestedFollows', 176 + ) 174 177 175 178 const onPressFollow = React.useCallback(async () => { 176 179 try {
+4 -1
src/view/com/profile/ProfileMenu.tsx
··· 52 52 53 53 const [queueMute, queueUnmute] = useProfileMuteMutationQueue(profile) 54 54 const [queueBlock, queueUnblock] = useProfileBlockMutationQueue(profile) 55 - const [, queueUnfollow] = useProfileFollowMutationQueue(profile) 55 + const [, queueUnfollow] = useProfileFollowMutationQueue( 56 + profile, 57 + 'ProfileMenu', 58 + ) 56 59 57 60 const blockPromptControl = Prompt.usePromptControl() 58 61
+7 -2
src/view/com/util/post-ctrls/PostCtrls.tsx
··· 44 44 showAppealLabelItem, 45 45 style, 46 46 onPressReply, 47 + logContext, 47 48 }: { 48 49 big?: boolean 49 50 post: Shadow<AppBskyFeedDefs.PostView> ··· 52 53 showAppealLabelItem?: boolean 53 54 style?: StyleProp<ViewStyle> 54 55 onPressReply: () => void 56 + logContext: 'FeedItem' | 'PostThreadItem' | 'Post' 55 57 }): React.ReactNode => { 56 58 const theme = useTheme() 57 59 const {_} = useLingui() 58 60 const {openComposer} = useComposerControls() 59 61 const {closeModal} = useModalControls() 60 - const [queueLike, queueUnlike] = usePostLikeMutationQueue(post) 61 - const [queueRepost, queueUnrepost] = usePostRepostMutationQueue(post) 62 + const [queueLike, queueUnlike] = usePostLikeMutationQueue(post, logContext) 63 + const [queueRepost, queueUnrepost] = usePostRepostMutationQueue( 64 + post, 65 + logContext, 66 + ) 62 67 const requireAuth = useRequireAuth() 63 68 64 69 const defaultCtrlColor = React.useMemo(