feat: added posthog

This commit is contained in:
DESKTOP-RTLN3BA\$punk 2025-12-25 13:25:13 -08:00
parent 80e4f1b798
commit c96be7d9e1
18 changed files with 506 additions and 19 deletions

View file

@ -0,0 +1,220 @@
/**
* PostHog Analytics Utility
* Provides typed event tracking functions for SurfSense
*/
import posthog from "posthog-js";
// Check if PostHog is initialized
function isPostHogReady(): boolean {
return typeof window !== "undefined" && !!process.env.NEXT_PUBLIC_POSTHOG_KEY;
}
// =============================================================================
// Event Names (Constants for consistency)
// =============================================================================
export const ANALYTICS_EVENTS = {
// Authentication
USER_SIGNED_UP: "user_signed_up",
USER_LOGGED_IN: "user_logged_in",
USER_LOGGED_OUT: "user_logged_out",
// Search Spaces
SEARCH_SPACE_CREATED: "search_space_created",
SEARCH_SPACE_DELETED: "search_space_deleted",
// Chat
CHAT_CREATED: "chat_created",
MESSAGE_SENT: "message_sent",
CHAT_DELETED: "chat_deleted",
// Documents
DOCUMENT_INDEXED: "document_indexed",
DOCUMENT_DELETED: "document_deleted",
// Connectors
CONNECTOR_ADDED: "connector_added",
CONNECTOR_DELETED: "connector_deleted",
CONNECTOR_INDEXED: "connector_indexed",
// Podcasts
PODCAST_GENERATED: "podcast_generated",
PODCAST_DELETED: "podcast_deleted",
// LLM Config
LLM_CONFIG_CREATED: "llm_config_created",
// Invites
INVITE_CREATED: "invite_created",
INVITE_ACCEPTED: "invite_accepted",
} as const;
// =============================================================================
// Authentication Events
// =============================================================================
export function trackUserSignedUp(properties?: { method?: "email" | "google" }) {
if (!isPostHogReady()) return;
posthog.capture(ANALYTICS_EVENTS.USER_SIGNED_UP, properties);
}
export function trackUserLoggedIn(properties?: { method?: "email" | "google" }) {
if (!isPostHogReady()) return;
posthog.capture(ANALYTICS_EVENTS.USER_LOGGED_IN, properties);
}
export function trackUserLoggedOut() {
if (!isPostHogReady()) return;
posthog.capture(ANALYTICS_EVENTS.USER_LOGGED_OUT);
}
// =============================================================================
// Search Space Events
// =============================================================================
export function trackSearchSpaceCreated(properties: { search_space_id: number; name: string }) {
if (!isPostHogReady()) return;
posthog.capture(ANALYTICS_EVENTS.SEARCH_SPACE_CREATED, properties);
}
export function trackSearchSpaceDeleted(properties: { search_space_id: number }) {
if (!isPostHogReady()) return;
posthog.capture(ANALYTICS_EVENTS.SEARCH_SPACE_DELETED, properties);
}
// =============================================================================
// Chat Events
// =============================================================================
export function trackChatCreated(properties: { search_space_id: number; thread_id: number }) {
if (!isPostHogReady()) return;
posthog.capture(ANALYTICS_EVENTS.CHAT_CREATED, properties);
}
export function trackMessageSent(properties: {
search_space_id: number;
thread_id: number;
role: "user" | "assistant" | "system";
}) {
if (!isPostHogReady()) return;
posthog.capture(ANALYTICS_EVENTS.MESSAGE_SENT, properties);
}
export function trackChatDeleted(properties: { search_space_id: number; thread_id: number }) {
if (!isPostHogReady()) return;
posthog.capture(ANALYTICS_EVENTS.CHAT_DELETED, properties);
}
// =============================================================================
// Document Events
// =============================================================================
export function trackDocumentIndexed(properties: {
search_space_id: number;
document_type: string;
count?: number;
}) {
if (!isPostHogReady()) return;
posthog.capture(ANALYTICS_EVENTS.DOCUMENT_INDEXED, {
...properties,
count: properties.count ?? 1,
});
}
export function trackDocumentDeleted(properties: { search_space_id: number; document_id: number }) {
if (!isPostHogReady()) return;
posthog.capture(ANALYTICS_EVENTS.DOCUMENT_DELETED, properties);
}
// =============================================================================
// Connector Events
// =============================================================================
export function trackConnectorAdded(properties: {
search_space_id: number;
connector_type: string;
connector_id?: number;
}) {
if (!isPostHogReady()) return;
posthog.capture(ANALYTICS_EVENTS.CONNECTOR_ADDED, properties);
}
export function trackConnectorDeleted(properties: {
search_space_id: number;
connector_type: string;
connector_id: number;
}) {
if (!isPostHogReady()) return;
posthog.capture(ANALYTICS_EVENTS.CONNECTOR_DELETED, properties);
}
export function trackConnectorIndexed(properties: {
search_space_id: number;
connector_type: string;
connector_id: number;
}) {
if (!isPostHogReady()) return;
posthog.capture(ANALYTICS_EVENTS.CONNECTOR_INDEXED, properties);
}
// =============================================================================
// Podcast Events
// =============================================================================
export function trackPodcastGenerated(properties: { search_space_id: number; podcast_id: number }) {
if (!isPostHogReady()) return;
posthog.capture(ANALYTICS_EVENTS.PODCAST_GENERATED, properties);
}
export function trackPodcastDeleted(properties: { search_space_id: number; podcast_id: number }) {
if (!isPostHogReady()) return;
posthog.capture(ANALYTICS_EVENTS.PODCAST_DELETED, properties);
}
// =============================================================================
// LLM Config Events
// =============================================================================
export function trackLLMConfigCreated(properties: {
search_space_id: number;
provider: string;
model_name: string;
}) {
if (!isPostHogReady()) return;
posthog.capture(ANALYTICS_EVENTS.LLM_CONFIG_CREATED, properties);
}
// =============================================================================
// Invite Events
// =============================================================================
export function trackInviteCreated(properties: { search_space_id: number; role_name?: string }) {
if (!isPostHogReady()) return;
posthog.capture(ANALYTICS_EVENTS.INVITE_CREATED, properties);
}
export function trackInviteAccepted(properties: { search_space_id: number; invite_code: string }) {
if (!isPostHogReady()) return;
posthog.capture(ANALYTICS_EVENTS.INVITE_ACCEPTED, properties);
}
// =============================================================================
// Utility Functions
// =============================================================================
/**
* Generic event capture for custom events
*/
export function trackEvent(eventName: string, properties?: Record<string, unknown>) {
if (!isPostHogReady()) return;
posthog.capture(eventName, properties);
}
/**
* Reset PostHog user identification (call on logout)
*/
export function resetAnalytics() {
if (!isPostHogReady()) return;
posthog.reset();
}

