feat: add PostHog event tracking for feature opt-in banner and dialog (#27579)

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
This commit is contained in:
Eunjae Lee
2026-02-04 09:32:35 +00:00
committed by GitHub
co-authored by Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
parent 4367028492
commit 5804cc4fb0
3 changed files with 63 additions and 2 deletions
@@ -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,
};
}
@@ -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}
/>
)}
</>
@@ -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?.();