Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: adding posthog libs for node + js #16862

Merged
5 changes: 5 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,11 @@ NEXT_PUBLIC_INTERCOM_APP_ID=
# Secret to enable Intercom Identity Verification
INTERCOM_SECRET=

# Posthog Config
NEXT_PUBLIC_POSTHOG_KEY=

NEXT_PUBLIC_POSTHOG_HOST=

# Zendesk Config
NEXT_PUBLIC_ZENDESK_KEY=

Expand Down
5 changes: 4 additions & 1 deletion apps/web/lib/app-providers-app-dir.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import type { ReactNode } from "react";
import { useEffect } from "react";
import CacheProvider from "react-inlinesvg/provider";

import DynamicPostHogProvider from "@calcom/features/ee/event-tracking/lib/posthog/providerDynamic";
import { OrgBrandingProvider } from "@calcom/features/ee/organizations/context/provider";
import DynamicHelpscoutProvider from "@calcom/features/ee/support/lib/helpscout/providerDynamic";
import DynamicIntercomProvider from "@calcom/features/ee/support/lib/intercom/providerDynamic";
Expand Down Expand Up @@ -299,7 +300,9 @@ const AppProviders = (props: PageWrapperProps) => {

return (
<DynamicHelpscoutProvider>
<DynamicIntercomProvider>{RemainingProviders}</DynamicIntercomProvider>
<DynamicIntercomProvider>
<DynamicPostHogProvider>{RemainingProviders}</DynamicPostHogProvider>
</DynamicIntercomProvider>
</DynamicHelpscoutProvider>
);
};
Expand Down
16 changes: 15 additions & 1 deletion apps/web/lib/app-providers.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,13 @@ import type { SSRConfig } from "next-i18next";
import { appWithTranslation } from "next-i18next";
import { ThemeProvider } from "next-themes";
import type { AppProps as NextAppProps, AppProps as NextJsAppProps } from "next/app";
import dynamic from "next/dynamic";
import type { ParsedUrlQuery } from "querystring";
import type { PropsWithChildren, ReactNode } from "react";
import { useEffect } from "react";
import CacheProvider from "react-inlinesvg/provider";

import DynamicPostHogProvider from "@calcom/features/ee/event-tracking/lib/posthog/providerDynamic";
import { OrgBrandingProvider } from "@calcom/features/ee/organizations/context/provider";
import DynamicHelpscoutProvider from "@calcom/features/ee/support/lib/helpscout/providerDynamic";
import DynamicIntercomProvider from "@calcom/features/ee/support/lib/intercom/providerDynamic";
Expand Down Expand Up @@ -56,6 +58,13 @@ export type AppProps = Omit<
err?: Error;
};

const PostHogPageView = dynamic(
() => import("@calcom/features/ee/event-tracking/lib/posthog/web/PostHogPageView"),
{
ssr: false,
}
);