View file

@ -3,6 +3,7 @@
* Provides API functions and thread list management.
*/
import { trackChatCreated, trackChatDeleted, trackMessageSent } from "@/lib/analytics";
import { baseApiService } from "@/lib/apis/base-api.service";
// =============================================================================
@ -80,13 +81,18 @@ export async function createThread(
searchSpaceId: number,
title = "New Chat"
): Promise<ThreadRecord> {
return baseApiService.post<ThreadRecord>("/api/v1/threads", undefined, {
const thread = await baseApiService.post<ThreadRecord>("/api/v1/threads", undefined, {
body: {
title,
archived: false,
search_space_id: searchSpaceId,
},
});
// Track chat creation event
trackChatCreated({ search_space_id: searchSpaceId, thread_id: thread.id });
return thread;
}
/**
@ -101,11 +107,27 @@ export async function getThreadMessages(threadId: number): Promise<ThreadHistory
*/
export async function appendMessage(
threadId: number,
message: { role: "user" | "assistant" | "system"; content: unknown }
message: { role: "user" | "assistant" | "system"; content: unknown },
searchSpaceId?: number
): Promise<MessageRecord> {
return baseApiService.post<MessageRecord>(`/api/v1/threads/${threadId}/messages`, undefined, {
body: message,
});
const result = await baseApiService.post<MessageRecord>(
`/api/v1/threads/${threadId}/messages`,
undefined,
{
body: message,
}
);
// Track message sent event (only for user messages to avoid double-counting)
if (message.role === "user" && searchSpaceId) {
trackMessageSent({
search_space_id: searchSpaceId,
thread_id: threadId,
role: message.role,
});
}
return result;
}
/**
@ -123,8 +145,13 @@ export async function updateThread(
/**
* Delete a thread
*/
export async function deleteThread(threadId: number): Promise<void> {
export async function deleteThread(threadId: number, searchSpaceId?: number): Promise<void> {
await baseApiService.delete(`/api/v1/threads/${threadId}`);
// Track chat deletion event
if (searchSpaceId) {
trackChatDeleted({ search_space_id: searchSpaceId, thread_id: threadId });
}
}
// =============================================================================
@ -218,7 +245,7 @@ export function createThreadListManager(config: ThreadListAdapterConfig) {
async deleteThread(threadId: number): Promise<boolean> {
try {
await deleteThread(threadId);
await deleteThread(threadId, config.searchSpaceId);
return true;
} catch (error) {
console.error("[ThreadListManager] Failed to delete thread:", error);