diff --git a/apps/web/modules/feature-opt-in/hooks/useFeatureOptInBanner.ts b/apps/web/modules/feature-opt-in/hooks/useFeatureOptInBanner.ts index 39a6b8d913..efbf962c18 100644 --- a/apps/web/modules/feature-opt-in/hooks/useFeatureOptInBanner.ts +++ b/apps/web/modules/feature-opt-in/hooks/useFeatureOptInBanner.ts @@ -3,7 +3,8 @@ import type { OptInFeatureConfig } from "@calcom/features/feature-opt-in/config"; import { getOptInFeatureConfig, shouldDisplayFeatureAt } from "@calcom/features/feature-opt-in/config"; import { trpc } from "@calcom/trpc/react"; -import { useCallback, useEffect, useMemo, useState } from "react"; +import posthog from "posthog-js"; +import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import { getFeatureOptInTimestamp, isFeatureDismissed, @@ -28,6 +29,12 @@ type FeatureOptInMutations = { invalidateQueries: () => void; }; +type FeatureOptInTrackingData = { + enableFor: "user" | "organization" | "teams"; + teamCount?: number; + autoOptIn: boolean; +}; + type UseFeatureOptInBannerResult = { shouldShow: boolean; isLoading: boolean; @@ -41,6 +48,7 @@ type UseFeatureOptInBannerResult = { dismiss: () => void; markOptedIn: () => void; mutations: FeatureOptInMutations; + trackFeatureEnabled: (data: FeatureOptInTrackingData) => void; }; function isFeatureOptedIn(featureId: string): boolean { @@ -108,7 +116,12 @@ function useFeatureOptInBanner(featureId: string): UseFeatureOptInBannerResult { ] ); + const hasBannerShownBeenTracked = useRef(false); + const dismiss = useCallback(() => { + posthog.capture("feature_opt_in_banner_dismissed", { + feature_slug: featureId, + }); setFeatureDismissed(featureId); setIsDismissed(true); }, [featureId]); @@ -119,13 +132,28 @@ function useFeatureOptInBanner(featureId: string): UseFeatureOptInBannerResult { }, [featureId]); const openDialog = useCallback(() => { + posthog.capture("feature_opt_in_banner_try_it_clicked", { + feature_slug: featureId, + }); setIsDialogOpen(true); - }, []); + }, [featureId]); const closeDialog = useCallback(() => { setIsDialogOpen(false); }, []); + const trackFeatureEnabled = useCallback( + (data: FeatureOptInTrackingData) => { + posthog.capture("feature_opt_in_enabled", { + feature_slug: featureId, + enable_for: data.enableFor, + team_count: data.teamCount, + auto_opt_in: data.autoOptIn, + }); + }, + [featureId] + ); + const shouldShow = useMemo(() => { if (isDismissed) return false; if (isOptedIn) return false; @@ -137,6 +165,15 @@ function useFeatureOptInBanner(featureId: string): UseFeatureOptInBannerResult { return eligibilityQuery.data.status === "can_opt_in"; }, [isDismissed, isOptedIn, featureConfig, eligibilityQuery.isLoading, eligibilityQuery.data]); + useEffect(() => { + if (shouldShow && !hasBannerShownBeenTracked.current) { + hasBannerShownBeenTracked.current = true; + posthog.capture("feature_opt_in_banner_shown", { + feature_slug: featureId, + }); + } + }, [shouldShow, featureId]); + return { shouldShow, isLoading: eligibilityQuery.isLoading, @@ -150,6 +187,7 @@ function useFeatureOptInBanner(featureId: string): UseFeatureOptInBannerResult { dismiss, markOptedIn, mutations, + trackFeatureEnabled, }; } diff --git a/packages/features/feature-opt-in/components/FeatureOptInBannerWrapper.tsx b/packages/features/feature-opt-in/components/FeatureOptInBannerWrapper.tsx index 91b5aa55d2..63d501cd48 100644 --- a/packages/features/feature-opt-in/components/FeatureOptInBannerWrapper.tsx +++ b/packages/features/feature-opt-in/components/FeatureOptInBannerWrapper.tsx @@ -15,6 +15,12 @@ type UserRoleContext = { adminTeamNames: { id: number; name: string }[]; }; +type FeatureOptInTrackingData = { + enableFor: "user" | "organization" | "teams"; + teamCount?: number; + autoOptIn: boolean; +}; + type FeatureOptInBannerState = { shouldShow: boolean; isLoading: boolean; @@ -28,6 +34,7 @@ type FeatureOptInBannerState = { dismiss: () => void; markOptedIn: () => void; mutations: FeatureOptInMutations; + trackFeatureEnabled: (data: FeatureOptInTrackingData) => void; }; interface FeatureOptInBannerWrapperProps { @@ -60,6 +67,7 @@ function FeatureOptInBannerWrapper({ state }: FeatureOptInBannerWrapperProps): R featureConfig={state.featureConfig} userRoleContext={state.userRoleContext} mutations={state.mutations} + onTrackFeatureEnabled={state.trackFeatureEnabled} /> )} diff --git a/packages/features/feature-opt-in/components/FeatureOptInConfirmDialog.tsx b/packages/features/feature-opt-in/components/FeatureOptInConfirmDialog.tsx index 2aa245e358..8ac01916ff 100644 --- a/packages/features/feature-opt-in/components/FeatureOptInConfirmDialog.tsx +++ b/packages/features/feature-opt-in/components/FeatureOptInConfirmDialog.tsx @@ -41,6 +41,12 @@ export type FeatureOptInMutations = { invalidateQueries: () => void; }; +type FeatureOptInTrackingData = { + enableFor: "user" | "organization" | "teams"; + teamCount?: number; + autoOptIn: boolean; +}; + interface FeatureOptInConfirmDialogProps { isOpen: boolean; onClose: () => void; @@ -49,6 +55,7 @@ interface FeatureOptInConfirmDialogProps { featureConfig: OptInFeatureConfig; userRoleContext: UserRoleContext; mutations: FeatureOptInMutations; + onTrackFeatureEnabled?: (data: FeatureOptInTrackingData) => void; } export function FeatureOptInConfirmDialog({ @@ -59,6 +66,7 @@ export function FeatureOptInConfirmDialog({ featureConfig, userRoleContext, mutations, + onTrackFeatureEnabled, }: FeatureOptInConfirmDialogProps): ReactElement { const { t } = useLocale(); const router = useRouter(); @@ -159,6 +167,13 @@ export function FeatureOptInConfirmDialog({ await Promise.all(promises); + const enableFor = enableForOrg ? "organization" : hasTeamsSelected ? "teams" : "user"; + onTrackFeatureEnabled?.({ + enableFor, + teamCount: hasTeamsSelected ? selectedTeamIds.length : undefined, + autoOptIn, + }); + setIsSuccess(true); setShouldInvalidate(true); onOptInSuccess?.();