feat: Enhance PostHog integration with referral tracking and error handling

- Added referral code tracking in PostHog initialization to capture initial and last seen referral codes.
- Wrapped PostHog capture calls in a safeCapture function to prevent app breakage due to ad-blockers.
- Introduced PostHogReferral component in PostHogProvider for improved referral tracking.
This commit is contained in:
DESKTOP-RTLN3BA\$punk 2026-03-11 02:47:46 -07:00
parent 88201d0209
commit 5e28125090
4 changed files with 156 additions and 80 deletions

View file

@ -5,17 +5,17 @@ import posthog from "posthog-js";
import type { ReactNode } from "react"; import type { ReactNode } from "react";
import "../../instrumentation-client"; import "../../instrumentation-client";
import { PostHogIdentify } from "./PostHogIdentify"; import { PostHogIdentify } from "./PostHogIdentify";
import { PostHogReferral } from "./PostHogReferral";
interface PostHogProviderProps { interface PostHogProviderProps {
children: ReactNode; children: ReactNode;
} }
export function PostHogProvider({ children }: PostHogProviderProps) { export function PostHogProvider({ children }: PostHogProviderProps) {
// posthog-js is initialized by importing instrumentation-client.ts above
// We wrap the app with the PostHogProvider for hook access
return ( return (
<PHProvider client={posthog}> <PHProvider client={posthog}>
<PostHogIdentify /> <PostHogIdentify />
<PostHogReferral />
{children} {children}
</PHProvider> </PHProvider>
); );

View file

@ -0,0 +1,34 @@
"use client";
import { useEffect } from "react";
import { trackReferralLanding } from "@/lib/posthog/events";
const REF_STORAGE_KEY = "surfsense_ref_code";
/**
* Captures the ?ref=<code> URL parameter on first landing and fires a
* PostHog event so marketing campaigns can be attributed.
*
* The ref code is persisted to sessionStorage so it survives client-side
* navigations that strip query params (e.g. login redirect), but a fresh
* event is fired for each new browser session with a ref param.
*/
export function PostHogReferral() {
useEffect(() => {
if (typeof window === "undefined") return;
const params = new URLSearchParams(window.location.search);
const ref = params.get("ref");
if (ref) {
try {
sessionStorage.setItem(REF_STORAGE_KEY, ref);
} catch {
// Private browsing may block sessionStorage
}
trackReferralLanding(ref, window.location.href);
}
}, []);
return null;
}

View file

@ -1,29 +1,47 @@
import posthog from "posthog-js"; import posthog from "posthog-js";
function initPostHog() { function initPostHog() {
if (!process.env.NEXT_PUBLIC_POSTHOG_KEY) return; try {
if (!process.env.NEXT_PUBLIC_POSTHOG_KEY) return;
posthog.init(process.env.NEXT_PUBLIC_POSTHOG_KEY, { posthog.init(process.env.NEXT_PUBLIC_POSTHOG_KEY, {
api_host: "/ingest", api_host: "/ingest",
ui_host: "https://us.posthog.com", ui_host: "https://us.posthog.com",
defaults: "2025-11-30", defaults: "2025-11-30",
capture_pageview: "history_change", capture_pageview: "history_change",
capture_pageleave: true, capture_pageleave: true,
before_send: (event) => { before_send: (event) => {
if (event.properties) { if (event.properties) {
event.properties.$set = { const params = new URLSearchParams(window.location.search);
...event.properties.$set, const ref = params.get("ref");
last_seen_at: new Date().toISOString(), if (ref) {
}; event.properties.ref_code = ref;
} event.properties.$set = {
return event; ...event.properties.$set,
}, initial_ref_code: ref,
loaded: (ph) => { };
if (typeof window !== "undefined") { event.properties.$set_once = {
window.posthog = ph; ...event.properties.$set_once,
} first_ref_code: ref,
}, };
}); }
event.properties.$set = {
...event.properties.$set,
last_seen_at: new Date().toISOString(),
};
}
return event;
},
loaded: (ph) => {
if (typeof window !== "undefined") {
window.posthog = ph;
}
},
});
} catch {
// PostHog init failed (likely ad-blocker) app must continue to work
}
} }
if (typeof window !== "undefined") { if (typeof window !== "undefined") {

View file

@ -3,7 +3,10 @@ import posthog from "posthog-js";
/** /**
* PostHog Analytics Event Definitions * PostHog Analytics Event Definitions
* *
* This file defines all custom analytics events tracked in SurfSense. * All capture/identify/reset calls are wrapped in try-catch so that
* ad-blockers that interfere with posthog-js can never break app
* functionality (e.g. the chat flow).
*
* Events follow a consistent naming convention: category_action * Events follow a consistent naming convention: category_action
* *
* Categories: * Categories:
@ -14,47 +17,47 @@ import posthog from "posthog-js";
* - connector: External connector events * - connector: External connector events
* - contact: Contact form events * - contact: Contact form events
* - settings: Settings changes * - settings: Settings changes
* - marketing: Marketing/referral tracking
*/ */
function safeCapture(event: string, properties?: Record<string, unknown>) {
try {
posthog.capture(event, properties);
} catch {
// Silently ignore analytics should never break the app
}
}
// ============================================ // ============================================
// AUTH EVENTS // AUTH EVENTS
// ============================================ // ============================================
export function trackLoginAttempt(method: "local" | "google") { export function trackLoginAttempt(method: "local" | "google") {
posthog.capture("auth_login_attempt", { safeCapture("auth_login_attempt", { method });
method,
});
} }
export function trackLoginSuccess(method: "local" | "google") { export function trackLoginSuccess(method: "local" | "google") {
posthog.capture("auth_login_success", { safeCapture("auth_login_success", { method });
method,
});
} }
export function trackLoginFailure(method: "local" | "google", error?: string) { export function trackLoginFailure(method: "local" | "google", error?: string) {
posthog.capture("auth_login_failure", { safeCapture("auth_login_failure", { method, error });
method,
error,
});
} }
export function trackRegistrationAttempt() { export function trackRegistrationAttempt() {
posthog.capture("auth_registration_attempt"); safeCapture("auth_registration_attempt");
} }
export function trackRegistrationSuccess() { export function trackRegistrationSuccess() {
posthog.capture("auth_registration_success"); safeCapture("auth_registration_success");
} }
export function trackRegistrationFailure(error?: string) { export function trackRegistrationFailure(error?: string) {
posthog.capture("auth_registration_failure", { safeCapture("auth_registration_failure", { error });
error,
});
} }
export function trackLogout() { export function trackLogout() {
posthog.capture("auth_logout"); safeCapture("auth_logout");
} }
// ============================================ // ============================================
@ -62,20 +65,20 @@ export function trackLogout() {
// ============================================ // ============================================
export function trackSearchSpaceCreated(searchSpaceId: number, name: string) { export function trackSearchSpaceCreated(searchSpaceId: number, name: string) {
posthog.capture("search_space_created", { safeCapture("search_space_created", {
search_space_id: searchSpaceId, search_space_id: searchSpaceId,
name, name,
}); });
} }
export function trackSearchSpaceDeleted(searchSpaceId: number) { export function trackSearchSpaceDeleted(searchSpaceId: number) {
posthog.capture("search_space_deleted", { safeCapture("search_space_deleted", {
search_space_id: searchSpaceId, search_space_id: searchSpaceId,
}); });
} }
export function trackSearchSpaceViewed(searchSpaceId: number) { export function trackSearchSpaceViewed(searchSpaceId: number) {
posthog.capture("search_space_viewed", { safeCapture("search_space_viewed", {
search_space_id: searchSpaceId, search_space_id: searchSpaceId,
}); });
} }
@ -85,7 +88,7 @@ export function trackSearchSpaceViewed(searchSpaceId: number) {
// ============================================ // ============================================
export function trackChatCreated(searchSpaceId: number, chatId: number) { export function trackChatCreated(searchSpaceId: number, chatId: number) {
posthog.capture("chat_created", { safeCapture("chat_created", {
search_space_id: searchSpaceId, search_space_id: searchSpaceId,
chat_id: chatId, chat_id: chatId,
}); });
@ -100,7 +103,7 @@ export function trackChatMessageSent(
messageLength?: number; messageLength?: number;
} }
) { ) {
posthog.capture("chat_message_sent", { safeCapture("chat_message_sent", {
search_space_id: searchSpaceId, search_space_id: searchSpaceId,
chat_id: chatId, chat_id: chatId,
has_attachments: options?.hasAttachments ?? false, has_attachments: options?.hasAttachments ?? false,
@ -110,14 +113,14 @@ export function trackChatMessageSent(
} }
export function trackChatResponseReceived(searchSpaceId: number, chatId: number) { export function trackChatResponseReceived(searchSpaceId: number, chatId: number) {
posthog.capture("chat_response_received", { safeCapture("chat_response_received", {
search_space_id: searchSpaceId, search_space_id: searchSpaceId,
chat_id: chatId, chat_id: chatId,
}); });
} }
export function trackChatError(searchSpaceId: number, chatId: number, error?: string) { export function trackChatError(searchSpaceId: number, chatId: number, error?: string) {
posthog.capture("chat_error", { safeCapture("chat_error", {
search_space_id: searchSpaceId, search_space_id: searchSpaceId,
chat_id: chatId, chat_id: chatId,
error, error,
@ -133,7 +136,7 @@ export function trackDocumentUploadStarted(
fileCount: number, fileCount: number,
totalSizeBytes: number totalSizeBytes: number
) { ) {
posthog.capture("document_upload_started", { safeCapture("document_upload_started", {
search_space_id: searchSpaceId, search_space_id: searchSpaceId,
file_count: fileCount, file_count: fileCount,
total_size_bytes: totalSizeBytes, total_size_bytes: totalSizeBytes,
@ -141,35 +144,35 @@ export function trackDocumentUploadStarted(
} }
export function trackDocumentUploadSuccess(searchSpaceId: number, fileCount: number) { export function trackDocumentUploadSuccess(searchSpaceId: number, fileCount: number) {
posthog.capture("document_upload_success", { safeCapture("document_upload_success", {
search_space_id: searchSpaceId, search_space_id: searchSpaceId,
file_count: fileCount, file_count: fileCount,
}); });
} }
export function trackDocumentUploadFailure(searchSpaceId: number, error?: string) { export function trackDocumentUploadFailure(searchSpaceId: number, error?: string) {
posthog.capture("document_upload_failure", { safeCapture("document_upload_failure", {
search_space_id: searchSpaceId, search_space_id: searchSpaceId,
error, error,
}); });
} }
export function trackDocumentDeleted(searchSpaceId: number, documentId: number) { export function trackDocumentDeleted(searchSpaceId: number, documentId: number) {
posthog.capture("document_deleted", { safeCapture("document_deleted", {
search_space_id: searchSpaceId, search_space_id: searchSpaceId,
document_id: documentId, document_id: documentId,
}); });
} }
export function trackDocumentBulkDeleted(searchSpaceId: number, count: number) { export function trackDocumentBulkDeleted(searchSpaceId: number, count: number) {
posthog.capture("document_bulk_deleted", { safeCapture("document_bulk_deleted", {
search_space_id: searchSpaceId, search_space_id: searchSpaceId,
count, count,
}); });
} }
export function trackYouTubeImport(searchSpaceId: number, url: string) { export function trackYouTubeImport(searchSpaceId: number, url: string) {
posthog.capture("youtube_import_started", { safeCapture("youtube_import_started", {
search_space_id: searchSpaceId, search_space_id: searchSpaceId,
url, url,
}); });
@ -180,7 +183,7 @@ export function trackYouTubeImport(searchSpaceId: number, url: string) {
// ============================================ // ============================================
export function trackConnectorSetupStarted(searchSpaceId: number, connectorType: string) { export function trackConnectorSetupStarted(searchSpaceId: number, connectorType: string) {
posthog.capture("connector_setup_started", { safeCapture("connector_setup_started", {
search_space_id: searchSpaceId, search_space_id: searchSpaceId,
connector_type: connectorType, connector_type: connectorType,
}); });
@ -191,7 +194,7 @@ export function trackConnectorSetupSuccess(
connectorType: string, connectorType: string,
connectorId: number connectorId: number
) { ) {
posthog.capture("connector_setup_success", { safeCapture("connector_setup_success", {
search_space_id: searchSpaceId, search_space_id: searchSpaceId,
connector_type: connectorType, connector_type: connectorType,
connector_id: connectorId, connector_id: connectorId,
@ -203,7 +206,7 @@ export function trackConnectorSetupFailure(
connectorType: string, connectorType: string,
error?: string error?: string
) { ) {
posthog.capture("connector_setup_failure", { safeCapture("connector_setup_failure", {
search_space_id: searchSpaceId, search_space_id: searchSpaceId,
connector_type: connectorType, connector_type: connectorType,
error, error,
@ -215,7 +218,7 @@ export function trackConnectorDeleted(
connectorType: string, connectorType: string,
connectorId: number connectorId: number
) { ) {
posthog.capture("connector_deleted", { safeCapture("connector_deleted", {
search_space_id: searchSpaceId, search_space_id: searchSpaceId,
connector_type: connectorType, connector_type: connectorType,
connector_id: connectorId, connector_id: connectorId,
@ -227,7 +230,7 @@ export function trackConnectorSynced(
connectorType: string, connectorType: string,
connectorId: number connectorId: number
) { ) {
posthog.capture("connector_synced", { safeCapture("connector_synced", {
search_space_id: searchSpaceId, search_space_id: searchSpaceId,
connector_type: connectorType, connector_type: connectorType,
connector_id: connectorId, connector_id: connectorId,
@ -239,14 +242,14 @@ export function trackConnectorSynced(
// ============================================ // ============================================
export function trackSettingsViewed(searchSpaceId: number, section: string) { export function trackSettingsViewed(searchSpaceId: number, section: string) {
posthog.capture("settings_viewed", { safeCapture("settings_viewed", {
search_space_id: searchSpaceId, search_space_id: searchSpaceId,
section, section,
}); });
} }
export function trackSettingsUpdated(searchSpaceId: number, section: string, setting: string) { export function trackSettingsUpdated(searchSpaceId: number, section: string, setting: string) {
posthog.capture("settings_updated", { safeCapture("settings_updated", {
search_space_id: searchSpaceId, search_space_id: searchSpaceId,
section, section,
setting, setting,
@ -258,14 +261,14 @@ export function trackSettingsUpdated(searchSpaceId: number, section: string, set
// ============================================ // ============================================
export function trackPodcastGenerated(searchSpaceId: number, chatId: number) { export function trackPodcastGenerated(searchSpaceId: number, chatId: number) {
posthog.capture("podcast_generated", { safeCapture("podcast_generated", {
search_space_id: searchSpaceId, search_space_id: searchSpaceId,
chat_id: chatId, chat_id: chatId,
}); });
} }
export function trackSourcesTabViewed(searchSpaceId: number, tab: string) { export function trackSourcesTabViewed(searchSpaceId: number, tab: string) {
posthog.capture("sources_tab_viewed", { safeCapture("sources_tab_viewed", {
search_space_id: searchSpaceId, search_space_id: searchSpaceId,
tab, tab,
}); });
@ -283,7 +286,7 @@ export function trackSearchSpaceInviteSent(
hasMaxUses?: boolean; hasMaxUses?: boolean;
} }
) { ) {
posthog.capture("search_space_invite_sent", { safeCapture("search_space_invite_sent", {
search_space_id: searchSpaceId, search_space_id: searchSpaceId,
role_name: options?.roleName, role_name: options?.roleName,
has_expiry: options?.hasExpiry ?? false, has_expiry: options?.hasExpiry ?? false,
@ -296,7 +299,7 @@ export function trackSearchSpaceInviteAccepted(
searchSpaceName: string, searchSpaceName: string,
roleName?: string | null roleName?: string | null
) { ) {
posthog.capture("search_space_invite_accepted", { safeCapture("search_space_invite_accepted", {
search_space_id: searchSpaceId, search_space_id: searchSpaceId,
search_space_name: searchSpaceName, search_space_name: searchSpaceName,
role_name: roleName, role_name: roleName,
@ -304,7 +307,7 @@ export function trackSearchSpaceInviteAccepted(
} }
export function trackSearchSpaceInviteDeclined(searchSpaceName?: string) { export function trackSearchSpaceInviteDeclined(searchSpaceName?: string) {
posthog.capture("search_space_invite_declined", { safeCapture("search_space_invite_declined", {
search_space_name: searchSpaceName, search_space_name: searchSpaceName,
}); });
} }
@ -314,7 +317,7 @@ export function trackSearchSpaceUserAdded(
searchSpaceName: string, searchSpaceName: string,
roleName?: string | null roleName?: string | null
) { ) {
posthog.capture("search_space_user_added", { safeCapture("search_space_user_added", {
search_space_id: searchSpaceId, search_space_id: searchSpaceId,
search_space_name: searchSpaceName, search_space_name: searchSpaceName,
role_name: roleName, role_name: roleName,
@ -326,7 +329,7 @@ export function trackSearchSpaceUsersViewed(
userCount: number, userCount: number,
ownerCount: number ownerCount: number
) { ) {
posthog.capture("search_space_users_viewed", { safeCapture("search_space_users_viewed", {
search_space_id: searchSpaceId, search_space_id: searchSpaceId,
user_count: userCount, user_count: userCount,
owner_count: ownerCount, owner_count: ownerCount,
@ -342,7 +345,7 @@ export function trackConnectorConnected(
connectorType: string, connectorType: string,
connectorId?: number connectorId?: number
) { ) {
posthog.capture("connector_connected", { safeCapture("connector_connected", {
search_space_id: searchSpaceId, search_space_id: searchSpaceId,
connector_type: connectorType, connector_type: connectorType,
connector_id: connectorId, connector_id: connectorId,
@ -358,7 +361,7 @@ export function trackIndexWithDateRangeOpened(
connectorType: string, connectorType: string,
connectorId: number connectorId: number
) { ) {
posthog.capture("index_with_date_range_opened", { safeCapture("index_with_date_range_opened", {
search_space_id: searchSpaceId, search_space_id: searchSpaceId,
connector_type: connectorType, connector_type: connectorType,
connector_id: connectorId, connector_id: connectorId,
@ -374,7 +377,7 @@ export function trackIndexWithDateRangeStarted(
hasEndDate?: boolean; hasEndDate?: boolean;
} }
) { ) {
posthog.capture("index_with_date_range_started", { safeCapture("index_with_date_range_started", {
search_space_id: searchSpaceId, search_space_id: searchSpaceId,
connector_type: connectorType, connector_type: connectorType,
connector_id: connectorId, connector_id: connectorId,
@ -388,7 +391,7 @@ export function trackQuickIndexClicked(
connectorType: string, connectorType: string,
connectorId: number connectorId: number
) { ) {
posthog.capture("quick_index_clicked", { safeCapture("quick_index_clicked", {
search_space_id: searchSpaceId, search_space_id: searchSpaceId,
connector_type: connectorType, connector_type: connectorType,
connector_id: connectorId, connector_id: connectorId,
@ -400,7 +403,7 @@ export function trackConfigurePeriodicIndexingOpened(
connectorType: string, connectorType: string,
connectorId: number connectorId: number
) { ) {
posthog.capture("configure_periodic_indexing_opened", { safeCapture("configure_periodic_indexing_opened", {
search_space_id: searchSpaceId, search_space_id: searchSpaceId,
connector_type: connectorType, connector_type: connectorType,
connector_id: connectorId, connector_id: connectorId,
@ -413,7 +416,7 @@ export function trackPeriodicIndexingStarted(
connectorId: number, connectorId: number,
frequencyMinutes: number frequencyMinutes: number
) { ) {
posthog.capture("periodic_indexing_started", { safeCapture("periodic_indexing_started", {
search_space_id: searchSpaceId, search_space_id: searchSpaceId,
connector_type: connectorType, connector_type: connectorType,
connector_id: connectorId, connector_id: connectorId,
@ -426,24 +429,37 @@ export function trackPeriodicIndexingStarted(
// ============================================ // ============================================
export function trackIncentivePageViewed() { export function trackIncentivePageViewed() {
posthog.capture("incentive_page_viewed"); safeCapture("incentive_page_viewed");
} }
export function trackIncentiveTaskCompleted(taskType: string, pagesRewarded: number) { export function trackIncentiveTaskCompleted(taskType: string, pagesRewarded: number) {
posthog.capture("incentive_task_completed", { safeCapture("incentive_task_completed", {
task_type: taskType, task_type: taskType,
pages_rewarded: pagesRewarded, pages_rewarded: pagesRewarded,
}); });
} }
export function trackIncentiveTaskClicked(taskType: string) { export function trackIncentiveTaskClicked(taskType: string) {
posthog.capture("incentive_task_clicked", { safeCapture("incentive_task_clicked", {
task_type: taskType, task_type: taskType,
}); });
} }
export function trackIncentiveContactOpened() { export function trackIncentiveContactOpened() {
posthog.capture("incentive_contact_opened"); safeCapture("incentive_contact_opened");
}
// ============================================
// MARKETING / REFERRAL EVENTS
// ============================================
export function trackReferralLanding(refCode: string, landingUrl: string) {
safeCapture("marketing_referral_landing", {
ref_code: refCode,
landing_url: landingUrl,
$set_once: { first_ref_code: refCode },
$set: { latest_ref_code: refCode },
});
} }
// ============================================ // ============================================
@ -455,12 +471,20 @@ export function trackIncentiveContactOpened() {
* Call this after successful authentication * Call this after successful authentication
*/ */
export function identifyUser(userId: string, properties?: Record<string, unknown>) { export function identifyUser(userId: string, properties?: Record<string, unknown>) {
posthog.identify(userId, properties); try {
posthog.identify(userId, properties);
} catch {
// Silently ignore ad-blockers may break posthog
}
} }
/** /**
* Reset user identity (call on logout) * Reset user identity (call on logout)
*/ */
export function resetUser() { export function resetUser() {
posthog.reset(); try {
posthog.reset();
} catch {
// Silently ignore ad-blockers may break posthog
}
} }