This repository has no description
0

Configure Feed

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

posthog capture card saved with additional metadata (source, filter, etc)

+249 -36
+36
src/webapp/features/analytics/types.ts
··· 1 + export enum CardSaveSource { 2 + PROFILE = 'profile', 3 + SEMBLE_PAGE = 'semble_page', 4 + COLLECTION = 'collection', 5 + FEED = 'feed', 6 + SIMILAR_CARDS = 'similar_cards', 7 + NOTIFICATIONS = 'notifications', 8 + SEARCH_RESULTS = 'search_results', 9 + ADD_CARD_DRAWER = 'add_card_drawer', 10 + } 11 + 12 + export interface CardSaveAnalyticsContext { 13 + saveSource: CardSaveSource; 14 + activeFilters?: { 15 + urlType?: string; 16 + sort?: string; 17 + searchQuery?: string; 18 + profileFilter?: string; 19 + }; 20 + pagePath?: string; 21 + } 22 + 23 + export interface CardSaveEventProperties { 24 + save_source: CardSaveSource; 25 + is_new_card: boolean; 26 + has_note: boolean; 27 + collection_count: number; 28 + active_filters?: { 29 + url_type?: string; 30 + sort?: string; 31 + search_query?: string; 32 + profile_filter?: string; 33 + }; 34 + via_card_id?: string; 35 + page_path?: string; 36 + }
+11
src/webapp/features/analytics/utils.ts
··· 1 + import posthog from 'posthog-js'; 2 + 3 + /** 4 + * Check if analytics should be captured 5 + * Centralizes the logic for when to track events 6 + */ 7 + export function shouldCaptureAnalytics(): boolean { 8 + return ( 9 + process.env.NEXT_PUBLIC_VERCEL_ENV === 'production' && posthog.__loaded 10 + ); 11 + }
+7 -3
src/webapp/features/cards/components/addCardDrawer/AddCardDrawer.tsx
··· 29 29 import MarginLogo from '@/components/MarginLogo'; 30 30 import { Collection, CollectionAccessType } from '@semble/types'; 31 31 import { FaSeedling } from 'react-icons/fa6'; 32 - import posthog from 'posthog-js'; 32 + import { CardSaveSource } from '@/features/analytics/types'; 33 + import { usePathname } from 'next/navigation'; 33 34 34 35 interface Props { 35 36 isOpen: boolean; ··· 39 40 } 40 41 41 42 export default function AddCardDrawer(props: Props) { 43 + const pathname = usePathname(); 42 44 const [collectionSelectorOpened, { toggle: toggleCollectionSelector }] = 43 45 useDisclosure(false); 44 46 const initialCollections = props.selectedCollection ··· 51 53 const myCollections = 52 54 collections?.pages.flatMap((page) => page.collections ?? []) ?? []; 53 55 54 - const addCard = useAddCard(); 56 + const addCard = useAddCard({ 57 + saveSource: CardSaveSource.ADD_CARD_DRAWER, 58 + pagePath: pathname, 59 + }); 55 60 56 61 const form = useForm({ 57 62 initialValues: { ··· 73 78 const handleAddCard = (e: React.FormEvent) => { 74 79 e.preventDefault(); 75 80 track('add new card'); 76 - posthog.capture('add_new_card'); 77 81 78 82 addCard.mutate( 79 83 {
+3
src/webapp/features/cards/components/addCardToModal/AddCardToModal.tsx
··· 5 5 import CollectionSelectorSkeleton from '@/features/collections/components/collectionSelector/Skeleton.CollectionSelector'; 6 6 import AddCardToModalContent from './AddCardToModalContent'; 7 7 import CardToBeAddedPreview from '../cardToBeAddedPreview/CardToBeAddedPreview'; 8 + import { CardSaveAnalyticsContext } from '@/features/analytics/types'; 8 9 9 10 interface Props { 10 11 isOpen: boolean; ··· 16 17 urlLibraryCount: number; 17 18 viaCardId?: string; 18 19 cardContent?: UrlCard['cardContent']; 20 + analyticsContext?: CardSaveAnalyticsContext; 19 21 } 20 22 21 23 export default function AddCardToModal(props: Props) { ··· 63 65 cardContent={props.cardContent} 64 66 note={props.note} 65 67 viaCardId={props.viaCardId} 68 + analyticsContext={props.analyticsContext} 66 69 /> 67 70 </Suspense> 68 71 </Stack>
+10 -4
src/webapp/features/cards/components/addCardToModal/AddCardToModalContent.tsx
··· 12 12 import useAddCard from '@/features/cards/lib/mutations/useAddCard'; 13 13 import { track } from '@vercel/analytics'; 14 14 import AddCardActions from '../addCardActions/AddCardActions'; 15 - import posthog from 'posthog-js'; 15 + import { CardSaveAnalyticsContext } from '@/features/analytics/types'; 16 16 17 17 interface Props { 18 18 onClose: () => void; ··· 21 21 note?: string; 22 22 cardContent?: UrlCard['cardContent']; 23 23 viaCardId?: string; 24 + analyticsContext?: CardSaveAnalyticsContext; 24 25 } 25 26 26 27 export default function AddCardToModalContent(props: Props) { ··· 29 30 const [note, setNote] = useState(isMyCard ? props.note : ''); 30 31 const { data, error } = useMyCollections(); 31 32 32 - const addCard = useAddCard(); 33 - const updateCardAssociations = useUpdateCardAssociations(); 33 + const addCard = useAddCard(props.analyticsContext); 34 + const updateCardAssociations = useUpdateCardAssociations( 35 + props.analyticsContext, 36 + ); 34 37 35 38 if (error) { 36 39 return <CollectionSelectorError />; ··· 51 54 const handleUpdateCard = (e: React.FormEvent) => { 52 55 e.preventDefault(); 53 56 track('add or update existing card'); 54 - posthog.capture('add or update existing card'); 55 57 56 58 const trimmedNote = note?.trimEnd() === '' ? undefined : note; 57 59 ··· 100 102 note?: string; 101 103 addToCollectionIds?: string[]; 102 104 removeFromCollectionIds?: string[]; 105 + addToLibrary?: boolean; 103 106 } = { cardId: cardStatus.data.card.id }; 104 107 105 108 if (hasNoteChanged) updatedCardPayload.note = trimmedNote; ··· 109 112 updatedCardPayload.removeFromCollectionIds = removedCollections.map( 110 113 (c) => c.id, 111 114 ); 115 + 116 + // Track as a card save if we're adding collections or a note (indicates user is saving/organizing the card) 117 + updatedCardPayload.addToLibrary = hasAdded || hasNoteChanged; 112 118 113 119 updateCardAssociations.mutate( 114 120 {
+3
src/webapp/features/cards/components/urlCard/UrlCard.tsx
··· 11 11 import { useUserSettings } from '@/features/settings/lib/queries/useUserSettings'; 12 12 import UrlCardDebugView from '../UrlCardDebugView/UrlCardDebugView'; 13 13 import Link from 'next/link'; 14 + import { CardSaveAnalyticsContext } from '@/features/analytics/types'; 14 15 15 16 interface Props { 16 17 id: string; ··· 25 26 cardAuthor?: User; 26 27 viaCardId?: string; 27 28 showAuthor?: boolean; 29 + analyticsContext?: CardSaveAnalyticsContext; 28 30 } 29 31 30 32 export default function UrlCard(props: Props) { ··· 133 135 urlLibraryCount={props.urlLibraryCount} 134 136 urlIsInLibrary={props.urlIsInLibrary ?? false} 135 137 viaCardId={props.viaCardId} 138 + analyticsContext={props.analyticsContext} 136 139 /> 137 140 </Stack> 138 141 </Stack>
+3
src/webapp/features/cards/components/urlCardActions/UrlCardActions.tsx
··· 20 20 import { IoMdCheckmark } from 'react-icons/io'; 21 21 import { notifications } from '@mantine/notifications'; 22 22 import { BiCopy } from 'react-icons/bi'; 23 + import { CardSaveAnalyticsContext } from '@/features/analytics/types'; 23 24 24 25 interface Props { 25 26 id: string; ··· 32 33 urlLibraryCount: number; 33 34 urlIsInLibrary: boolean; 34 35 viaCardId?: string; 36 + analyticsContext?: CardSaveAnalyticsContext; 35 37 } 36 38 37 39 export default function UrlCardActions(props: Props) { ··· 194 196 isInYourLibrary={props.urlIsInLibrary} 195 197 urlLibraryCount={props.urlLibraryCount} 196 198 viaCardId={props.viaCardId} 199 + analyticsContext={props.analyticsContext} 197 200 /> 198 201 199 202 <NoteCardModal
+11 -1
src/webapp/features/cards/containers/cardsContainerContent/CardsContainerContent.tsx
··· 9 9 import { useNavbarContext } from '@/providers/navbar'; 10 10 import { FaRegNoteSticky } from 'react-icons/fa6'; 11 11 import { useUserSettings } from '@/features/settings/lib/queries/useUserSettings'; 12 - import { useSearchParams } from 'next/navigation'; 12 + import { useSearchParams, usePathname } from 'next/navigation'; 13 + import { CardSaveSource } from '@/features/analytics/types'; 13 14 14 15 interface Props { 15 16 handle: string; ··· 22 23 }; 23 24 24 25 export default function CardsContainerContent(props: Props) { 26 + const pathname = usePathname(); 25 27 const { desktopOpened } = useNavbarContext(); 26 28 const { settings } = useUserSettings(); 27 29 ··· 97 99 urlLibraryCount={card.urlLibraryCount} 98 100 urlIsInLibrary={card.urlInLibrary} 99 101 viaCardId={card.id} 102 + analyticsContext={{ 103 + saveSource: CardSaveSource.PROFILE, 104 + activeFilters: { 105 + urlType: selectedUrlType, 106 + sort: sortBy, 107 + }, 108 + pagePath: pathname, 109 + }} 100 110 /> 101 111 </Grid.Col> 102 112 ))}
+31 -1
src/webapp/features/cards/lib/mutations/useAddCard.tsx
··· 5 5 import { feedKeys } from '@/features/feeds/lib/feedKeys'; 6 6 import { noteKeys } from '@/features/notes/lib/noteKeys'; 7 7 import { sembleKeys } from '@/features/semble/lib/sembleKeys'; 8 + import posthog from 'posthog-js'; 9 + import { 10 + CardSaveAnalyticsContext, 11 + CardSaveEventProperties, 12 + } from '@/features/analytics/types'; 13 + import { shouldCaptureAnalytics } from '@/features/analytics/utils'; 8 14 9 - export default function useAddCard() { 15 + export default function useAddCard( 16 + analyticsContext?: CardSaveAnalyticsContext, 17 + ) { 10 18 const queryClient = useQueryClient(); 11 19 12 20 const mutation = useMutation({ ··· 47 55 queryKey: collectionKeys.infinite(id), 48 56 }); 49 57 }); 58 + 59 + // Track card save event in PostHog 60 + if (shouldCaptureAnalytics() && analyticsContext) { 61 + const eventProperties: CardSaveEventProperties = { 62 + save_source: analyticsContext.saveSource, 63 + is_new_card: true, 64 + has_note: !!variables.note, 65 + collection_count: variables.collectionIds?.length || 0, 66 + active_filters: analyticsContext.activeFilters 67 + ? { 68 + url_type: analyticsContext.activeFilters.urlType, 69 + sort: analyticsContext.activeFilters.sort, 70 + search_query: analyticsContext.activeFilters.searchQuery, 71 + profile_filter: analyticsContext.activeFilters.profileFilter, 72 + } 73 + : undefined, 74 + via_card_id: variables.viaCardId, 75 + page_path: analyticsContext.pagePath, 76 + }; 77 + 78 + posthog.capture('card_saved', eventProperties); 79 + } 50 80 }, 51 81 }); 52 82
+36 -1
src/webapp/features/cards/lib/mutations/useUpdateCardAssociations.tsx
··· 5 5 import { noteKeys } from '@/features/notes/lib/noteKeys'; 6 6 import { sembleKeys } from '@/features/semble/lib/sembleKeys'; 7 7 import { feedKeys } from '@/features/feeds/lib/feedKeys'; 8 + import posthog from 'posthog-js'; 9 + import { 10 + CardSaveAnalyticsContext, 11 + CardSaveEventProperties, 12 + } from '@/features/analytics/types'; 13 + import { shouldCaptureAnalytics } from '@/features/analytics/utils'; 8 14 9 - export default function useUpdateCardAssociations() { 15 + export default function useUpdateCardAssociations( 16 + analyticsContext?: CardSaveAnalyticsContext, 17 + ) { 10 18 const client = createSembleClient(); 11 19 12 20 const queryClient = useQueryClient(); ··· 18 26 addToCollectionIds?: string[]; 19 27 removeFromCollectionIds?: string[]; 20 28 viaCardId?: string; 29 + addToLibrary?: boolean; 21 30 }) => { 22 31 return client.updateUrlCardAssociations({ 23 32 cardId: updatedCard.cardId, ··· 47 56 queryKey: collectionKeys.collection(id), 48 57 }); 49 58 }); 59 + 60 + // Track card save event in PostHog (only when adding to library) 61 + if ( 62 + shouldCaptureAnalytics() && 63 + analyticsContext && 64 + variables.addToLibrary 65 + ) { 66 + const eventProperties: CardSaveEventProperties = { 67 + save_source: analyticsContext.saveSource, 68 + is_new_card: false, 69 + has_note: !!variables.note, 70 + collection_count: variables.addToCollectionIds?.length || 0, 71 + active_filters: analyticsContext.activeFilters 72 + ? { 73 + url_type: analyticsContext.activeFilters.urlType, 74 + sort: analyticsContext.activeFilters.sort, 75 + search_query: analyticsContext.activeFilters.searchQuery, 76 + profile_filter: analyticsContext.activeFilters.profileFilter, 77 + } 78 + : undefined, 79 + via_card_id: variables.viaCardId, 80 + page_path: analyticsContext.pagePath, 81 + }; 82 + 83 + posthog.capture('card_saved', eventProperties); 84 + } 50 85 }, 51 86 }); 52 87
+11 -1
src/webapp/features/collections/containers/collectionContainerContent/CollectionContainerContent.tsx
··· 11 11 import AddCardDrawer from '@/features/cards/components/addCardDrawer/AddCardDrawer'; 12 12 import Link from 'next/link'; 13 13 import { FiPlus } from 'react-icons/fi'; 14 - import { useSearchParams } from 'next/navigation'; 14 + import { useSearchParams, usePathname } from 'next/navigation'; 15 + import { CardSaveSource } from '@/features/analytics/types'; 15 16 16 17 interface Props { 17 18 rkey: string; ··· 27 28 }; 28 29 29 30 export default function CollectionContainerContent(props: Props) { 31 + const pathname = usePathname(); 30 32 const { desktopOpened } = useNavbarContext(); 31 33 const [showAddDrawer, setShowAddDrawer] = useState(false); 32 34 const { user } = useAuth(); ··· 84 86 currentCollection={firstPage} 85 87 viaCardId={card.id} 86 88 showAuthor={props.handle !== card.author.handle} 89 + analyticsContext={{ 90 + saveSource: CardSaveSource.COLLECTION, 91 + activeFilters: { 92 + urlType: selectedUrlType, 93 + sort: sortBy, 94 + }, 95 + pagePath: pathname, 96 + }} 87 97 /> 88 98 </Grid.Col> 89 99 ))}
+3
src/webapp/features/feeds/components/feedItem/FeedItem.tsx
··· 2 2 import { Stack } from '@mantine/core'; 3 3 import UrlCard from '@/features/cards/components/urlCard/UrlCard'; 4 4 import FeedActivityStatus from '../feedActivityStatus/FeedActivityStatus'; 5 + import { CardSaveAnalyticsContext } from '@/features/analytics/types'; 5 6 6 7 interface Props { 7 8 item: FeedItemType; 9 + analyticsContext?: CardSaveAnalyticsContext; 8 10 } 9 11 10 12 export default function FeedItem(props: Props) { ··· 27 29 urlIsInLibrary={props.item.card.urlInLibrary} 28 30 authorHandle={props.item.user.handle} 29 31 viaCardId={props.item.card.id} 32 + analyticsContext={props.analyticsContext} 30 33 /> 31 34 </Stack> 32 35 );
+14 -2
src/webapp/features/feeds/containers/myFeedContainer/MyFeedContainer.tsx
··· 9 9 import InfiniteScroll from '@/components/contentDisplay/infiniteScroll/InfiniteScroll'; 10 10 import RefetchButton from '@/components/navigation/refetchButton/RefetchButton'; 11 11 import { UrlType, ActivitySource } from '@semble/types'; 12 - import { useSearchParams, useRouter } from 'next/navigation'; 12 + import { useSearchParams, useRouter, usePathname } from 'next/navigation'; 13 + import { CardSaveSource } from '@/features/analytics/types'; 13 14 14 15 export default function MyFeedContainer() { 15 16 const router = useRouter(); 17 + const pathname = usePathname(); 16 18 const searchParams = useSearchParams(); 17 19 const selectedUrlType = searchParams.get('type') as UrlType; 18 20 const selectedSource = searchParams.get('source') as ActivitySource; ··· 81 83 <Stack gap={'xl'} mx={'auto'} maw={600} w={'100%'}> 82 84 <Stack gap={60}> 83 85 {allActivities.map((item) => ( 84 - <FeedItem key={item.id} item={item} /> 86 + <FeedItem 87 + key={item.id} 88 + item={item} 89 + analyticsContext={{ 90 + saveSource: CardSaveSource.FEED, 91 + activeFilters: { 92 + urlType: selectedUrlType, 93 + }, 94 + pagePath: pathname, 95 + }} 96 + /> 85 97 ))} 86 98 </Stack> 87 99 </Stack>
+3
src/webapp/features/notifications/components/notificationItem/NotificationItem.tsx
··· 5 5 import NotificationActivityStatus from '../notificationActivityStatus/NotificationActivityStatus'; 6 6 import FollowButton from '@/features/follows/components/followButton/FollowButton'; 7 7 import { useRouter } from 'next/navigation'; 8 + import { CardSaveAnalyticsContext } from '@/features/analytics/types'; 8 9 9 10 interface Props { 10 11 item: NotificationItemType; 12 + analyticsContext?: CardSaveAnalyticsContext; 11 13 } 12 14 13 15 export default function NotificationItem(props: Props) { ··· 69 71 urlIsInLibrary={props.item.card.urlInLibrary} 70 72 authorHandle={props.item.user.handle} 71 73 viaCardId={props.item.card.id} 74 + analyticsContext={props.analyticsContext} 72 75 /> 73 76 )} 74 77 </Stack>
+11 -1
src/webapp/features/notifications/containers/notificationsContainer/NotificationsContainer.tsx
··· 21 21 import useMarkAllNotificationsAsRead from '../../lib/mutations/useMarkAllNotificationsAsRead'; 22 22 import { IoCheckmarkDoneSharp } from 'react-icons/io5'; 23 23 import useUnreadNotificationCount from '../../lib/queries/useUnreadNotificationCount'; 24 + import { CardSaveSource } from '@/features/analytics/types'; 25 + import { usePathname } from 'next/navigation'; 24 26 25 27 export default function NotificationsContainer() { 28 + const pathname = usePathname(); 26 29 const { 27 30 data, 28 31 error, ··· 117 120 <Stack gap={'xl'} mx={'auto'} maw={600} w={'100%'}> 118 121 <Stack gap={60}> 119 122 {allNotifications.map((item) => ( 120 - <NotificationItem key={item.id} item={item} /> 123 + <NotificationItem 124 + key={item.id} 125 + item={item} 126 + analyticsContext={{ 127 + saveSource: CardSaveSource.NOTIFICATIONS, 128 + pagePath: pathname, 129 + }} 130 + /> 121 131 ))} 122 132 </Stack> 123 133 </Stack>
+7
src/webapp/features/profile/containers/profileContainer/ProfileContainer.tsx
··· 18 18 import { BiCollection } from 'react-icons/bi'; 19 19 import { FaRegNoteSticky } from 'react-icons/fa6'; 20 20 import { useNavbarContext } from '@/providers/navbar'; 21 + import { CardSaveSource } from '@/features/analytics/types'; 22 + import { usePathname } from 'next/navigation'; 21 23 22 24 interface Props { 23 25 handle: string; 24 26 } 25 27 26 28 export default function ProfileContainer(props: Props) { 29 + const pathname = usePathname(); 27 30 const { data: collectionsData } = useCollections({ 28 31 limit: 4, 29 32 didOrHandle: props.handle, ··· 81 84 urlLibraryCount={card.urlLibraryCount} 82 85 urlIsInLibrary={card.urlInLibrary} 83 86 viaCardId={card.id} 87 + analyticsContext={{ 88 + saveSource: CardSaveSource.PROFILE, 89 + pagePath: pathname, 90 + }} 84 91 /> 85 92 </Grid.Col> 86 93 ))}
+15 -1
src/webapp/features/search/containers/cardSearchResultsContainer/CardSearchResultsContainer.tsx
··· 10 10 import SearchResultsContainerError from '../searchResultsContainer/Error.SearchResultsContainer'; 11 11 import SearchQueryAlert from '../../components/searchQueryAlert/SearchQueryAlert'; 12 12 import { SearchFilters } from '../../components/searchFilters/SearchFilters'; 13 + import { CardSaveSource } from '@/features/analytics/types'; 14 + import { usePathname } from 'next/navigation'; 13 15 14 16 interface Props { 15 17 query: string; ··· 18 20 } 19 21 20 22 export default function CardSearchResultsContainer(props: Props) { 23 + const pathname = usePathname(); 21 24 const { 22 25 data, 23 26 error, ··· 59 62 <Grid gutter="xs"> 60 63 {allUrls.map((urlView) => ( 61 64 <Grid.Col key={urlView.url} span={12}> 62 - <SimilarUrlCard urlView={urlView} /> 65 + <SimilarUrlCard 66 + urlView={urlView} 67 + analyticsContext={{ 68 + saveSource: CardSaveSource.SEARCH_RESULTS, 69 + activeFilters: { 70 + searchQuery: props.query, 71 + profileFilter: props.handle, 72 + urlType: props.urlType, 73 + }, 74 + pagePath: pathname, 75 + }} 76 + /> 63 77 </Grid.Col> 64 78 ))} 65 79 </Grid>
+7 -4
src/webapp/features/semble/components/sembleActions/SembleActions.tsx
··· 10 10 import { MdIosShare } from 'react-icons/md'; 11 11 import useSembleLibraries from '../../lib/queries/useSembleLibraries'; 12 12 import { track } from '@vercel/analytics'; 13 - import posthog from 'posthog-js'; 13 + import { CardSaveSource } from '@/features/analytics/types'; 14 + import { usePathname } from 'next/navigation'; 14 15 15 16 interface Props { 16 17 url: string; ··· 18 19 } 19 20 20 21 export default function SembleActions(props: Props) { 22 + const pathname = usePathname(); 21 23 const cardStatus = useGetCardFromMyLibrary({ url: props.url }); 22 24 const isInYourLibrary = cardStatus.data.card?.urlInLibrary; 23 25 const [showAddToModal, setShowAddToModal] = useState(false); ··· 76 78 track( 77 79 `Semble: ${isInYourLibrary ? 'update card' : 'add to library'}`, 78 80 ); 79 - posthog.capture( 80 - `Semble: ${isInYourLibrary ? 'update card' : 'add to library'}`, 81 - ); 82 81 }} 83 82 > 84 83 {isInYourLibrary ? 'Update card' : 'Add to library'} ··· 95 94 isInYourLibrary={cardStatus.data.card?.urlInLibrary} 96 95 urlLibraryCount={urlLibraryCount} 97 96 viaCardId={props.viaCardId} 97 + analyticsContext={{ 98 + saveSource: CardSaveSource.SEMBLE_PAGE, 99 + pagePath: pathname, 100 + }} 98 101 /> 99 102 </Fragment> 100 103 );
+3
src/webapp/features/semble/components/similarUrlCard/SimilarUrlCard.tsx
··· 2 2 3 3 import type { UrlView } from '@/api-client'; 4 4 import UrlCard from '@/features/cards/components/urlCard/UrlCard'; 5 + import { CardSaveAnalyticsContext } from '@/features/analytics/types'; 5 6 6 7 interface Props { 7 8 urlView: UrlView; 9 + analyticsContext?: CardSaveAnalyticsContext; 8 10 } 9 11 10 12 export default function SimilarUrlCard(props: Props) { ··· 15 17 cardContent={props.urlView.metadata} 16 18 urlLibraryCount={props.urlView.urlLibraryCount} 17 19 urlIsInLibrary={props.urlView.urlInLibrary ?? false} 20 + analyticsContext={props.analyticsContext} 18 21 /> 19 22 ); 20 23 }
+13 -2
src/webapp/features/semble/containers/sembleSimilarCardsContainer/SembleSimilarCardsContainer.tsx
··· 8 8 import SembleEmptyTab from '../../components/sembleEmptyTab/SembleEmptyTab'; 9 9 import { BiLink } from 'react-icons/bi'; 10 10 import { CardFilters } from '@/features/cards/components/cardFilters/CardFilters'; 11 - import { useSearchParams } from 'next/navigation'; 11 + import { useSearchParams, usePathname } from 'next/navigation'; 12 12 import { UrlType } from '@semble/types'; 13 + import { CardSaveSource } from '@/features/analytics/types'; 13 14 14 15 interface Props { 15 16 url: string; 16 17 } 17 18 18 19 export default function SembleSimilarCardsContainer(props: Props) { 20 + const pathname = usePathname(); 19 21 const searchParams = useSearchParams(); 20 22 const selectedUrlType = searchParams.get('type') as UrlType; 21 23 ··· 58 60 <Grid gutter="sm" mx={'auto'} maw={600} w={'100%'}> 59 61 {allSimilarUrls.map((urlView) => ( 60 62 <Grid.Col key={urlView.url} span={12}> 61 - <SimilarUrlCard urlView={urlView} /> 63 + <SimilarUrlCard 64 + urlView={urlView} 65 + analyticsContext={{ 66 + saveSource: CardSaveSource.SIMILAR_CARDS, 67 + activeFilters: { 68 + urlType: selectedUrlType, 69 + }, 70 + pagePath: pathname, 71 + }} 72 + /> 62 73 </Grid.Col> 63 74 ))} 64 75 </Grid>
+4 -14
src/webapp/hooks/useAuth.tsx
··· 9 9 import { usePathname } from 'next/navigation'; 10 10 import posthog from 'posthog-js'; 11 11 import { isInternalUser, isEarlyTester } from '@/lib/userLists'; 12 + import { shouldCaptureAnalytics } from '@/features/analytics/utils'; 12 13 13 14 const ENABLE_AUTH_LOGGING = true; 14 15 ··· 37 38 } 38 39 39 40 // Reset PostHog user identity 40 - if ( 41 - process.env.NEXT_PUBLIC_VERCEL_ENV === 'production' && 42 - posthog.__loaded 43 - ) { 41 + if (shouldCaptureAnalytics()) { 44 42 posthog.reset(); 45 43 if (ENABLE_AUTH_LOGGING) { 46 44 console.log('[useAuth] PostHog user identity reset'); ··· 72 70 useEffect(() => { 73 71 const user = query.data; 74 72 75 - // Only identify in production and when user is available 76 - if ( 77 - process.env.NEXT_PUBLIC_VERCEL_ENV === 'production' && 78 - user && 79 - posthog.__loaded 80 - ) { 73 + // Only identify when analytics is enabled and user is available 74 + if (shouldCaptureAnalytics() && user) { 81 75 posthog.identify(user.id, { 82 76 name: user.name, 83 77 handle: user.handle, 84 78 is_internal: isInternalUser(user.handle), 85 79 is_early_tester: isEarlyTester(user.handle), 86 80 }); 87 - 88 - if (ENABLE_AUTH_LOGGING) { 89 - console.log('[useAuth] User identified in PostHog:', user.id); 90 - } 91 81 } 92 82 }, [query.data]); 93 83
+4
src/webapp/lib/userLists.ts
··· 9 9 'cosmik.network', 10 10 'semble.so', 11 11 'jasmine-pyz.bsky.social', 12 + 'cosmiktesting.bsky.social', 13 + 'sembot.bsky.social', 14 + 'alice.bsky.social', 15 + 'bob.bsky.social', 12 16 ]; 13 17 14 18 export const EARLY_TESTER_HANDLES = [
+3 -1
src/webapp/providers/index.tsx
··· 27 27 28 28 posthog.init(process.env.NEXT_PUBLIC_POSTHOG_PROJECT_API_KEY!, { 29 29 api_host: process.env.NEXT_PUBLIC_POSTHOG_API_HOST, 30 - defaults: '2026-01-30', 31 30 disable_session_recording: true, 31 + autocapture: false, 32 + capture_pageview: false, 33 + capture_pageleave: false, 32 34 }); 33 35 }, []); 34 36