From 5e28125090aa54e769ca67c6bce7692f0dbaf5fb Mon Sep 17 00:00:00 2001 From: "DESKTOP-RTLN3BA\\$punk" Date: Wed, 11 Mar 2026 02:47:46 -0700 Subject: [PATCH] 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. --- .../components/providers/PostHogProvider.tsx | 4 +- .../components/providers/PostHogReferral.tsx | 34 +++++ surfsense_web/instrumentation-client.ts | 62 +++++--- surfsense_web/lib/posthog/events.ts | 136 ++++++++++-------- 4 files changed, 156 insertions(+), 80 deletions(-) create mode 100644 surfsense_web/components/providers/PostHogReferral.tsx diff --git a/surfsense_web/components/providers/PostHogProvider.tsx b/surfsense_web/components/providers/PostHogProvider.tsx index 1216730f3..906d552c2 100644 --- a/surfsense_web/components/providers/PostHogProvider.tsx +++ b/surfsense_web/components/providers/PostHogProvider.tsx @@ -5,17 +5,17 @@ import posthog from "posthog-js"; import type { ReactNode } from "react"; import "../../instrumentation-client"; import { PostHogIdentify } from "./PostHogIdentify"; +import { PostHogReferral } from "./PostHogReferral"; interface PostHogProviderProps { children: ReactNode; } 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 ( + {children} ); diff --git a/surfsense_web/components/providers/PostHogReferral.tsx b/surfsense_web/components/providers/PostHogReferral.tsx new file mode 100644 index 000000000..8db27ac95 --- /dev/null +++ b/surfsense_web/components/providers/PostHogReferral.tsx @@ -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= 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; +} diff --git a/surfsense_web/instrumentation-client.ts b/surfsense_web/instrumentation-client.ts index c6c6320b0..2c5e0a2c2 100644 --- a/surfsense_web/instrumentation-client.ts +++ b/surfsense_web/instrumentation-client.ts @@ -1,29 +1,47 @@ import posthog from "posthog-js"; 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, { - api_host: "/ingest", - ui_host: "https://us.posthog.com", - defaults: "2025-11-30", - capture_pageview: "history_change", - capture_pageleave: true, - before_send: (event) => { - if (event.properties) { - event.properties.$set = { - ...event.properties.$set, - last_seen_at: new Date().toISOString(), - }; - } - return event; - }, - loaded: (ph) => { - if (typeof window !== "undefined") { - window.posthog = ph; - } - }, - }); + posthog.init(process.env.NEXT_PUBLIC_POSTHOG_KEY, { + api_host: "/ingest", + ui_host: "https://us.posthog.com", + defaults: "2025-11-30", + capture_pageview: "history_change", + capture_pageleave: true, + before_send: (event) => { + if (event.properties) { + const params = new URLSearchParams(window.location.search); + const ref = params.get("ref"); + if (ref) { + event.properties.ref_code = ref; + event.properties.$set = { + ...event.properties.$set, + initial_ref_code: ref, + }; + event.properties.$set_once = { + ...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") { diff --git a/surfsense_web/lib/posthog/events.ts b/surfsense_web/lib/posthog/events.ts index aa1d1e8c5..53aaa71b9 100644 --- a/surfsense_web/lib/posthog/events.ts +++ b/surfsense_web/lib/posthog/events.ts @@ -3,7 +3,10 @@ import posthog from "posthog-js"; /** * 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 * * Categories: @@ -14,47 +17,47 @@ import posthog from "posthog-js"; * - connector: External connector events * - contact: Contact form events * - settings: Settings changes + * - marketing: Marketing/referral tracking */ +function safeCapture(event: string, properties?: Record) { + try { + posthog.capture(event, properties); + } catch { + // Silently ignore – analytics should never break the app + } +} + // ============================================ // AUTH EVENTS // ============================================ export function trackLoginAttempt(method: "local" | "google") { - posthog.capture("auth_login_attempt", { - method, - }); + safeCapture("auth_login_attempt", { method }); } export function trackLoginSuccess(method: "local" | "google") { - posthog.capture("auth_login_success", { - method, - }); + safeCapture("auth_login_success", { method }); } export function trackLoginFailure(method: "local" | "google", error?: string) { - posthog.capture("auth_login_failure", { - method, - error, - }); + safeCapture("auth_login_failure", { method, error }); } export function trackRegistrationAttempt() { - posthog.capture("auth_registration_attempt"); + safeCapture("auth_registration_attempt"); } export function trackRegistrationSuccess() { - posthog.capture("auth_registration_success"); + safeCapture("auth_registration_success"); } export function trackRegistrationFailure(error?: string) { - posthog.capture("auth_registration_failure", { - error, - }); + safeCapture("auth_registration_failure", { error }); } export function trackLogout() { - posthog.capture("auth_logout"); + safeCapture("auth_logout"); } // ============================================ @@ -62,20 +65,20 @@ export function trackLogout() { // ============================================ export function trackSearchSpaceCreated(searchSpaceId: number, name: string) { - posthog.capture("search_space_created", { + safeCapture("search_space_created", { search_space_id: searchSpaceId, name, }); } export function trackSearchSpaceDeleted(searchSpaceId: number) { - posthog.capture("search_space_deleted", { + safeCapture("search_space_deleted", { search_space_id: searchSpaceId, }); } export function trackSearchSpaceViewed(searchSpaceId: number) { - posthog.capture("search_space_viewed", { + safeCapture("search_space_viewed", { search_space_id: searchSpaceId, }); } @@ -85,7 +88,7 @@ export function trackSearchSpaceViewed(searchSpaceId: number) { // ============================================ export function trackChatCreated(searchSpaceId: number, chatId: number) { - posthog.capture("chat_created", { + safeCapture("chat_created", { search_space_id: searchSpaceId, chat_id: chatId, }); @@ -100,7 +103,7 @@ export function trackChatMessageSent( messageLength?: number; } ) { - posthog.capture("chat_message_sent", { + safeCapture("chat_message_sent", { search_space_id: searchSpaceId, chat_id: chatId, has_attachments: options?.hasAttachments ?? false, @@ -110,14 +113,14 @@ export function trackChatMessageSent( } export function trackChatResponseReceived(searchSpaceId: number, chatId: number) { - posthog.capture("chat_response_received", { + safeCapture("chat_response_received", { search_space_id: searchSpaceId, chat_id: chatId, }); } export function trackChatError(searchSpaceId: number, chatId: number, error?: string) { - posthog.capture("chat_error", { + safeCapture("chat_error", { search_space_id: searchSpaceId, chat_id: chatId, error, @@ -133,7 +136,7 @@ export function trackDocumentUploadStarted( fileCount: number, totalSizeBytes: number ) { - posthog.capture("document_upload_started", { + safeCapture("document_upload_started", { search_space_id: searchSpaceId, file_count: fileCount, total_size_bytes: totalSizeBytes, @@ -141,35 +144,35 @@ export function trackDocumentUploadStarted( } export function trackDocumentUploadSuccess(searchSpaceId: number, fileCount: number) { - posthog.capture("document_upload_success", { + safeCapture("document_upload_success", { search_space_id: searchSpaceId, file_count: fileCount, }); } export function trackDocumentUploadFailure(searchSpaceId: number, error?: string) { - posthog.capture("document_upload_failure", { + safeCapture("document_upload_failure", { search_space_id: searchSpaceId, error, }); } export function trackDocumentDeleted(searchSpaceId: number, documentId: number) { - posthog.capture("document_deleted", { + safeCapture("document_deleted", { search_space_id: searchSpaceId, document_id: documentId, }); } export function trackDocumentBulkDeleted(searchSpaceId: number, count: number) { - posthog.capture("document_bulk_deleted", { + safeCapture("document_bulk_deleted", { search_space_id: searchSpaceId, count, }); } export function trackYouTubeImport(searchSpaceId: number, url: string) { - posthog.capture("youtube_import_started", { + safeCapture("youtube_import_started", { search_space_id: searchSpaceId, url, }); @@ -180,7 +183,7 @@ export function trackYouTubeImport(searchSpaceId: number, url: string) { // ============================================ export function trackConnectorSetupStarted(searchSpaceId: number, connectorType: string) { - posthog.capture("connector_setup_started", { + safeCapture("connector_setup_started", { search_space_id: searchSpaceId, connector_type: connectorType, }); @@ -191,7 +194,7 @@ export function trackConnectorSetupSuccess( connectorType: string, connectorId: number ) { - posthog.capture("connector_setup_success", { + safeCapture("connector_setup_success", { search_space_id: searchSpaceId, connector_type: connectorType, connector_id: connectorId, @@ -203,7 +206,7 @@ export function trackConnectorSetupFailure( connectorType: string, error?: string ) { - posthog.capture("connector_setup_failure", { + safeCapture("connector_setup_failure", { search_space_id: searchSpaceId, connector_type: connectorType, error, @@ -215,7 +218,7 @@ export function trackConnectorDeleted( connectorType: string, connectorId: number ) { - posthog.capture("connector_deleted", { + safeCapture("connector_deleted", { search_space_id: searchSpaceId, connector_type: connectorType, connector_id: connectorId, @@ -227,7 +230,7 @@ export function trackConnectorSynced( connectorType: string, connectorId: number ) { - posthog.capture("connector_synced", { + safeCapture("connector_synced", { search_space_id: searchSpaceId, connector_type: connectorType, connector_id: connectorId, @@ -239,14 +242,14 @@ export function trackConnectorSynced( // ============================================ export function trackSettingsViewed(searchSpaceId: number, section: string) { - posthog.capture("settings_viewed", { + safeCapture("settings_viewed", { search_space_id: searchSpaceId, section, }); } export function trackSettingsUpdated(searchSpaceId: number, section: string, setting: string) { - posthog.capture("settings_updated", { + safeCapture("settings_updated", { search_space_id: searchSpaceId, section, setting, @@ -258,14 +261,14 @@ export function trackSettingsUpdated(searchSpaceId: number, section: string, set // ============================================ export function trackPodcastGenerated(searchSpaceId: number, chatId: number) { - posthog.capture("podcast_generated", { + safeCapture("podcast_generated", { search_space_id: searchSpaceId, chat_id: chatId, }); } export function trackSourcesTabViewed(searchSpaceId: number, tab: string) { - posthog.capture("sources_tab_viewed", { + safeCapture("sources_tab_viewed", { search_space_id: searchSpaceId, tab, }); @@ -283,7 +286,7 @@ export function trackSearchSpaceInviteSent( hasMaxUses?: boolean; } ) { - posthog.capture("search_space_invite_sent", { + safeCapture("search_space_invite_sent", { search_space_id: searchSpaceId, role_name: options?.roleName, has_expiry: options?.hasExpiry ?? false, @@ -296,7 +299,7 @@ export function trackSearchSpaceInviteAccepted( searchSpaceName: string, roleName?: string | null ) { - posthog.capture("search_space_invite_accepted", { + safeCapture("search_space_invite_accepted", { search_space_id: searchSpaceId, search_space_name: searchSpaceName, role_name: roleName, @@ -304,7 +307,7 @@ export function trackSearchSpaceInviteAccepted( } export function trackSearchSpaceInviteDeclined(searchSpaceName?: string) { - posthog.capture("search_space_invite_declined", { + safeCapture("search_space_invite_declined", { search_space_name: searchSpaceName, }); } @@ -314,7 +317,7 @@ export function trackSearchSpaceUserAdded( searchSpaceName: string, roleName?: string | null ) { - posthog.capture("search_space_user_added", { + safeCapture("search_space_user_added", { search_space_id: searchSpaceId, search_space_name: searchSpaceName, role_name: roleName, @@ -326,7 +329,7 @@ export function trackSearchSpaceUsersViewed( userCount: number, ownerCount: number ) { - posthog.capture("search_space_users_viewed", { + safeCapture("search_space_users_viewed", { search_space_id: searchSpaceId, user_count: userCount, owner_count: ownerCount, @@ -342,7 +345,7 @@ export function trackConnectorConnected( connectorType: string, connectorId?: number ) { - posthog.capture("connector_connected", { + safeCapture("connector_connected", { search_space_id: searchSpaceId, connector_type: connectorType, connector_id: connectorId, @@ -358,7 +361,7 @@ export function trackIndexWithDateRangeOpened( connectorType: string, connectorId: number ) { - posthog.capture("index_with_date_range_opened", { + safeCapture("index_with_date_range_opened", { search_space_id: searchSpaceId, connector_type: connectorType, connector_id: connectorId, @@ -374,7 +377,7 @@ export function trackIndexWithDateRangeStarted( hasEndDate?: boolean; } ) { - posthog.capture("index_with_date_range_started", { + safeCapture("index_with_date_range_started", { search_space_id: searchSpaceId, connector_type: connectorType, connector_id: connectorId, @@ -388,7 +391,7 @@ export function trackQuickIndexClicked( connectorType: string, connectorId: number ) { - posthog.capture("quick_index_clicked", { + safeCapture("quick_index_clicked", { search_space_id: searchSpaceId, connector_type: connectorType, connector_id: connectorId, @@ -400,7 +403,7 @@ export function trackConfigurePeriodicIndexingOpened( connectorType: string, connectorId: number ) { - posthog.capture("configure_periodic_indexing_opened", { + safeCapture("configure_periodic_indexing_opened", { search_space_id: searchSpaceId, connector_type: connectorType, connector_id: connectorId, @@ -413,7 +416,7 @@ export function trackPeriodicIndexingStarted( connectorId: number, frequencyMinutes: number ) { - posthog.capture("periodic_indexing_started", { + safeCapture("periodic_indexing_started", { search_space_id: searchSpaceId, connector_type: connectorType, connector_id: connectorId, @@ -426,24 +429,37 @@ export function trackPeriodicIndexingStarted( // ============================================ export function trackIncentivePageViewed() { - posthog.capture("incentive_page_viewed"); + safeCapture("incentive_page_viewed"); } export function trackIncentiveTaskCompleted(taskType: string, pagesRewarded: number) { - posthog.capture("incentive_task_completed", { + safeCapture("incentive_task_completed", { task_type: taskType, pages_rewarded: pagesRewarded, }); } export function trackIncentiveTaskClicked(taskType: string) { - posthog.capture("incentive_task_clicked", { + safeCapture("incentive_task_clicked", { task_type: taskType, }); } 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 */ export function identifyUser(userId: string, properties?: Record) { - posthog.identify(userId, properties); + try { + posthog.identify(userId, properties); + } catch { + // Silently ignore – ad-blockers may break posthog + } } /** * Reset user identity (call on logout) */ export function resetUser() { - posthog.reset(); + try { + posthog.reset(); + } catch { + // Silently ignore – ad-blockers may break posthog + } }