feat: centralize PostHog analytics with proper error handling

- Create lib/analytics.ts with centralized tracking functions
- Use arrow functions and camelCase naming per code standards
- Add error handling to prevent analytics failures from crashing app
- Update auth forms and provider workflows to use new analytics lib
- Export TypeScript interfaces for type-safe analytics payloads

Addresses PR review comments:
- Convert to arrow functions (requested by @paabloLC)
- Use camelCase for event and property names
- Centralize analytics logic for better maintainability
This commit is contained in:
Amit Sharma
2025-07-29 19:49:12 -07:00
parent 255df2dbc1
commit bc34c9df4a
4 changed files with 161 additions and 29 deletions
+7 -20
View File
@@ -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<typeof formSchema>) => {
//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!",
@@ -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");
}
+144
View File
@@ -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<string, any>): 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<string, any>): 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<string, any>): 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;
}
};
+4 -2
View File
@@ -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;