diff --git a/apps/web/app/(use-page-wrapper)/(main-nav)/availability/page.tsx b/apps/web/app/(use-page-wrapper)/(main-nav)/availability/page.tsx index 21a6665ca5..703ec3b492 100644 --- a/apps/web/app/(use-page-wrapper)/(main-nav)/availability/page.tsx +++ b/apps/web/app/(use-page-wrapper)/(main-nav)/availability/page.tsx @@ -64,8 +64,8 @@ const Page = async ({ searchParams: _searchParams }: PageProps) => { const organizationRepository = getOrganizationRepository(); const isOrgPrivate = organizationId ? await organizationRepository.checkIfPrivate({ - orgId: organizationId, - }) + orgId: organizationId, + }) : false; const permissionService = new PermissionCheckService(); @@ -81,12 +81,7 @@ const Page = async ({ searchParams: _searchParams }: PageProps) => { heading={t("availability")} subtitle={t("configure_availability")} CTA={ - + }> {searchParams?.type === "team" && canViewTeamAvailability ? ( diff --git a/apps/web/app/(use-page-wrapper)/(main-nav)/availability/skeleton.tsx b/apps/web/app/(use-page-wrapper)/(main-nav)/availability/skeleton.tsx index d6ad66b184..8f539ed39a 100644 --- a/apps/web/app/(use-page-wrapper)/(main-nav)/availability/skeleton.tsx +++ b/apps/web/app/(use-page-wrapper)/(main-nav)/availability/skeleton.tsx @@ -9,16 +9,12 @@ import { AvailabilityCTA } from "~/availability/availability-view"; export default function AvailabilityLoader() { const { t } = useLocale(); - const toggleGroupOptions = [ - { value: "mine", label: t("my_availability") }, - { value: "team", label: t("team_availability") }, - ]; return ( }> + CTA={}> ); diff --git a/apps/web/app/(use-page-wrapper)/(main-nav)/teams/CTA.tsx b/apps/web/app/(use-page-wrapper)/(main-nav)/teams/CTA.tsx index 8ec59602dd..a39498673f 100644 --- a/apps/web/app/(use-page-wrapper)/(main-nav)/teams/CTA.tsx +++ b/apps/web/app/(use-page-wrapper)/(main-nav)/teams/CTA.tsx @@ -1,4 +1,5 @@ "use client"; +import posthog from "posthog-js"; import { WEBAPP_URL } from "@calcom/lib/constants"; import { useLocale } from "@calcom/lib/hooks/useLocale"; @@ -13,6 +14,9 @@ export const TeamsCTA = () => { StartIcon="plus" size="sm" type="button" + onClick={() => { + posthog.capture("add_team_button_clicked") + }} href={`${WEBAPP_URL}/settings/teams/new?returnTo=${WEBAPP_URL}/teams`}> {t("new")} diff --git a/apps/web/app/(use-page-wrapper)/apps/routing-forms/forms/[[...pages]]/Forms.tsx b/apps/web/app/(use-page-wrapper)/apps/routing-forms/forms/[[...pages]]/Forms.tsx index b08a18f8ae..d19c52a597 100644 --- a/apps/web/app/(use-page-wrapper)/apps/routing-forms/forms/[[...pages]]/Forms.tsx +++ b/apps/web/app/(use-page-wrapper)/apps/routing-forms/forms/[[...pages]]/Forms.tsx @@ -3,6 +3,7 @@ import { useAutoAnimate } from "@formkit/auto-animate/react"; import { useEffect, useState } from "react"; import { useFormContext } from "react-hook-form"; +import posthog from "posthog-js"; import { isFallbackRoute } from "@calcom/app-store/routing-forms/lib/isFallbackRoute"; import type { RoutingFormWithResponseCount } from "@calcom/app-store/routing-forms/types/types"; @@ -46,6 +47,7 @@ function NewFormButton({ setNewFormDialogState }: { setNewFormDialogState: SetNe data-testid="new-routing-form" createFunction={(teamId) => { setNewFormDialogState({ action: "new", target: teamId ? String(teamId) : "" }); + posthog.capture("new_routing_form_button_clicked", { teamId }); }} withPermission={{ permission: "routingForm.create", @@ -76,6 +78,7 @@ export default function RoutingForms({ appUrl }: { appUrl: string }) { useEffect(() => { hookForm.reset({}); + // eslint-disable-next-line react-hooks/exhaustive-deps }, []); const filters = getTeamsFiltersFromQuery(routerQuery); diff --git a/apps/web/app/(use-page-wrapper)/settings/(settings-layout)/SettingsLayoutAppDirClient.tsx b/apps/web/app/(use-page-wrapper)/settings/(settings-layout)/SettingsLayoutAppDirClient.tsx index 0ca9440c30..e154eac956 100644 --- a/apps/web/app/(use-page-wrapper)/settings/(settings-layout)/SettingsLayoutAppDirClient.tsx +++ b/apps/web/app/(use-page-wrapper)/settings/(settings-layout)/SettingsLayoutAppDirClient.tsx @@ -37,13 +37,13 @@ const getTabs = (orgBranding: OrganizationBranding | null) => { href: "/settings/my-account", icon: "user", children: [ - { name: "profile", href: "/settings/my-account/profile" }, - { name: "general", href: "/settings/my-account/general" }, - { name: "calendars", href: "/settings/my-account/calendars" }, - { name: "conferencing", href: "/settings/my-account/conferencing" }, - { name: "appearance", href: "/settings/my-account/appearance" }, - { name: "out_of_office", href: "/settings/my-account/out-of-office" }, - { name: "push_notifications", href: "/settings/my-account/push-notifications" }, + { name: "profile", href: "/settings/my-account/profile", trackingMetadata: { section: "my_account", page: "profile" } }, + { name: "general", href: "/settings/my-account/general", trackingMetadata: { section: "my_account", page: "general" } }, + { name: "calendars", href: "/settings/my-account/calendars", trackingMetadata: { section: "my_account", page: "calendars" } }, + { name: "conferencing", href: "/settings/my-account/conferencing", trackingMetadata: { section: "my_account", page: "conferencing" } }, + { name: "appearance", href: "/settings/my-account/appearance", trackingMetadata: { section: "my_account", page: "appearance" } }, + { name: "out_of_office", href: "/settings/my-account/out-of-office", trackingMetadata: { section: "my_account", page: "out_of_office" } }, + { name: "push_notifications", href: "/settings/my-account/push-notifications", trackingMetadata: { section: "my_account", page: "push_notifications" } }, // TODO // { name: "referrals", href: "/settings/my-account/referrals" }, ], @@ -53,16 +53,16 @@ const getTabs = (orgBranding: OrganizationBranding | null) => { href: "/settings/security", icon: "key", children: [ - { name: "password", href: "/settings/security/password" }, - { name: "impersonation", href: "/settings/security/impersonation" }, - { name: "2fa_auth", href: "/settings/security/two-factor-auth" }, + { name: "password", href: "/settings/security/password", trackingMetadata: { section: "security", page: "password" } }, + { name: "impersonation", href: "/settings/security/impersonation", trackingMetadata: { section: "security", page: "impersonation" } }, + { name: "2fa_auth", href: "/settings/security/two-factor-auth", trackingMetadata: { section: "security", page: "2fa_auth" } }, ], }, { name: "billing", href: "/settings/billing", icon: "credit-card", - children: [{ name: "manage_billing", href: "/settings/billing" }], + children: [{ name: "manage_billing", href: "/settings/billing", trackingMetadata: { section: "billing", page: "manage_billing" } }], }, { name: "developer", @@ -70,9 +70,9 @@ const getTabs = (orgBranding: OrganizationBranding | null) => { icon: "terminal", children: [ // - { name: "webhooks", href: "/settings/developer/webhooks" }, - { name: "api_keys", href: "/settings/developer/api-keys" }, - { name: "admin_api", href: "/settings/organizations/admin-api" }, + { name: "webhooks", href: "/settings/developer/webhooks", trackingMetadata: { section: "developer", page: "webhooks" } }, + { name: "api_keys", href: "/settings/developer/api-keys", trackingMetadata: { section: "developer", page: "api_keys" } }, + { name: "admin_api", href: "/settings/organizations/admin-api", trackingMetadata: { section: "developer", page: "admin_api" } }, // TODO: Add profile level for embeds // { name: "embeds", href: "/v2/settings/developer/embeds" }, ], @@ -84,36 +84,43 @@ const getTabs = (orgBranding: OrganizationBranding | null) => { { name: "profile", href: "/settings/organizations/profile", + trackingMetadata: { section: "organization", page: "profile" }, }, { name: "general", href: "/settings/organizations/general", + trackingMetadata: { section: "organization", page: "general" }, }, ...(orgBranding ? [ - { - name: "members", - href: `${WEBAPP_URL}/settings/organizations/${orgBranding.slug}/members`, - isExternalLink: true, - }, - ] + { + name: "members", + href: `${WEBAPP_URL}/settings/organizations/${orgBranding.slug}/members`, + isExternalLink: true, + trackingMetadata: { section: "organization", page: "members" }, + }, + ] : []), { name: "privacy_and_security", href: "/settings/organizations/privacy", + trackingMetadata: { section: "organization", page: "privacy_and_security" }, }, { name: "SSO", href: "/settings/organizations/sso", + trackingMetadata: { section: "organization", page: "sso" }, }, { name: "directory_sync", href: "/settings/organizations/dsync", + trackingMetadata: { section: "organization", page: "directory_sync" }, }, { name: "admin_api", href: "https://cal.com/docs/enterprise-features/api/api-reference/bookings#admin-access", isExternalLink: true, + trackingMetadata: { section: "organization", page: "admin_api" }, }, ], }, @@ -135,31 +142,31 @@ const getTabs = (orgBranding: OrganizationBranding | null) => { icon: "lock", children: [ // - { name: "features", href: "/settings/admin/flags" }, - { name: "license", href: "/auth/setup?step=1" }, - { name: "impersonation", href: "/settings/admin/impersonation" }, - { name: "apps", href: "/settings/admin/apps/calendar" }, - { name: "users", href: "/settings/admin/users" }, - { name: "organizations", href: "/settings/admin/organizations" }, - { name: "lockedSMS", href: "/settings/admin/lockedSMS" }, - { name: "oAuth", href: "/settings/admin/oAuth" }, - { name: "Workspace Platforms", href: "/settings/admin/workspace-platforms" }, - { name: "Playground", href: "/settings/admin/playground" }, + { name: "features", href: "/settings/admin/flags", trackingMetadata: { section: "admin", page: "features" } }, + { name: "license", href: "/auth/setup?step=1", trackingMetadata: { section: "admin", page: "license" } }, + { name: "impersonation", href: "/settings/admin/impersonation", trackingMetadata: { section: "admin", page: "impersonation" } }, + { name: "apps", href: "/settings/admin/apps/calendar", trackingMetadata: { section: "admin", page: "apps" } }, + { name: "users", href: "/settings/admin/users", trackingMetadata: { section: "admin", page: "users" } }, + { name: "organizations", href: "/settings/admin/organizations", trackingMetadata: { section: "admin", page: "organizations" } }, + { name: "lockedSMS", href: "/settings/admin/lockedSMS", trackingMetadata: { section: "admin", page: "locked_sms" } }, + { name: "oAuth", href: "/settings/admin/oAuth", trackingMetadata: { section: "admin", page: "oauth" } }, + { name: "Workspace Platforms", href: "/settings/admin/workspace-platforms", trackingMetadata: { section: "admin", page: "workspace_platforms" } }, + { name: "Playground", href: "/settings/admin/playground", trackingMetadata: { section: "admin", page: "playground" } }, ], }, ]; tabs.find((tab) => { if (tab.name === "security" && !HOSTED_CAL_FEATURES) { - tab.children?.push({ name: "sso_configuration", href: "/settings/security/sso" }); + tab.children?.push({ name: "sso_configuration", href: "/settings/security/sso", trackingMetadata: { section: "security", page: "sso_configuration" } }); // TODO: Enable dsync for self hosters // tab.children?.push({ name: "directory_sync", href: "/settings/security/dsync" }); } if (tab.name === "admin" && IS_CALCOM) { - tab.children?.push({ name: "create_org", href: "/settings/organizations/new" }); + tab.children?.push({ name: "create_org", href: "/settings/organizations/new", trackingMetadata: { section: "admin", page: "create_org" } }); } if (tab.name === "admin" && IS_CALCOM) { - tab.children?.push({ name: "create_license_key", href: "/settings/license-key/new" }); + tab.children?.push({ name: "create_license_key", href: "/settings/license-key/new", trackingMetadata: { section: "admin", page: "create_license_key" } }); } }); @@ -216,6 +223,7 @@ const useTabs = ({ newArray.splice(4, 0, { name: "attributes", href: "/settings/organizations/attributes", + trackingMetadata: { section: "organization", page: "attributes" }, }); } @@ -224,6 +232,7 @@ const useTabs = ({ newArray.push({ name: "delegation_credential", href: "/settings/organizations/delegation-credential", + trackingMetadata: { section: "organization", page: "delegation_credential" }, }); } @@ -233,6 +242,7 @@ const useTabs = ({ newArray.push({ name: "roles_and_permissions", href: "/settings/organizations/roles", + trackingMetadata: { section: "organization", page: "roles_and_permissions" }, }); } @@ -240,6 +250,7 @@ const useTabs = ({ newArray.push({ name: "billing", href: "/settings/organizations/billing", + trackingMetadata: { section: "organization", page: "billing" }, }); } } else { @@ -247,6 +258,7 @@ const useTabs = ({ newArray.push({ name: "billing", href: "/settings/organizations/billing", + trackingMetadata: { section: "organization", page: "billing" }, }); } // Show roles page (modal will appear if PBAC not enabled) @@ -254,7 +266,8 @@ const useTabs = ({ newArray.push({ name: "roles", href: "/settings/organizations/roles", - isBadged: true, // Show "New" badge + isBadged: true, // Show "New" badge, + trackingMetadata: { section: "organization", page: "roles" }, }); } } @@ -345,6 +358,7 @@ const TeamRolesNavItem = ({ @@ -420,9 +434,8 @@ const TeamListCollapsible = ({ teamFeatures }: { teamFeatures?: Record + aria-label={`${team.name} ${teamMenuState[index].teamMenuOpen ? t("collapse_menu") : t("expand_menu") + }`}>
{teamMenuState[index].teamMenuOpen ? ( @@ -430,6 +443,7 @@ const TeamListCollapsible = ({ teamFeatures }: { teamFeatures?: Record )}
+ { } {!team.parentId && ( - {/* TODO */} - {/* + {/* TODO */} + {/* @@ -492,6 +509,7 @@ const TeamListCollapsible = ({ teamFeatures }: { teamFeatures?: Record @@ -501,6 +519,7 @@ const TeamListCollapsible = ({ teamFeatures }: { teamFeatures?: Record @@ -601,6 +620,7 @@ const SettingsSidebarContainer = ({ className="text-subtle h-[16px] w-[16px] stroke-[2px] ltr:mr-3 rtl:ml-3 md:mt-0" /> )} + { } {!tab.icon && tab?.avatar && ( + aria-label={`${otherTeam.name} ${otherTeamMenuState[index].teamMenuOpen + ? t("collapse_menu") + : t("expand_menu") + }`}>
{otherTeamMenuState[index].teamMenuOpen ? ( @@ -754,6 +775,7 @@ const SettingsSidebarContainer = ({ )}
+ { } {!otherTeam.parentId && ( (undefined); + +export function GeoProvider({ + country, + children, +}: { + country: string; + children: React.ReactNode; +}) { + return {children}; +} + +export function useGeo() { + const context = useContext(GeoContext); + if (context === undefined) { + throw new Error("useGeo must be used within a GeoProvider"); + } + return context; +} diff --git a/apps/web/app/layout.tsx b/apps/web/app/layout.tsx index 98a85314a8..c4849a1334 100644 --- a/apps/web/app/layout.tsx +++ b/apps/web/app/layout.tsx @@ -98,6 +98,8 @@ export default async function RootLayout({ children }: { children: React.ReactNo const h = await headers(); const nonce = h.get("x-csp-nonce") ?? ""; + const country = h.get("cf-ipcountry") || h.get("x-vercel-ip-country") || "Unknown"; + const { locale, direction, isEmbed, embedColorScheme } = await getInitialProps(); const ns = "common"; @@ -154,7 +156,7 @@ export default async function RootLayout({ children }: { children: React.ReactNo ]} /> - + {children} diff --git a/apps/web/app/providers.tsx b/apps/web/app/providers.tsx index 2f51289b23..2ca9379608 100644 --- a/apps/web/app/providers.tsx +++ b/apps/web/app/providers.tsx @@ -9,23 +9,28 @@ import { NotificationSoundHandler } from "@calcom/web/components/notification-so import useIsBookingPage from "@lib/hooks/useIsBookingPage"; +import { GeoProvider } from "./GeoContext"; + type ProvidersProps = { isEmbed: boolean; children: React.ReactNode; nonce: string | undefined; + country: string; }; -export function Providers({ isEmbed, children, nonce }: ProvidersProps) { +export function Providers({ isEmbed, children, country }: ProvidersProps) { const isBookingPage = useIsBookingPage(); return ( - - - {!isEmbed && !isBookingPage && } - {/* @ts-expect-error FIXME remove this comment when upgrading typescript to v5 */} - - {children} - - - + + + + {!isEmbed && !isBookingPage && } + {/* @ts-expect-error FIXME remove this comment when upgrading typescript to v5 */} + + {children} + + + + ); } diff --git a/apps/web/components/getting-started/components/AppConnectionItem.tsx b/apps/web/components/getting-started/components/AppConnectionItem.tsx index 9f3a284660..81731873bd 100644 --- a/apps/web/components/getting-started/components/AppConnectionItem.tsx +++ b/apps/web/components/getting-started/components/AppConnectionItem.tsx @@ -1,5 +1,6 @@ import Link from "next/link"; import { useState } from "react"; +import posthog from "posthog-js"; import { InstallAppButtonWithoutPlanCheck } from "@calcom/app-store/InstallAppButtonWithoutPlanCheck"; import type { TDependencyData } from "@calcom/app-store/_appRegistry"; @@ -44,6 +45,7 @@ const AppConnectionItem = (props: IAppConnectionItem) => { return (
+ {/* eslint-disable @next/next/no-img-element */} {title}

{title}

{isDefault && {t("default")}} @@ -108,6 +110,12 @@ const AppConnectionItem = (props: IAppConnectionItem) => { ) : undefined } onClick={(event) => { + posthog.capture("onboarding_app_connect_clicked", { + app_title: title, + app_type: type, + app_slug: slug, + has_dependency: !!dependency, + }); // Save cookie key to return url step document.cookie = `return-to=${window.location.href};path=/;max-age=3600;SameSite=Lax`; buttonProps && buttonProps.onClick && buttonProps?.onClick(event); diff --git a/apps/web/components/getting-started/steps-views/UserProfile.tsx b/apps/web/components/getting-started/steps-views/UserProfile.tsx index 3c1325efae..35942fab89 100644 --- a/apps/web/components/getting-started/steps-views/UserProfile.tsx +++ b/apps/web/components/getting-started/steps-views/UserProfile.tsx @@ -3,6 +3,7 @@ import { useRouter } from "next/navigation"; import { useRef, useState } from "react"; import { useForm } from "react-hook-form"; +import posthog from "posthog-js"; import { useLocale } from "@calcom/lib/hooks/useLocale"; import { md } from "@calcom/lib/markdownIt"; @@ -65,6 +66,8 @@ const UserProfile = ({ user }: UserProfileProps) => { console.error(error); } + posthog.capture("onboarding_completed"); + await utils.viewer.me.get.refetch(); const redirectUrl = localStorage.getItem("onBoardingRedirect"); localStorage.removeItem("onBoardingRedirect"); diff --git a/apps/web/lib/app-providers-app-dir.tsx b/apps/web/lib/app-providers-app-dir.tsx index 1175d50158..e2fa2a0e54 100644 --- a/apps/web/lib/app-providers-app-dir.tsx +++ b/apps/web/lib/app-providers-app-dir.tsx @@ -20,6 +20,8 @@ import type { WithLocaleProps } from "@lib/withLocale"; import type { PageWrapperProps } from "@components/PageWrapperAppDir"; +import DynamicPostHogPageView from "@calcom/features/ee/event-tracking/lib/posthog/web/pageViewDynamic"; + import { getThemeProviderProps } from "./getThemeProviderProps"; // Workaround for https://github.com/vercel/next.js/issues/8592 @@ -77,6 +79,7 @@ const CalcomThemeProvider = (props: CalcomThemeProps) => { {/* Embed Mode can be detected reliably only on client side here as there can be static generated pages as well which can't determine if it's embed mode at backend */} {/* color-scheme makes background:transparent not work in iframe which is required by embed. */} {typeof window !== "undefined" && !isEmbedMode && ( + //eslint-disable-next-line react/no-unknown-property