type AppPropsWithChildren = AppProps & {
children: ReactNode;
};
Expand Down Expand Up @@ -319,7 +328,12 @@ const AppProviders = (props: AppPropsWithChildren) => {

return (
<DynamicHelpscoutProvider>
<DynamicIntercomProvider>{RemainingProviders}</DynamicIntercomProvider>
<DynamicIntercomProvider>
<DynamicPostHogProvider>
<PostHogPageView />
{RemainingProviders}
</DynamicPostHogProvider>
</DynamicIntercomProvider>
</DynamicHelpscoutProvider>
);
};
Expand Down
6 changes: 3 additions & 3 deletions apps/web/modules/signup-view.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -624,7 +624,7 @@ export default function Signup({
</div>
</div>
</div>
<div className="border-subtle lg:bg-subtle mx-auto mt-24 w-full max-w-2xl flex-col justify-between rounded-l-2xl pl-4 dark:bg-none lg:mt-0 lg:flex lg:max-w-full lg:border lg:py-12 lg:pl-12">
<div className="border-subtle lg:bg-subtle mx-auto mt-24 w-full max-w-2xl flex-col justify-between rounded-l-2xl pl-4 lg:mt-0 lg:flex lg:max-w-full lg:border lg:py-12 lg:pl-12 dark:bg-none">
{IS_CALCOM && (
<>
<div className="-mt-4 mb-6 mr-12 grid w-full grid-cols-3 gap-5 pr-4 sm:gap-3 lg:grid-cols-4">
Expand Down Expand Up @@ -675,7 +675,7 @@ export default function Signup({
</div>
</>
)}
<div className="border-default hidden rounded-bl-2xl rounded-br-none rounded-tl-2xl border border-r-0 border-dashed bg-black/[3%] dark:bg-white/5 lg:block lg:py-[6px] lg:pl-[6px]">
<div className="border-default hidden rounded-bl-2xl rounded-br-none rounded-tl-2xl border border-r-0 border-dashed bg-black/[3%] lg:block lg:py-[6px] lg:pl-[6px] dark:bg-white/5">
<img className="block dark:hidden" src="/mock-event-type-list.svg" alt="Cal.com Booking Page" />
<img
className="hidden dark:block"
Expand All @@ -686,7 +686,7 @@ export default function Signup({
<div className="mr-12 mt-8 hidden h-full w-full grid-cols-3 gap-4 overflow-hidden lg:grid">
{FEATURES.map((feature) => (
<>
<div className="max-w-52 mb-8 flex flex-col leading-none sm:mb-0">
<div className="mb-8 flex max-w-52 flex-col leading-none sm:mb-0">
<div className="text-emphasis items-center">
<Icon name={feature.icon} className="mb-1 h-4 w-4" />
<span className="text-sm font-medium">{t(feature.title)}</span>
Expand Down
2 changes: 2 additions & 0 deletions apps/web/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,8 @@
"nodemailer": "^6.7.8",
"nuqs": "^1.20.0",
"otplib": "^12.0.1",
"posthog-js": "^1.164.1",
"posthog-node": "^4.2.0",
"qrcode": "^1.5.1",
"raw-body": "^2.5.1",
"react": "^18.2.0",
Expand Down
43 changes: 26 additions & 17 deletions packages/features/auth/lib/next-auth-options.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import GoogleProvider from "next-auth/providers/google";

import { LicenseKeySingleton } from "@calcom/ee/common/server/LicenseKeyService";
import createUsersAndConnectToOrg from "@calcom/features/ee/dsync/lib/users/createUsersAndConnectToOrg";
import postHogClient from "@calcom/features/ee/event-tracking/lib/posthog/postHogClient";
import ImpersonationProvider from "@calcom/features/ee/impersonation/lib/ImpersonationProvider";
import { getOrgFullOrigin, subdomainSuffix } from "@calcom/features/ee/organizations/lib/orgDomains";
import { clientSecretVerifier, hostedCal, isSAMLLoginEnabled } from "@calcom/features/ee/sso/lib/saml";
Expand Down Expand Up @@ -947,24 +948,32 @@ export const getOptions = ({
// this is a workaround – in the future once we move to use the Account model in the DB
// we should use NextAuth's isNewUser flag instead: https://next-auth.js.org/configuration/events#signin
const isNewUser = new Date(user.createdDate) > new Date(Date.now() - 10 * 60 * 1000);
if ((isENVDev || IS_CALCOM) && process.env.DUB_API_KEY && isNewUser) {
const clickId = getDubId();
// check if there's a clickId (dub_id) cookie set by @dub/analytics
if (clickId) {
// here we use waitUntil – meaning this code will run async to not block the main thread
waitUntil(
// if so, send a lead event to Dub
// @see https://d.to/conversions/next-auth
dub.track.lead({
clickId,
eventName: "Sign Up",
customerId: user.id.toString(),
customerName: user.name,
customerEmail: user.email,
customerAvatar: user.image,
})
);
if ((isENVDev || IS_CALCOM) && isNewUser) {
if (process.env.DUB_API_KEY) {
const clickId = getDubId();
// check if there's a clickId (dub_id) cookie set by @dub/analytics
if (clickId) {
// here we use waitUntil – meaning this code will run async to not block the main thread
waitUntil(
// if so, send a lead event to Dub
// @see https://d.to/conversions/next-auth
dub.track.lead({
clickId,
eventName: "Sign Up",
customerId: user.id.toString(),
customerName: user.name,
customerEmail: user.email,
customerAvatar: user.image,
})
);
}
}

postHogClient().capture(user.id.toString(), "Sign Up", {
sean-brydon marked this conversation as resolved.
Show resolved Hide resolved
email: user.email,
name: user.name,
username: user.username,
});
}
},
},
Expand Down
4 changes: 2 additions & 2 deletions packages/features/bookings/Booker/Booker.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -275,7 +275,7 @@ const BookerComponent = ({
data-testid="booker-container"
className={classNames(
...getBookerSizeClassNames(layout, bookerState, hideEventTypeDetails),
`bg-default dark:bg-muted grid max-w-full items-start dark:[color-scheme:dark] sm:transition-[width] sm:duration-300 sm:motion-reduce:transition-none md:flex-row`,
`bg-default dark:bg-muted grid max-w-full items-start sm:transition-[width] sm:duration-300 sm:motion-reduce:transition-none md:flex-row dark:[color-scheme:dark]`,
// We remove border only when the content covers entire viewport. Because in embed, it can almost never be the case that it covers entire viewport, we show the border there
(layout === BookerLayouts.MONTH_VIEW || isEmbed) && "border-subtle rounded-md",
!isEmbed && "sm:transition-[width] sm:duration-300",
Expand Down Expand Up @@ -411,7 +411,7 @@ const BookerComponent = ({
layout === BookerLayouts.COLUMN_VIEW
}
className={classNames(
"border-subtle rtl:border-default flex h-full w-full flex-col overflow-x-auto px-5 py-3 pb-0 rtl:border-r ltr:md:border-l",
"border-subtle rtl:border-default flex h-full w-full flex-col overflow-x-auto px-5 py-3 pb-0 ltr:md:border-l rtl:border-r",
layout === BookerLayouts.MONTH_VIEW &&
"h-full overflow-hidden md:w-[var(--booker-timeslots-width)]",
layout !== BookerLayouts.MONTH_VIEW && "sticky top-0"
Expand Down
40 changes: 40 additions & 0 deletions packages/features/ee/event-tracking/lib/posthog/postHogClient.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
// eslint-disable-next-line no-restricted-imports
import { noop } from "lodash";
import { PostHog, type PostHog as PostHogType } from "posthog-node";

let postHog: {
capture: PostHogType["capture"];
identify: PostHogType["identify"];
};

// eslint-disable-next-line turbo/no-undeclared-env-vars
if (process.env.NEXT_PUBLIC_POSTHOG_KEY) {
// eslint-disable-next-line turbo/no-undeclared-env-vars
postHog = new PostHog(process.env.NEXT_PUBLIC_POSTHOG_KEY, {
// eslint-disable-next-line turbo/no-undeclared-env-vars
host: process.env.NEXT_PUBLIC_POSTHOG_HOST,
flushAt: 1,
});
} else {
postHog = {
capture: noop,
identify: noop,
};
}

export function postHogClient() {
function capture(distinctId: string, event: string, properties?: Record<string, any>) {
postHog?.capture({ distinctId, event, properties });
}

function identify(distinctId: string, properties?: Record<string, any>) {
postHog?.identify({ distinctId, properties });
}

return {
capture,
identify,
};
}

export default postHogClient;
25 changes: 25 additions & 0 deletions packages/features/ee/event-tracking/lib/posthog/provider.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
"use client";

import posthog from "posthog-js";
import { PostHogProvider } from "posthog-js/react";
import { useEffect } from "react";

function Provider({ children }: { children: React.ReactNode }) {
useEffect(() => {
if (!process.env.NEXT_PUBLIC_POSTHOG_KEY) return;
posthog.init(process.env.NEXT_PUBLIC_POSTHOG_KEY, {
api_host: process.env.NEXT_PUBLIC_POSTHOG_HOST || "https://us.i.posthog.com",
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Even though we use the EU PostHog region, we can keep the US one as the fallback

autocapture: false,
persistence: "memory",
request_batching: false,
// Enable debug mode in development
loaded: (posthog) => {
if (process.env.NODE_ENV === "development") posthog.debug();
},
});
}, []);

return <PostHogProvider client={posthog}>{children}</PostHogProvider>;
}

export default Provider;
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import dynamic from "next/dynamic";
import { Fragment } from "react";

const initPostProvider = () => {
// eslint-disable-next-line turbo/no-undeclared-env-vars
if (!process.env.NEXT_PUBLIC_POSTHOG_KEY) {
return Fragment;
}

return dynamic(() => import("./provider"));
};

const DynamicPostHogProvider = initPostProvider();
export default DynamicPostHogProvider;
66 changes: 66 additions & 0 deletions packages/features/ee/event-tracking/lib/posthog/userPostHog.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
// eslint-disable-next-line no-restricted-imports
import { noop } from "lodash";
import { usePostHog as usePostHogLib } from "posthog-js/react";
import { z } from "zod";

import dayjs from "@calcom/dayjs";
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 = {
capture: (event: string, properties?: Record<string, any>) => void;
identify: (distinctId: string, properties?: Record<string, any>) => void;
reset: () => void;
};

const usePostHogHook: () => PostHogHook = isPostHogEnabled
? usePostHogLib
: () => ({
identify: noop,
capture: noop,
reset: noop,
});

const usePostHog = () => {
const posthog = usePostHogHook();
const { data } = trpc.viewer.me.useQuery();
const { hasPaidPlan } = useHasPaidPlan();
const { hasTeamPlan } = useHasTeamPlan();

const identify = async () => {
if (!data) 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,
has_paid_plan: hasPaidPlan,
has_team_plan: hasTeamPlan,
metadata: data?.metadata,
completed_onboarding: data?.completedOnboarding,
sum_of_bookings: data?.sumOfBookings,
sum_of_calendars: data?.sumOfCalendars,
sum_of_teams: data?.sumOfTeams,
has_orgs_plan: !!data?.organizationId,
organization: data?.organization?.slug,
sum_of_event_types: data?.sumOfEventTypes,
sum_of_team_event_types: data?.sumOfTeamEventTypes,
is_premium: data?.isPremium,
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@sean-brydon check which properties to remove

});
};

return { identify, capture: posthog.capture, reset: posthog.reset };
};

export default usePostHog;
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
"use client";

import { usePathname, useSearchParams } from "next/navigation";
import { usePostHog } from "posthog-js/react";
import { useEffect } from "react";

export default function PostHogPageView() {
const pathname = usePathname();
const searchParams = useSearchParams();
const posthog = usePostHog();
useEffect(() => {
// Track pageviews
if (pathname && posthog) {
let url = window.origin + pathname;
if (searchParams.toString()) {
url = `${url}?${searchParams.toString()}`;
}
posthog.capture("$pageview", {
$current_url: url,
});
}
}, [pathname, searchParams, posthog]);

return null;
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ import { Button, Dialog, DialogClose, DialogContent, DialogFooter, showToast } f

import CreateEventTypeForm from "./CreateEventTypeForm";

import usePostHog from "../../ee/event-tracking/lib/posthog/userPostHog";

// this describes the uniform data needed to create a new event type on Profile or Team
export interface EventTypeParent {
teamId: number | null | undefined; // if undefined, then it's a profile
Expand Down Expand Up @@ -60,6 +62,7 @@ export default function CreateEventTypeDialog({
membershipRole: MembershipRole | null | undefined;
}[];
}) {
const postHog = usePostHog();
const { t } = useLocale();
const router = useRouter();
const orgBranding = useOrgBranding();
Expand Down Expand Up @@ -137,6 +140,7 @@ export default function CreateEventTypeDialog({
form={form}
isManagedEventType={isManagedEventType}
handleSubmit={(values) => {
postHog.capture("Event Created Frontend");
createMutation.mutate(values);
}}
SubmitButton={SubmitButton}
Expand Down
2 changes: 1 addition & 1 deletion packages/features/insights/filters/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ const ClearFilters = () => {
color="secondary"
target="_blank"
rel="noreferrer"
className="min-w-24 h-[38px] border-0"
className="h-[38px] min-w-24 border-0"
onClick={() => {
clearFilters();
}}>
Expand Down
Loading
Loading