fix: PostHog event tracking (#17653)

* posthog provider fix

* posthog fixes w/ trpc changes

* Update yarn.lock

---------

Co-authored-by: Keith Williams <keithwillcode@gmail.com>
This commit is contained in:
Nizzy
2024-11-19 22:52:18 +00:00
committed by GitHub
co-authored by Keith Williams
parent 1b457f82a1
commit 757e3a2faf
2 changed files with 79 additions and 35 deletions
@@ -2,17 +2,25 @@
import posthog from "posthog-js";
import { PostHogProvider } from "posthog-js/react";
import { useEffect } from "react";
import { useEffect, useRef } from "react";
function Provider({ children }: { children: React.ReactNode }) {
const initializeOnce = useRef(false);
useEffect(() => {
if (!process.env.NEXT_PUBLIC_POSTHOG_KEY) return;
if (!process.env.NEXT_PUBLIC_POSTHOG_KEY || initializeOnce.current) return;
initializeOnce.current = true;
posthog.init(process.env.NEXT_PUBLIC_POSTHOG_KEY, {
api_host: process.env.NEXT_PUBLIC_POSTHOG_HOST || "https://us.i.posthog.com",
autocapture: false,
persistence: "memory",
request_batching: false,
// Enable debug mode in development
request_batching: true,
capture_pageview: false,
capture_pageleave: false,
disable_session_recording: true,
advanced_disable_decide: true,
loaded: (posthog) => {
if (process.env.NODE_ENV === "development") posthog.debug();
},
@@ -1,6 +1,7 @@
// eslint-disable-next-line no-restricted-imports
import { noop } from "lodash";
import type { PostHog } from "posthog-js";
import { usePostHog as usePostHogLib } from "posthog-js/react";
import { useEffect, useRef } from "react";
import { z } from "zod";
import dayjs from "@calcom/dayjs";
@@ -8,26 +9,31 @@ import { WEBSITE_URL } from "@calcom/lib/constants";
import { useHasTeamPlan, useHasPaidPlan } from "@calcom/lib/hooks/useHasPaidPlan";
import { trpc } from "@calcom/trpc/react";
// eslint-disable-next-line turbo/no-undeclared-env-vars
export const isPostHogEnabled = z.string().min(1).safeParse(process.env.NEXT_PUBLIC_POSTHOG_KEY).success;
type PostHogHook = {
// Define the return type for our hook
type PostHogMethods = {
capture: (event: string, properties?: Record<string, any>) => void;
identify: (distinctId: string, properties?: Record<string, any>) => void;
identify: (distinctId?: string, properties?: Record<string, any>) => void;
reset: () => void;
};
const usePostHogHook: () => PostHogHook = isPostHogEnabled
// Internal hook type that includes the methods we need
type InternalPostHogHook = Pick<PostHog, "identify" | "capture" | "reset" | "register" | "get_distinct_id">;
const usePostHogHook: () => InternalPostHogHook = isPostHogEnabled
? usePostHogLib
: () => ({
identify: noop,
capture: noop,
reset: noop,
identify: () => undefined,
capture: () => undefined,
reset: () => undefined,
register: () => undefined,
get_distinct_id: () => "",
});
const usePostHog = () => {
const usePostHog = (): PostHogMethods => {
const posthog = usePostHogHook();
const { data } = trpc.viewer.me.useQuery();
const { data: userData } = trpc.viewer.me.useQuery();
const { data: statsData } = trpc.viewer.myStats.useQuery(undefined, {
trpc: {
context: {
@@ -37,37 +43,67 @@ const usePostHog = () => {
});
const { hasPaidPlan } = useHasPaidPlan();
const { hasTeamPlan } = useHasTeamPlan();
const lastPropertiesString = useRef<string | null>(null);
const identify = async () => {
if (!data || !statsData) return;
useEffect(() => {
if (!userData || !statsData) return;
posthog.identify(String(data.id), {
distinctId: String(data.id),
...(data && data?.name && { name: data.name }),
...(data && data?.email && { email: data.email }),
created_at: String(dayjs(data?.createdDate).unix()),
//keys should be snake cased
user_name: data?.username,
link: `${WEBSITE_URL}/${data?.username}`,
identity_provider: data?.identityProvider,
timezone: data?.timeZone,
locale: data?.locale,
const properties = {
name: userData.name,
email: userData.email,
created_at: String(dayjs(userData.createdDate).unix()),
user_name: userData.username,
link: `${WEBSITE_URL}/${userData.username}`,
identity_provider: userData.identityProvider,
timezone: userData.timeZone,
locale: userData.locale,
has_paid_plan: hasPaidPlan,
has_team_plan: hasTeamPlan,
metadata: data?.metadata,
completed_onboarding: data?.completedOnboarding,
completed_onboarding: userData.completedOnboarding,
has_orgs_plan: !!userData.organizationId,
organization: userData.organization?.slug,
is_premium: userData.isPremium,
sum_of_bookings: statsData?.sumOfBookings,
sum_of_calendars: statsData?.sumOfCalendars,
sum_of_teams: statsData?.sumOfTeams,
has_orgs_plan: !!data?.organizationId,
organization: data?.organization?.slug,
sum_of_event_types: statsData?.sumOfEventTypes,
sum_of_team_event_types: statsData?.sumOfTeamEventTypes,
is_premium: data?.isPremium,
});
};
};
return { identify, capture: posthog.capture, reset: posthog.reset };
// Clean properties
const cleanProperties = Object.fromEntries(Object.entries(properties).filter(([_, v]) => v != null));
// Create a string representation of the properties for comparison
const propertiesString = JSON.stringify(cleanProperties);
// Only set properties if they've changed
if (propertiesString !== lastPropertiesString.current) {
lastPropertiesString.current = propertiesString;
// Use register instead of identify to avoid duplicate calls
posthog.register(cleanProperties);
// Only identify once
if (!posthog.get_distinct_id()) {
posthog.identify(String(userData.id));
}
}
}, [userData, statsData, hasPaidPlan, hasTeamPlan, posthog]);
return {
capture: (event: string, properties?: Record<string, any>) => {
if (posthog.get_distinct_id()) {
posthog.capture(event, properties);
}
},
identify: (distinctId?: string, properties?: Record<string, any>) => {
if (distinctId) {
posthog.identify(distinctId, properties);
}
},
reset: () => {
lastPropertiesString.current = null;
posthog.reset();
},
};
};
export default usePostHog;