diff --git a/ui/components/auth/oss/auth-form.tsx b/ui/components/auth/oss/auth-form.tsx index 7c68363af8..25791f6cbf 100644 --- a/ui/components/auth/oss/auth-form.tsx +++ b/ui/components/auth/oss/auth-form.tsx @@ -4,7 +4,6 @@ import { zodResolver } from "@hookform/resolvers/zod"; import { Icon } from "@iconify/react"; import { Button, Checkbox, Divider, Link, Tooltip } from "@nextui-org/react"; import { useRouter, useSearchParams } from "next/navigation"; -import posthog from "posthog-js"; import { useEffect } from "react"; import { useForm } from "react-hook-form"; import { z } from "zod"; @@ -22,6 +21,7 @@ import { FormField, FormMessage, } from "@/components/ui/form"; +import { initializeSession, trackUserLogin, trackUserRegistration } from "@/lib/analytics"; import { ApiError, authFormSchema } from "@/types"; export const AuthForm = ({ @@ -82,7 +82,7 @@ export const AuthForm = ({ const onSubmit = async (data: z.infer) => { //getting an new posthog init - posthog.reset(true); + initializeSession(); if (type === "sign-in") { if (data.isSamlMode) { const email = data.email.toLowerCase(); @@ -110,11 +110,7 @@ export const AuthForm = ({ password: data.password, }); if (result?.message === "Success") { - posthog.identify(data.email.toLowerCase()); - posthog.capture("user_login", { - email: data.email.toLocaleLowerCase(), - timestamp: Date.now(), - }); + trackUserLogin({ email: data.email }); router.push("/"); } else if (result?.errors && "credentials" in result.errors) { form.setError("email", { @@ -136,19 +132,10 @@ export const AuthForm = ({ const newUser = await createNewUser(data); if (!newUser.errors) { - let firstName = ""; - let lastName = ""; - if (data.name) { - const nameParts = data.name.trim().split(" "); - firstName = nameParts[0] || ""; - lastName = nameParts.slice(1).join(" ") || ""; - } - posthog.capture("user_registered", { - email: data.email.toLocaleLowerCase(), - firstname: firstName, - lastname: lastName, - company: data.company || "", - timestamp: Date.now(), + trackUserRegistration({ + email: data.email, + fullName: data.name, + company: data.company, }); toast({ title: "Success!", diff --git a/ui/components/providers/workflow/forms/test-connection-form.tsx b/ui/components/providers/workflow/forms/test-connection-form.tsx index 9cd0bb7718..b0d6f8f532 100644 --- a/ui/components/providers/workflow/forms/test-connection-form.tsx +++ b/ui/components/providers/workflow/forms/test-connection-form.tsx @@ -5,7 +5,6 @@ import { Icon } from "@iconify/react"; import { Checkbox } from "@nextui-org/react"; import Link from "next/link"; import { useRouter } from "next/navigation"; -import posthog from "posthog-js"; import { useState } from "react"; import { useForm } from "react-hook-form"; import { z } from "zod"; @@ -18,6 +17,7 @@ import { scanOnDemand, scheduleDaily } from "@/actions/scans"; import { getTask } from "@/actions/task/tasks"; import { CheckIcon, RocketIcon } from "@/components/icons"; import { useToast } from "@/components/ui"; +import { trackCloudConnectionSuccess } from "@/lib/analytics"; import { CustomButton } from "@/components/ui/custom"; import { Form } from "@/components/ui/form"; import { checkTaskStatus } from "@/lib/helper"; @@ -146,12 +146,11 @@ export const TestConnectionForm = ({ }); } else { setIsRedirecting(true); - // Capture PostHog event for successful cloud connection - posthog.capture("cloud_connection_success", { - provider_type: providerType, - provider_alias: providerData.data.attributes.alias, - scan_type: form.watch("runOnce") ? "single" : "scheduled", - timestamp: Date.now(), + // Track cloud connection success event + trackCloudConnectionSuccess({ + providerType: providerType, + providerAlias: providerData.data.attributes.alias, + scanType: form.watch("runOnce") ? "single" : "scheduled", }); router.push("/scans"); } diff --git a/ui/lib/analytics.ts b/ui/lib/analytics.ts new file mode 100644 index 0000000000..30727c5904 --- /dev/null +++ b/ui/lib/analytics.ts @@ -0,0 +1,144 @@ +import posthog from "posthog-js"; + +// Type definitions for tracking payloads +export interface UserLoginPayload { + email: string; +} + +export interface UserRegistrationPayload { + email: string; + firstName?: string; + lastName?: string; + company?: string; + fullName?: string; +} + +export interface CloudConnectionPayload { + providerType: string; + providerAlias: string; + scanType: "single" | "scheduled"; +} + +// Initialize a new PostHog session +export const initializeSession = (): void => { + try { + posthog.reset(true); + } catch (error) { + console.error("Failed to initialize PostHog session:", error); + } +}; + +// Identify a user in PostHog +export const identifyUser = (email: string): void => { + try { + posthog.identify(email.toLowerCase()); + } catch (error) { + console.error("Failed to identify user in PostHog:", error); + } +}; + +// Track user login event +export const trackUserLogin = ({ email }: UserLoginPayload): void => { + try { + const normalizedEmail = email.toLowerCase(); + identifyUser(normalizedEmail); + posthog.capture("userLogin", { + email: normalizedEmail, + timestamp: Date.now(), + }); + } catch (error) { + console.error("Failed to track user login:", error); + } +}; + +// Track user registration event +export const trackUserRegistration = ({ + email, + firstName = "", + lastName = "", + company = "", + fullName = "", +}: UserRegistrationPayload): void => { + try { + // Parse name if fullName is provided + let first = firstName; + let last = lastName; + + if (fullName && (!firstName || !lastName)) { + const nameParts = fullName.trim().split(" "); + first = nameParts[0] || ""; + last = nameParts.slice(1).join(" ") || ""; + } + + posthog.capture("userRegistered", { + email: email.toLowerCase(), + firstName: first, + lastName: last, + company: company, + timestamp: Date.now(), + }); + } catch (error) { + console.error("Failed to track user registration:", error); + } +}; + +// Track cloud connection success +export const trackCloudConnectionSuccess = ({ + providerType, + providerAlias, + scanType, +}: CloudConnectionPayload): void => { + try { + posthog.capture("cloudConnectionSuccess", { + providerType: providerType, + providerAlias: providerAlias, + scanType: scanType, + timestamp: Date.now(), + }); + } catch (error) { + console.error("Failed to track cloud connection success:", error); + } +}; + +// Generic event tracking function for custom events +export const trackEvent = (eventName: string, properties?: Record): void => { + try { + posthog.capture(eventName, { + ...properties, + timestamp: Date.now(), + }); + } catch (error) { + console.error(`Failed to track event "${eventName}":`, error); + } +}; + +// Track page view +export const trackPageView = (pageName: string, properties?: Record): void => { + try { + posthog.capture("$pageview", { + $current_url: window.location.href, + pageName, + ...properties, + }); + } catch (error) { + console.error("Failed to track page view:", error); + } +}; + +// Set user properties +export const setUserProperties = (properties: Record): void => { + try { + posthog.people.set(properties); + } catch (error) { + console.error("Failed to set user properties:", error); + } +}; + +// Check if PostHog is initialized and ready +export const isAnalyticsReady = (): boolean => { + try { + return typeof posthog !== "undefined" && posthog._isIdentified !== undefined; + } catch { + return false; + } +}; diff --git a/ui/lib/posthog.ts b/ui/lib/posthog.ts index 057cdcb5a3..dc270a6144 100644 --- a/ui/lib/posthog.ts +++ b/ui/lib/posthog.ts @@ -1,11 +1,13 @@ import { PostHog } from "posthog-node"; // NOTE: This is a Node.js client, so you can use it for sending events from the server side to PostHog. -export default function PostHogClient() { +const PostHogClient = () => { const posthogClient = new PostHog(process.env.NEXT_PUBLIC_POSTHOG_KEY!, { host: process.env.NEXT_PUBLIC_POSTHOG_HOST, flushAt: 1, flushInterval: 0, }); return posthogClient; -} +}; + +export default PostHogClient;