diff --git a/src/webapp/features/analytics/types.ts b/src/webapp/features/analytics/types.ts
new file mode 100644
index 00000000..0172a023
--- /dev/null
+++ b/src/webapp/features/analytics/types.ts
@@ -0,0 +1,36 @@
+export enum CardSaveSource {
+ PROFILE = 'profile',
+ SEMBLE_PAGE = 'semble_page',
+ COLLECTION = 'collection',
+ FEED = 'feed',
+ SIMILAR_CARDS = 'similar_cards',
+ NOTIFICATIONS = 'notifications',
+ SEARCH_RESULTS = 'search_results',
+ ADD_CARD_DRAWER = 'add_card_drawer',
+}
+
+export interface CardSaveAnalyticsContext {
+ saveSource: CardSaveSource;
+ activeFilters?: {
+ urlType?: string;
+ sort?: string;
+ searchQuery?: string;
+ profileFilter?: string;
+ };
+ pagePath?: string;
+}
+
+export interface CardSaveEventProperties {
+ save_source: CardSaveSource;
+ is_new_card: boolean;
+ has_note: boolean;
+ collection_count: number;
+ active_filters?: {
+ url_type?: string;
+ sort?: string;
+ search_query?: string;
+ profile_filter?: string;
+ };
+ via_card_id?: string;
+ page_path?: string;
+}
diff --git a/src/webapp/features/analytics/utils.ts b/src/webapp/features/analytics/utils.ts
new file mode 100644
index 00000000..52ae9710
--- /dev/null
+++ b/src/webapp/features/analytics/utils.ts
@@ -0,0 +1,11 @@
+import posthog from 'posthog-js';
+
+/**
+ * Check if analytics should be captured
+ * Centralizes the logic for when to track events
+ */
+export function shouldCaptureAnalytics(): boolean {
+ return (
+ process.env.NEXT_PUBLIC_VERCEL_ENV === 'production' && posthog.__loaded
+ );
+}
diff --git a/src/webapp/features/cards/components/addCardDrawer/AddCardDrawer.tsx b/src/webapp/features/cards/components/addCardDrawer/AddCardDrawer.tsx
index 6d44a2f6..8b08e94c 100644
--- a/src/webapp/features/cards/components/addCardDrawer/AddCardDrawer.tsx
+++ b/src/webapp/features/cards/components/addCardDrawer/AddCardDrawer.tsx
@@ -29,7 +29,8 @@ import { isMarginUri, getMarginUrl } from '@/lib/utils/margin';
import MarginLogo from '@/components/MarginLogo';
import { Collection, CollectionAccessType } from '@semble/types';
import { FaSeedling } from 'react-icons/fa6';
-import posthog from 'posthog-js';
+import { CardSaveSource } from '@/features/analytics/types';
+import { usePathname } from 'next/navigation';
interface Props {
isOpen: boolean;
@@ -39,6 +40,7 @@ interface Props {
}
export default function AddCardDrawer(props: Props) {
+ const pathname = usePathname();
const [collectionSelectorOpened, { toggle: toggleCollectionSelector }] =
useDisclosure(false);
const initialCollections = props.selectedCollection
@@ -51,7 +53,10 @@ export default function AddCardDrawer(props: Props) {
const myCollections =
collections?.pages.flatMap((page) => page.collections ?? []) ?? [];
- const addCard = useAddCard();
+ const addCard = useAddCard({
+ saveSource: CardSaveSource.ADD_CARD_DRAWER,
+ pagePath: pathname,
+ });
const form = useForm({
initialValues: {
@@ -73,7 +78,6 @@ export default function AddCardDrawer(props: Props) {
const handleAddCard = (e: React.FormEvent) => {
e.preventDefault();
track('add new card');
- posthog.capture('add_new_card');
addCard.mutate(
{
diff --git a/src/webapp/features/cards/components/addCardToModal/AddCardToModal.tsx b/src/webapp/features/cards/components/addCardToModal/AddCardToModal.tsx
index fa63c176..d3cb94a5 100644
--- a/src/webapp/features/cards/components/addCardToModal/AddCardToModal.tsx
+++ b/src/webapp/features/cards/components/addCardToModal/AddCardToModal.tsx
@@ -5,6 +5,7 @@ import { Suspense } from 'react';
import CollectionSelectorSkeleton from '@/features/collections/components/collectionSelector/Skeleton.CollectionSelector';
import AddCardToModalContent from './AddCardToModalContent';
import CardToBeAddedPreview from '../cardToBeAddedPreview/CardToBeAddedPreview';
+import { CardSaveAnalyticsContext } from '@/features/analytics/types';
interface Props {
isOpen: boolean;
@@ -16,6 +17,7 @@ interface Props {
urlLibraryCount: number;
viaCardId?: string;
cardContent?: UrlCard['cardContent'];
+ analyticsContext?: CardSaveAnalyticsContext;
}
export default function AddCardToModal(props: Props) {
@@ -63,6 +65,7 @@ export default function AddCardToModal(props: Props) {
cardContent={props.cardContent}
note={props.note}
viaCardId={props.viaCardId}
+ analyticsContext={props.analyticsContext}
/>
diff --git a/src/webapp/features/cards/components/addCardToModal/AddCardToModalContent.tsx b/src/webapp/features/cards/components/addCardToModal/AddCardToModalContent.tsx
index 13bb9b35..f5cf0f70 100644
--- a/src/webapp/features/cards/components/addCardToModal/AddCardToModalContent.tsx
+++ b/src/webapp/features/cards/components/addCardToModal/AddCardToModalContent.tsx
@@ -12,7 +12,7 @@ import useUpdateCardAssociations from '@/features/cards/lib/mutations/useUpdateC
import useAddCard from '@/features/cards/lib/mutations/useAddCard';
import { track } from '@vercel/analytics';
import AddCardActions from '../addCardActions/AddCardActions';
-import posthog from 'posthog-js';
+import { CardSaveAnalyticsContext } from '@/features/analytics/types';
interface Props {
onClose: () => void;
@@ -21,6 +21,7 @@ interface Props {
note?: string;
cardContent?: UrlCard['cardContent'];
viaCardId?: string;
+ analyticsContext?: CardSaveAnalyticsContext;
}
export default function AddCardToModalContent(props: Props) {
@@ -29,8 +30,10 @@ export default function AddCardToModalContent(props: Props) {
const [note, setNote] = useState(isMyCard ? props.note : '');
const { data, error } = useMyCollections();
- const addCard = useAddCard();
- const updateCardAssociations = useUpdateCardAssociations();
+ const addCard = useAddCard(props.analyticsContext);
+ const updateCardAssociations = useUpdateCardAssociations(
+ props.analyticsContext,
+ );
if (error) {
return ;
@@ -51,7 +54,6 @@ export default function AddCardToModalContent(props: Props) {
const handleUpdateCard = (e: React.FormEvent) => {
e.preventDefault();
track('add or update existing card');
- posthog.capture('add or update existing card');
const trimmedNote = note?.trimEnd() === '' ? undefined : note;
@@ -100,6 +102,7 @@ export default function AddCardToModalContent(props: Props) {
note?: string;
addToCollectionIds?: string[];
removeFromCollectionIds?: string[];
+ addToLibrary?: boolean;
} = { cardId: cardStatus.data.card.id };
if (hasNoteChanged) updatedCardPayload.note = trimmedNote;
@@ -110,6 +113,9 @@ export default function AddCardToModalContent(props: Props) {
(c) => c.id,
);
+ // Track as a card save if we're adding collections or a note (indicates user is saving/organizing the card)
+ updatedCardPayload.addToLibrary = hasAdded || hasNoteChanged;
+
updateCardAssociations.mutate(
{
...updatedCardPayload,
diff --git a/src/webapp/features/cards/components/urlCard/UrlCard.tsx b/src/webapp/features/cards/components/urlCard/UrlCard.tsx
index 3c323e18..e7a0be8e 100644
--- a/src/webapp/features/cards/components/urlCard/UrlCard.tsx
+++ b/src/webapp/features/cards/components/urlCard/UrlCard.tsx
@@ -11,6 +11,7 @@ import styles from './UrlCard.module.css';
import { useUserSettings } from '@/features/settings/lib/queries/useUserSettings';
import UrlCardDebugView from '../UrlCardDebugView/UrlCardDebugView';
import Link from 'next/link';
+import { CardSaveAnalyticsContext } from '@/features/analytics/types';
interface Props {
id: string;
@@ -25,6 +26,7 @@ interface Props {
cardAuthor?: User;
viaCardId?: string;
showAuthor?: boolean;
+ analyticsContext?: CardSaveAnalyticsContext;
}
export default function UrlCard(props: Props) {
@@ -133,6 +135,7 @@ export default function UrlCard(props: Props) {
urlLibraryCount={props.urlLibraryCount}
urlIsInLibrary={props.urlIsInLibrary ?? false}
viaCardId={props.viaCardId}
+ analyticsContext={props.analyticsContext}
/>
diff --git a/src/webapp/features/cards/components/urlCardActions/UrlCardActions.tsx b/src/webapp/features/cards/components/urlCardActions/UrlCardActions.tsx
index c4fda860..5db9f808 100644
--- a/src/webapp/features/cards/components/urlCardActions/UrlCardActions.tsx
+++ b/src/webapp/features/cards/components/urlCardActions/UrlCardActions.tsx
@@ -20,6 +20,7 @@ import { useAuth } from '@/hooks/useAuth';
import { IoMdCheckmark } from 'react-icons/io';
import { notifications } from '@mantine/notifications';
import { BiCopy } from 'react-icons/bi';
+import { CardSaveAnalyticsContext } from '@/features/analytics/types';
interface Props {
id: string;
@@ -32,6 +33,7 @@ interface Props {
urlLibraryCount: number;
urlIsInLibrary: boolean;
viaCardId?: string;
+ analyticsContext?: CardSaveAnalyticsContext;
}
export default function UrlCardActions(props: Props) {
@@ -194,6 +196,7 @@ export default function UrlCardActions(props: Props) {
isInYourLibrary={props.urlIsInLibrary}
urlLibraryCount={props.urlLibraryCount}
viaCardId={props.viaCardId}
+ analyticsContext={props.analyticsContext}
/>
= {
};
export default function CardsContainerContent(props: Props) {
+ const pathname = usePathname();
const { desktopOpened } = useNavbarContext();
const { settings } = useUserSettings();
@@ -97,6 +99,14 @@ export default function CardsContainerContent(props: Props) {
urlLibraryCount={card.urlLibraryCount}
urlIsInLibrary={card.urlInLibrary}
viaCardId={card.id}
+ analyticsContext={{
+ saveSource: CardSaveSource.PROFILE,
+ activeFilters: {
+ urlType: selectedUrlType,
+ sort: sortBy,
+ },
+ pagePath: pathname,
+ }}
/>
))}
diff --git a/src/webapp/features/cards/lib/mutations/useAddCard.tsx b/src/webapp/features/cards/lib/mutations/useAddCard.tsx
index b18f9493..66d24cd6 100644
--- a/src/webapp/features/cards/lib/mutations/useAddCard.tsx
+++ b/src/webapp/features/cards/lib/mutations/useAddCard.tsx
@@ -5,8 +5,16 @@ import { collectionKeys } from '@/features/collections/lib/collectionKeys';
import { feedKeys } from '@/features/feeds/lib/feedKeys';
import { noteKeys } from '@/features/notes/lib/noteKeys';
import { sembleKeys } from '@/features/semble/lib/sembleKeys';
+import posthog from 'posthog-js';
+import {
+ CardSaveAnalyticsContext,
+ CardSaveEventProperties,
+} from '@/features/analytics/types';
+import { shouldCaptureAnalytics } from '@/features/analytics/utils';
-export default function useAddCard() {
+export default function useAddCard(
+ analyticsContext?: CardSaveAnalyticsContext,
+) {
const queryClient = useQueryClient();
const mutation = useMutation({
@@ -47,6 +55,28 @@ export default function useAddCard() {
queryKey: collectionKeys.infinite(id),
});
});
+
+ // Track card save event in PostHog
+ if (shouldCaptureAnalytics() && analyticsContext) {
+ const eventProperties: CardSaveEventProperties = {
+ save_source: analyticsContext.saveSource,
+ is_new_card: true,
+ has_note: !!variables.note,
+ collection_count: variables.collectionIds?.length || 0,
+ active_filters: analyticsContext.activeFilters
+ ? {
+ url_type: analyticsContext.activeFilters.urlType,
+ sort: analyticsContext.activeFilters.sort,
+ search_query: analyticsContext.activeFilters.searchQuery,
+ profile_filter: analyticsContext.activeFilters.profileFilter,
+ }
+ : undefined,
+ via_card_id: variables.viaCardId,
+ page_path: analyticsContext.pagePath,
+ };
+
+ posthog.capture('card_saved', eventProperties);
+ }
},
});
diff --git a/src/webapp/features/cards/lib/mutations/useUpdateCardAssociations.tsx b/src/webapp/features/cards/lib/mutations/useUpdateCardAssociations.tsx
index 9083b953..0503cd5b 100644
--- a/src/webapp/features/cards/lib/mutations/useUpdateCardAssociations.tsx
+++ b/src/webapp/features/cards/lib/mutations/useUpdateCardAssociations.tsx
@@ -5,8 +5,16 @@ import { collectionKeys } from '@/features/collections/lib/collectionKeys';
import { noteKeys } from '@/features/notes/lib/noteKeys';
import { sembleKeys } from '@/features/semble/lib/sembleKeys';
import { feedKeys } from '@/features/feeds/lib/feedKeys';
+import posthog from 'posthog-js';
+import {
+ CardSaveAnalyticsContext,
+ CardSaveEventProperties,
+} from '@/features/analytics/types';
+import { shouldCaptureAnalytics } from '@/features/analytics/utils';
-export default function useUpdateCardAssociations() {
+export default function useUpdateCardAssociations(
+ analyticsContext?: CardSaveAnalyticsContext,
+) {
const client = createSembleClient();
const queryClient = useQueryClient();
@@ -18,6 +26,7 @@ export default function useUpdateCardAssociations() {
addToCollectionIds?: string[];
removeFromCollectionIds?: string[];
viaCardId?: string;
+ addToLibrary?: boolean;
}) => {
return client.updateUrlCardAssociations({
cardId: updatedCard.cardId,
@@ -47,6 +56,32 @@ export default function useUpdateCardAssociations() {
queryKey: collectionKeys.collection(id),
});
});
+
+ // Track card save event in PostHog (only when adding to library)
+ if (
+ shouldCaptureAnalytics() &&
+ analyticsContext &&
+ variables.addToLibrary
+ ) {
+ const eventProperties: CardSaveEventProperties = {
+ save_source: analyticsContext.saveSource,
+ is_new_card: false,
+ has_note: !!variables.note,
+ collection_count: variables.addToCollectionIds?.length || 0,
+ active_filters: analyticsContext.activeFilters
+ ? {
+ url_type: analyticsContext.activeFilters.urlType,
+ sort: analyticsContext.activeFilters.sort,
+ search_query: analyticsContext.activeFilters.searchQuery,
+ profile_filter: analyticsContext.activeFilters.profileFilter,
+ }
+ : undefined,
+ via_card_id: variables.viaCardId,
+ page_path: analyticsContext.pagePath,
+ };
+
+ posthog.capture('card_saved', eventProperties);
+ }
},
});
diff --git a/src/webapp/features/collections/containers/collectionContainerContent/CollectionContainerContent.tsx b/src/webapp/features/collections/containers/collectionContainerContent/CollectionContainerContent.tsx
index d31524c3..e3f1eb47 100644
--- a/src/webapp/features/collections/containers/collectionContainerContent/CollectionContainerContent.tsx
+++ b/src/webapp/features/collections/containers/collectionContainerContent/CollectionContainerContent.tsx
@@ -11,7 +11,8 @@ import UrlCard from '@/features/cards/components/urlCard/UrlCard';
import AddCardDrawer from '@/features/cards/components/addCardDrawer/AddCardDrawer';
import Link from 'next/link';
import { FiPlus } from 'react-icons/fi';
-import { useSearchParams } from 'next/navigation';
+import { useSearchParams, usePathname } from 'next/navigation';
+import { CardSaveSource } from '@/features/analytics/types';
interface Props {
rkey: string;
@@ -27,6 +28,7 @@ const sortOrderMap: Record = {
};
export default function CollectionContainerContent(props: Props) {
+ const pathname = usePathname();
const { desktopOpened } = useNavbarContext();
const [showAddDrawer, setShowAddDrawer] = useState(false);
const { user } = useAuth();
@@ -84,6 +86,14 @@ export default function CollectionContainerContent(props: Props) {
currentCollection={firstPage}
viaCardId={card.id}
showAuthor={props.handle !== card.author.handle}
+ analyticsContext={{
+ saveSource: CardSaveSource.COLLECTION,
+ activeFilters: {
+ urlType: selectedUrlType,
+ sort: sortBy,
+ },
+ pagePath: pathname,
+ }}
/>
))}
diff --git a/src/webapp/features/feeds/components/feedItem/FeedItem.tsx b/src/webapp/features/feeds/components/feedItem/FeedItem.tsx
index e75acb06..a222dddf 100644
--- a/src/webapp/features/feeds/components/feedItem/FeedItem.tsx
+++ b/src/webapp/features/feeds/components/feedItem/FeedItem.tsx
@@ -2,9 +2,11 @@ import type { FeedItem as FeedItemType } from '@/api-client';
import { Stack } from '@mantine/core';
import UrlCard from '@/features/cards/components/urlCard/UrlCard';
import FeedActivityStatus from '../feedActivityStatus/FeedActivityStatus';
+import { CardSaveAnalyticsContext } from '@/features/analytics/types';
interface Props {
item: FeedItemType;
+ analyticsContext?: CardSaveAnalyticsContext;
}
export default function FeedItem(props: Props) {
@@ -27,6 +29,7 @@ export default function FeedItem(props: Props) {
urlIsInLibrary={props.item.card.urlInLibrary}
authorHandle={props.item.user.handle}
viaCardId={props.item.card.id}
+ analyticsContext={props.analyticsContext}
/>
);
diff --git a/src/webapp/features/feeds/containers/myFeedContainer/MyFeedContainer.tsx b/src/webapp/features/feeds/containers/myFeedContainer/MyFeedContainer.tsx
index ad395226..4a8339a5 100644
--- a/src/webapp/features/feeds/containers/myFeedContainer/MyFeedContainer.tsx
+++ b/src/webapp/features/feeds/containers/myFeedContainer/MyFeedContainer.tsx
@@ -9,10 +9,12 @@ import MyFeedContainerError from './Error.MyFeedContainer';
import InfiniteScroll from '@/components/contentDisplay/infiniteScroll/InfiniteScroll';
import RefetchButton from '@/components/navigation/refetchButton/RefetchButton';
import { UrlType, ActivitySource } from '@semble/types';
-import { useSearchParams, useRouter } from 'next/navigation';
+import { useSearchParams, useRouter, usePathname } from 'next/navigation';
+import { CardSaveSource } from '@/features/analytics/types';
export default function MyFeedContainer() {
const router = useRouter();
+ const pathname = usePathname();
const searchParams = useSearchParams();
const selectedUrlType = searchParams.get('type') as UrlType;
const selectedSource = searchParams.get('source') as ActivitySource;
@@ -81,7 +83,17 @@ export default function MyFeedContainer() {
{allActivities.map((item) => (
-
+
))}
diff --git a/src/webapp/features/notifications/components/notificationItem/NotificationItem.tsx b/src/webapp/features/notifications/components/notificationItem/NotificationItem.tsx
index b78c3a69..96e28835 100644
--- a/src/webapp/features/notifications/components/notificationItem/NotificationItem.tsx
+++ b/src/webapp/features/notifications/components/notificationItem/NotificationItem.tsx
@@ -5,9 +5,11 @@ import UrlCard from '@/features/cards/components/urlCard/UrlCard';
import NotificationActivityStatus from '../notificationActivityStatus/NotificationActivityStatus';
import FollowButton from '@/features/follows/components/followButton/FollowButton';
import { useRouter } from 'next/navigation';
+import { CardSaveAnalyticsContext } from '@/features/analytics/types';
interface Props {
item: NotificationItemType;
+ analyticsContext?: CardSaveAnalyticsContext;
}
export default function NotificationItem(props: Props) {
@@ -69,6 +71,7 @@ export default function NotificationItem(props: Props) {
urlIsInLibrary={props.item.card.urlInLibrary}
authorHandle={props.item.user.handle}
viaCardId={props.item.card.id}
+ analyticsContext={props.analyticsContext}
/>
)}
diff --git a/src/webapp/features/notifications/containers/notificationsContainer/NotificationsContainer.tsx b/src/webapp/features/notifications/containers/notificationsContainer/NotificationsContainer.tsx
index 7c0b2cb8..881805f1 100644
--- a/src/webapp/features/notifications/containers/notificationsContainer/NotificationsContainer.tsx
+++ b/src/webapp/features/notifications/containers/notificationsContainer/NotificationsContainer.tsx
@@ -21,8 +21,11 @@ import { useEffect, useRef } from 'react';
import useMarkAllNotificationsAsRead from '../../lib/mutations/useMarkAllNotificationsAsRead';
import { IoCheckmarkDoneSharp } from 'react-icons/io5';
import useUnreadNotificationCount from '../../lib/queries/useUnreadNotificationCount';
+import { CardSaveSource } from '@/features/analytics/types';
+import { usePathname } from 'next/navigation';
export default function NotificationsContainer() {
+ const pathname = usePathname();
const {
data,
error,
@@ -117,7 +120,14 @@ export default function NotificationsContainer() {
{allNotifications.map((item) => (
-
+
))}
diff --git a/src/webapp/features/profile/containers/profileContainer/ProfileContainer.tsx b/src/webapp/features/profile/containers/profileContainer/ProfileContainer.tsx
index 2d33d2a7..6065f301 100644
--- a/src/webapp/features/profile/containers/profileContainer/ProfileContainer.tsx
+++ b/src/webapp/features/profile/containers/profileContainer/ProfileContainer.tsx
@@ -18,12 +18,15 @@ import ProfileEmptyTab from '../../components/profileEmptyTab/ProfileEmptyTab';
import { BiCollection } from 'react-icons/bi';
import { FaRegNoteSticky } from 'react-icons/fa6';
import { useNavbarContext } from '@/providers/navbar';
+import { CardSaveSource } from '@/features/analytics/types';
+import { usePathname } from 'next/navigation';
interface Props {
handle: string;
}
export default function ProfileContainer(props: Props) {
+ const pathname = usePathname();
const { data: collectionsData } = useCollections({
limit: 4,
didOrHandle: props.handle,
@@ -81,6 +84,10 @@ export default function ProfileContainer(props: Props) {
urlLibraryCount={card.urlLibraryCount}
urlIsInLibrary={card.urlInLibrary}
viaCardId={card.id}
+ analyticsContext={{
+ saveSource: CardSaveSource.PROFILE,
+ pagePath: pathname,
+ }}
/>
))}
diff --git a/src/webapp/features/search/containers/cardSearchResultsContainer/CardSearchResultsContainer.tsx b/src/webapp/features/search/containers/cardSearchResultsContainer/CardSearchResultsContainer.tsx
index e74dc905..a641e805 100644
--- a/src/webapp/features/search/containers/cardSearchResultsContainer/CardSearchResultsContainer.tsx
+++ b/src/webapp/features/search/containers/cardSearchResultsContainer/CardSearchResultsContainer.tsx
@@ -10,6 +10,8 @@ import SimilarUrlCard from '@/features/semble/components/similarUrlCard/SimilarU
import SearchResultsContainerError from '../searchResultsContainer/Error.SearchResultsContainer';
import SearchQueryAlert from '../../components/searchQueryAlert/SearchQueryAlert';
import { SearchFilters } from '../../components/searchFilters/SearchFilters';
+import { CardSaveSource } from '@/features/analytics/types';
+import { usePathname } from 'next/navigation';
interface Props {
query: string;
@@ -18,6 +20,7 @@ interface Props {
}
export default function CardSearchResultsContainer(props: Props) {
+ const pathname = usePathname();
const {
data,
error,
@@ -59,7 +62,18 @@ export default function CardSearchResultsContainer(props: Props) {
{allUrls.map((urlView) => (
-
+
))}
diff --git a/src/webapp/features/semble/components/sembleActions/SembleActions.tsx b/src/webapp/features/semble/components/sembleActions/SembleActions.tsx
index 0c04107c..1a39a305 100644
--- a/src/webapp/features/semble/components/sembleActions/SembleActions.tsx
+++ b/src/webapp/features/semble/components/sembleActions/SembleActions.tsx
@@ -10,7 +10,8 @@ import { IoMdCheckmark } from 'react-icons/io';
import { MdIosShare } from 'react-icons/md';
import useSembleLibraries from '../../lib/queries/useSembleLibraries';
import { track } from '@vercel/analytics';
-import posthog from 'posthog-js';
+import { CardSaveSource } from '@/features/analytics/types';
+import { usePathname } from 'next/navigation';
interface Props {
url: string;
@@ -18,6 +19,7 @@ interface Props {
}
export default function SembleActions(props: Props) {
+ const pathname = usePathname();
const cardStatus = useGetCardFromMyLibrary({ url: props.url });
const isInYourLibrary = cardStatus.data.card?.urlInLibrary;
const [showAddToModal, setShowAddToModal] = useState(false);
@@ -76,9 +78,6 @@ export default function SembleActions(props: Props) {
track(
`Semble: ${isInYourLibrary ? 'update card' : 'add to library'}`,
);
- posthog.capture(
- `Semble: ${isInYourLibrary ? 'update card' : 'add to library'}`,
- );
}}
>
{isInYourLibrary ? 'Update card' : 'Add to library'}
@@ -95,6 +94,10 @@ export default function SembleActions(props: Props) {
isInYourLibrary={cardStatus.data.card?.urlInLibrary}
urlLibraryCount={urlLibraryCount}
viaCardId={props.viaCardId}
+ analyticsContext={{
+ saveSource: CardSaveSource.SEMBLE_PAGE,
+ pagePath: pathname,
+ }}
/>
);
diff --git a/src/webapp/features/semble/components/similarUrlCard/SimilarUrlCard.tsx b/src/webapp/features/semble/components/similarUrlCard/SimilarUrlCard.tsx
index 62bd075d..8862bd85 100644
--- a/src/webapp/features/semble/components/similarUrlCard/SimilarUrlCard.tsx
+++ b/src/webapp/features/semble/components/similarUrlCard/SimilarUrlCard.tsx
@@ -2,9 +2,11 @@
import type { UrlView } from '@/api-client';
import UrlCard from '@/features/cards/components/urlCard/UrlCard';
+import { CardSaveAnalyticsContext } from '@/features/analytics/types';
interface Props {
urlView: UrlView;
+ analyticsContext?: CardSaveAnalyticsContext;
}
export default function SimilarUrlCard(props: Props) {
@@ -15,6 +17,7 @@ export default function SimilarUrlCard(props: Props) {
cardContent={props.urlView.metadata}
urlLibraryCount={props.urlView.urlLibraryCount}
urlIsInLibrary={props.urlView.urlInLibrary ?? false}
+ analyticsContext={props.analyticsContext}
/>
);
}
diff --git a/src/webapp/features/semble/containers/sembleSimilarCardsContainer/SembleSimilarCardsContainer.tsx b/src/webapp/features/semble/containers/sembleSimilarCardsContainer/SembleSimilarCardsContainer.tsx
index 6d38241f..d33070c0 100644
--- a/src/webapp/features/semble/containers/sembleSimilarCardsContainer/SembleSimilarCardsContainer.tsx
+++ b/src/webapp/features/semble/containers/sembleSimilarCardsContainer/SembleSimilarCardsContainer.tsx
@@ -8,14 +8,16 @@ import SimilarUrlCard from '../../components/similarUrlCard/SimilarUrlCard';
import SembleEmptyTab from '../../components/sembleEmptyTab/SembleEmptyTab';
import { BiLink } from 'react-icons/bi';
import { CardFilters } from '@/features/cards/components/cardFilters/CardFilters';
-import { useSearchParams } from 'next/navigation';
+import { useSearchParams, usePathname } from 'next/navigation';
import { UrlType } from '@semble/types';
+import { CardSaveSource } from '@/features/analytics/types';
interface Props {
url: string;
}
export default function SembleSimilarCardsContainer(props: Props) {
+ const pathname = usePathname();
const searchParams = useSearchParams();
const selectedUrlType = searchParams.get('type') as UrlType;
@@ -58,7 +60,16 @@ export default function SembleSimilarCardsContainer(props: Props) {
{allSimilarUrls.map((urlView) => (
-
+
))}
diff --git a/src/webapp/hooks/useAuth.tsx b/src/webapp/hooks/useAuth.tsx
index 0167daa8..c93aeaff 100644
--- a/src/webapp/hooks/useAuth.tsx
+++ b/src/webapp/hooks/useAuth.tsx
@@ -9,6 +9,7 @@ import { verifySessionOnClient } from '@/lib/auth/dal';
import { usePathname } from 'next/navigation';
import posthog from 'posthog-js';
import { isInternalUser, isEarlyTester } from '@/lib/userLists';
+import { shouldCaptureAnalytics } from '@/features/analytics/utils';
const ENABLE_AUTH_LOGGING = true;
@@ -37,10 +38,7 @@ export const AuthProvider = ({ children }: { children: ReactNode }) => {
}
// Reset PostHog user identity
- if (
- process.env.NEXT_PUBLIC_VERCEL_ENV === 'production' &&
- posthog.__loaded
- ) {
+ if (shouldCaptureAnalytics()) {
posthog.reset();
if (ENABLE_AUTH_LOGGING) {
console.log('[useAuth] PostHog user identity reset');
@@ -72,22 +70,14 @@ export const AuthProvider = ({ children }: { children: ReactNode }) => {
useEffect(() => {
const user = query.data;
- // Only identify in production and when user is available
- if (
- process.env.NEXT_PUBLIC_VERCEL_ENV === 'production' &&
- user &&
- posthog.__loaded
- ) {
+ // Only identify when analytics is enabled and user is available
+ if (shouldCaptureAnalytics() && user) {
posthog.identify(user.id, {
name: user.name,
handle: user.handle,
is_internal: isInternalUser(user.handle),
is_early_tester: isEarlyTester(user.handle),
});
-
- if (ENABLE_AUTH_LOGGING) {
- console.log('[useAuth] User identified in PostHog:', user.id);
- }
}
}, [query.data]);
diff --git a/src/webapp/lib/userLists.ts b/src/webapp/lib/userLists.ts
index 1cd1a8c0..304e93ff 100644
--- a/src/webapp/lib/userLists.ts
+++ b/src/webapp/lib/userLists.ts
@@ -9,6 +9,10 @@ export const INTERNAL_HANDLES = [
'cosmik.network',
'semble.so',
'jasmine-pyz.bsky.social',
+ 'cosmiktesting.bsky.social',
+ 'sembot.bsky.social',
+ 'alice.bsky.social',
+ 'bob.bsky.social',
];
export const EARLY_TESTER_HANDLES = [
diff --git a/src/webapp/providers/index.tsx b/src/webapp/providers/index.tsx
index ede0392d..97a1495e 100644
--- a/src/webapp/providers/index.tsx
+++ b/src/webapp/providers/index.tsx
@@ -27,8 +27,10 @@ export default function Providers(props: Props) {
posthog.init(process.env.NEXT_PUBLIC_POSTHOG_PROJECT_API_KEY!, {
api_host: process.env.NEXT_PUBLIC_POSTHOG_API_HOST,
- defaults: '2026-01-30',
disable_session_recording: true,
+ autocapture: false,
+ capture_pageview: false,
+ capture_pageleave: false,
});
}, []);