-
+
No artifacts yet
diff --git a/surfsense_web/hooks/use-run-stream.ts b/surfsense_web/hooks/use-run-stream.ts
index 5d737e059..0ee5c0267 100644
--- a/surfsense_web/hooks/use-run-stream.ts
+++ b/surfsense_web/hooks/use-run-stream.ts
@@ -4,7 +4,6 @@ import { useQueryClient } from "@tanstack/react-query";
import { useCallback, useEffect, useRef, useState } from "react";
import type { ScraperRunDetail, ScraperRunEvent } from "@/contracts/types/scraper.types";
import { scrapersApiService } from "@/lib/apis/scrapers-api.service";
-import { trackWeeklyUser } from "@/lib/posthog/events";
import { cacheKeys } from "@/lib/query-client/cache-keys";
export type RunStatus = "idle" | "running" | "success" | "error" | "cancelled";
@@ -110,7 +109,8 @@ export function useRunStream(workspaceId: number) {
try {
const started = await scrapersApiService.runAsync(workspaceId, platform, verb, payload);
runIdRef.current = started.run_id;
- trackWeeklyUser("api_run", workspaceId);
+ // weekly_users removed — WAU is derived server-side from
+ // scraper_run_completed / chat_turn_completed.
setState((s) => ({ ...s, runId: started.run_id }));
void consume(started.run_id, controller.signal);
} catch (e) {
diff --git a/surfsense_web/lib/apis/base-api.service.ts b/surfsense_web/lib/apis/base-api.service.ts
index 867dea93e..8e5daa00a 100644
--- a/surfsense_web/lib/apis/base-api.service.ts
+++ b/surfsense_web/lib/apis/base-api.service.ts
@@ -40,6 +40,30 @@ function blockRefreshRetry(key: string): void {
refreshRetryBlockedUntil.set(key, Date.now() + REFRESH_RETRY_BLOCK_MS);
}
+/**
+ * Send an API failure to PostHog error tracking. Scoped by the caller to only
+ * 5xx server faults + network outages — 4xx responses are expected behavior.
+ * Lazy-imports posthog-js so an ad-blocker can never break the request path.
+ */
+function captureApiException(error: unknown, url: string, method?: RequestOptions["method"]): void {
+ import("posthog-js")
+ .then(({ default: posthog }) => {
+ posthog.captureException(error, {
+ api_url: url,
+ api_method: method ?? "GET",
+ ...(error instanceof AppError && {
+ status_code: error.status,
+ status_text: error.statusText,
+ error_code: error.code,
+ request_id: error.requestId,
+ }),
+ });
+ })
+ .catch(() => {
+ console.error("Failed to capture exception in PostHog");
+ });
+}
+
export type RequestOptions = {
method: "GET" | "POST" | "PUT" | "PATCH" | "DELETE";
headers?: Record;
@@ -288,9 +312,12 @@ class BaseApiService {
throw new AbortedError();
}
if (error instanceof TypeError && !(error instanceof AppError)) {
- throw new NetworkError(
+ const networkError = new NetworkError(
"Unable to connect to the server. Check your internet connection and try again."
);
+ // Network failures are genuine outages worth tracking.
+ captureApiException(networkError, url, options?.method);
+ throw networkError;
}
// Handled client errors (validation, credits, auth, not-found, ...) are
@@ -305,23 +332,10 @@ class BaseApiService {
if (!isHandledClientError) {
console.error("Request failed:", JSON.stringify(error));
- if (!(error instanceof AuthenticationError)) {
- import("posthog-js")
- .then(({ default: posthog }) => {
- posthog.captureException(error, {
- api_url: url,
- api_method: options?.method ?? "GET",
- ...(error instanceof AppError && {
- status_code: error.status,
- status_text: error.statusText,
- error_code: error.code,
- request_id: error.requestId,
- }),
- });
- })
- .catch(() => {
- console.error("Failed to capture exception in PostHog");
- });
+ // Only 5xx server faults are unexpected; 4xx are handled above and
+ // network outages were already captured before this point.
+ if (error instanceof AppError && error.status >= 500) {
+ captureApiException(error, url, options?.method);
}
}
throw error;
diff --git a/surfsense_web/lib/chat/stream-engine/engine.ts b/surfsense_web/lib/chat/stream-engine/engine.ts
index 57ff47f61..34536efc1 100644
--- a/surfsense_web/lib/chat/stream-engine/engine.ts
+++ b/surfsense_web/lib/chat/stream-engine/engine.ts
@@ -58,7 +58,6 @@ import {
import { buildBackendUrl } from "@/lib/env-config";
import {
trackChatBlocked,
- trackChatCreated,
trackChatErrorDetailed,
trackChatMessageSent,
trackChatResponseReceived,
@@ -384,7 +383,8 @@ export async function startNewChat(ctx: EngineContext, message: AppendMessage):
queryClient.setQueryData(cacheKeys.threads.detail(newThread.id), newThread);
queryClient.setQueryData(cacheKeys.threads.messages(newThread.id), { messages: [] });
- trackChatCreated(workspaceId, currentThreadId);
+ // chat_created is now emitted server-side (new_chat_routes.create_thread)
+ // so PAT/MCP-created threads are also counted.
isNewThread = true;
// Update URL silently using browser API (not router.replace) to avoid
diff --git a/surfsense_web/lib/posthog/events.ts b/surfsense_web/lib/posthog/events.ts
index 33d808278..173fd3469 100644
--- a/surfsense_web/lib/posthog/events.ts
+++ b/surfsense_web/lib/posthog/events.ts
@@ -3,24 +3,20 @@ import type { ChatErrorKind, ChatErrorSeverity, ChatFlow } from "@/lib/chat/chat
import { getConnectorTelemetryMeta } from "@/lib/connector-telemetry";
/**
- * PostHog Analytics Event Definitions
+ * PostHog Analytics Event Definitions (frontend)
*
* 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
+ * SCOPE: this file now holds only *intent* and client-perceived *UX* events.
+ * Authoritative *outcome* events (resource creation, task/ingestion/indexing
+ * completion, auth success, billing) are emitted server-side in
+ * surfsense_backend/app/observability/analytics.py — they are reliable
+ * regardless of ad-blockers, tab-close, or non-browser (MCP/PAT/OAuth)
+ * clients. Do NOT re-add optimistic outcome captures here; they double-count.
*
- * Categories:
- * - auth: Authentication events
- * - workspace: Search space management
- * - document: Document management
- * - chat: Chat and messaging (authenticated + anonymous)
- * - connector: External connector events (all lifecycle stages)
- * - contact: Contact form events
- * - settings: Settings changes
- * - automation: Automation lifecycle (create/update/delete/trigger/chat)
- * - marketing: Marketing/referral tracking
+ * Events follow a consistent naming convention: category_action
*/
function safeCapture(event: string, properties?: Record) {
@@ -43,17 +39,13 @@ function compact(obj: T): Record {
}
// ============================================
-// AUTH EVENTS
+// AUTH EVENTS (attempts + failures only; successes are server-side)
// ============================================
export function trackLoginAttempt(method: "local" | "google") {
safeCapture("auth_login_attempt", { method });
}
-export function trackLoginSuccess(method: "local" | "google") {
- safeCapture("auth_login_success", { method });
-}
-
export function trackLoginFailure(method: "local" | "google", error?: string) {
safeCapture("auth_login_failure", { method, error });
}
@@ -62,10 +54,6 @@ export function trackRegistrationAttempt() {
safeCapture("auth_registration_attempt");
}
-export function trackRegistrationSuccess() {
- safeCapture("auth_registration_success");
-}
-
export function trackRegistrationFailure(error?: string) {
safeCapture("auth_registration_failure", { error });
}
@@ -75,56 +63,9 @@ export function trackLogout() {
}
// ============================================
-// SEARCH SPACE EVENTS
+// CHAT EVENTS (client-perceived UX)
// ============================================
-export function trackWorkspaceCreated(workspaceId: number, name: string) {
- safeCapture("workspace_created", {
- workspace_id: workspaceId,
- name,
- });
-}
-
-export function trackWorkspaceDeleted(workspaceId: number) {
- safeCapture("workspace_deleted", {
- workspace_id: workspaceId,
- });
-}
-
-export function trackWorkspaceViewed(workspaceId: number) {
- safeCapture("workspace_viewed", {
- workspace_id: workspaceId,
- });
-}
-
-// ============================================
-// ACTIVE-USER (WAU) EVENT
-// ============================================
-
-/**
- * Single signal for active-user counting. Fired whenever a user sends a
- * chat message or starts an API run, so a "weekly unique users on
- * weekly_users" insight in PostHog is our WAU number.
- *
- * ponytail: frontend-only capture — API runs made directly against the
- * backend (PAT/curl, no browser) are not counted. Upgrade path is a
- * server-side capture in the backend if that ever matters.
- */
-export function trackWeeklyUser(source: "chat_message" | "api_run", workspaceId?: number) {
- safeCapture("weekly_users", compact({ source, workspace_id: workspaceId }));
-}
-
-// ============================================
-// CHAT EVENTS
-// ============================================
-
-export function trackChatCreated(workspaceId: number, chatId: number) {
- safeCapture("chat_created", {
- workspace_id: workspaceId,
- chat_id: chatId,
- });
-}
-
export function trackChatMessageSent(
workspaceId: number,
chatId: number,
@@ -141,7 +82,6 @@ export function trackChatMessageSent(
has_mentioned_documents: options?.hasMentionedDocuments ?? false,
message_length: options?.messageLength,
});
- trackWeeklyUser("chat_message", workspaceId);
}
export function trackChatResponseReceived(workspaceId: number, chatId: number) {
@@ -213,6 +153,10 @@ export function trackChatErrorDetailed(
* flow. This is intentionally a separate event from `chat_message_sent`
* so WAU / retention queries on the authenticated event stay clean while
* still giving us visibility into top-of-funnel usage on /free/*.
+ *
+ * Kept frontend-side despite the backend's `anon_chat_turn_completed`: the
+ * frontend anon distinct id is what merges into the person at signup,
+ * powering the anonymous-to-registered conversion funnel.
*/
export function trackAnonymousChatMessageSent(options: {
modelSlug: string;
@@ -229,7 +173,7 @@ export function trackAnonymousChatMessageSent(options: {
}
// ============================================
-// DOCUMENT EVENTS
+// DOCUMENT EVENTS (intent only; ingestion outcome is server-side)
// ============================================
export function trackDocumentUploadStarted(
@@ -244,59 +188,20 @@ export function trackDocumentUploadStarted(
});
}
-export function trackDocumentUploadSuccess(workspaceId: number, fileCount: number) {
- safeCapture("document_upload_success", {
- workspace_id: workspaceId,
- file_count: fileCount,
- });
-}
-
-export function trackDocumentUploadFailure(workspaceId: number, error?: string) {
- safeCapture("document_upload_failure", {
- workspace_id: workspaceId,
- error,
- });
-}
-
-export function trackDocumentDeleted(workspaceId: number, documentId: number) {
- safeCapture("document_deleted", {
- workspace_id: workspaceId,
- document_id: documentId,
- });
-}
-
-export function trackDocumentBulkDeleted(workspaceId: number, count: number) {
- safeCapture("document_bulk_deleted", {
- workspace_id: workspaceId,
- count,
- });
-}
-
-export function trackYouTubeImport(workspaceId: number, url: string) {
- safeCapture("youtube_import_started", {
- workspace_id: workspaceId,
- url,
- });
-}
-
// ============================================
-// CONNECTOR EVENTS (generic lifecycle dispatcher)
+// CONNECTOR EVENTS (setup intent/UX; connected/deleted are server-side)
// ============================================
//
// All connector events go through `trackConnectorEvent`. The connector's
-// human-readable title and its group (oauth/composio/crawler/other) are
-// auto-attached from the shared registry in `connector-constants.ts`, so
-// adding a new connector to that list is the only change required for it
-// to show up correctly in PostHog dashboards.
+// group (oauth/composio/crawler/other) is auto-attached from the shared
+// registry, so adding a new connector to that list is the only change
+// required for it to show up correctly in PostHog dashboards.
export type ConnectorEventStage =
| "setup_started"
| "setup_success"
| "setup_failure"
- | "oauth_initiated"
- | "connected"
- | "deleted"
- | "synced";
+ | "oauth_initiated";
export interface ConnectorEventOptions {
workspaceId?: number | null;
@@ -312,6 +217,9 @@ export interface ConnectorEventOptions {
/**
* Generic connector lifecycle tracker. Every connector analytics event
* should funnel through here so the enrichment stays consistent.
+ *
+ * ``connector_title`` is intentionally NOT sent — it's a display label with
+ * no aggregation value; segment on ``connector_type`` / ``connector_group``.
*/
export function trackConnectorEvent(
stage: ConnectorEventStage,
@@ -327,7 +235,6 @@ export function trackConnectorEvent(
error: options.error,
}),
connector_type: meta.connector_type,
- connector_title: meta.connector_title,
connector_group: meta.connector_group,
is_oauth: meta.is_oauth,
...(options.extra ?? {}),
@@ -365,59 +272,10 @@ export function trackConnectorSetupFailure(
});
}
-export function trackConnectorDeleted(
- workspaceId: number,
- connectorType: string,
- connectorId: number
-) {
- trackConnectorEvent("deleted", connectorType, { workspaceId, connectorId });
-}
-
-export function trackConnectorSynced(
- workspaceId: number,
- connectorType: string,
- connectorId: number
-) {
- trackConnectorEvent("synced", connectorType, { workspaceId, connectorId });
-}
-
-// ============================================
-// SETTINGS EVENTS
-// ============================================
-
-export function trackSettingsViewed(workspaceId: number, section: string) {
- safeCapture("settings_viewed", {
- workspace_id: workspaceId,
- section,
- });
-}
-
-export function trackSettingsUpdated(workspaceId: number, section: string, setting: string) {
- safeCapture("settings_updated", {
- workspace_id: workspaceId,
- section,
- setting,
- });
-}
-
// ============================================
// FEATURE USAGE EVENTS
// ============================================
-export function trackPodcastGenerated(workspaceId: number, chatId: number) {
- safeCapture("podcast_generated", {
- workspace_id: workspaceId,
- chat_id: chatId,
- });
-}
-
-export function trackSourcesTabViewed(workspaceId: number, tab: string) {
- safeCapture("sources_tab_viewed", {
- workspace_id: workspaceId,
- tab,
- });
-}
-
export function trackDesktopDownloadClicked(options: {
os: string;
placement: "sidebar_collapsed" | "sidebar_expanded";
@@ -429,84 +287,7 @@ export function trackDesktopDownloadClicked(options: {
}
// ============================================
-// SEARCH SPACE INVITE EVENTS
-// ============================================
-
-export function trackWorkspaceInviteSent(
- workspaceId: number,
- options?: {
- roleName?: string;
- hasExpiry?: boolean;
- hasMaxUses?: boolean;
- }
-) {
- safeCapture("workspace_invite_sent", {
- workspace_id: workspaceId,
- role_name: options?.roleName,
- has_expiry: options?.hasExpiry ?? false,
- has_max_uses: options?.hasMaxUses ?? false,
- });
-}
-
-export function trackWorkspaceInviteAccepted(
- workspaceId: number,
- workspaceName: string,
- roleName?: string | null
-) {
- safeCapture("workspace_invite_accepted", {
- workspace_id: workspaceId,
- workspace_name: workspaceName,
- role_name: roleName,
- });
-}
-
-export function trackWorkspaceInviteDeclined(workspaceName?: string) {
- safeCapture("workspace_invite_declined", {
- workspace_name: workspaceName,
- });
-}
-
-export function trackWorkspaceUserAdded(
- workspaceId: number,
- workspaceName: string,
- roleName?: string | null
-) {
- safeCapture("workspace_user_added", {
- workspace_id: workspaceId,
- workspace_name: workspaceName,
- role_name: roleName,
- });
-}
-
-export function trackWorkspaceUsersViewed(
- workspaceId: number,
- userCount: number,
- ownerCount: number
-) {
- safeCapture("workspace_users_viewed", {
- workspace_id: workspaceId,
- user_count: userCount,
- owner_count: ownerCount,
- });
-}
-
-// ============================================
-// CONNECTOR CONNECTION EVENTS
-// ============================================
-
-export function trackConnectorConnected(
- workspaceId: number,
- connectorType: string,
- connectorId?: number
-) {
- trackConnectorEvent("connected", connectorType, {
- workspaceId,
- connectorId: connectorId ?? undefined,
- });
-}
-
-// ============================================
-// INDEXING EVENTS
+// INDEXING EVENTS (intent/UX; indexing outcome is server-side)
// ============================================
export function trackIndexWithDateRangeOpened(
@@ -578,20 +359,19 @@ export function trackPeriodicIndexingStarted(
}
// ============================================
-// INCENTIVE TASKS EVENTS
+// SEARCH SPACE INVITE EVENTS (decline is client-only; sent/accepted server-side)
// ============================================
-export function trackIncentivePageViewed() {
- safeCapture("incentive_page_viewed");
-}
-
-export function trackIncentiveTaskCompleted(taskType: string, creditMicrosRewarded: number) {
- safeCapture("incentive_task_completed", {
- task_type: taskType,
- credit_micros_rewarded: creditMicrosRewarded,
+export function trackWorkspaceInviteDeclined(workspaceName?: string) {
+ safeCapture("workspace_invite_declined", {
+ workspace_name: workspaceName,
});
}
+// ============================================
+// INCENTIVE TASKS EVENTS (click intent only; completion is server-side)
+// ============================================
+
export function trackIncentiveTaskClicked(taskType: string) {
safeCapture("incentive_task_clicked", {
task_type: taskType,
@@ -616,83 +396,25 @@ export function trackReferralLanding(refCode: string, landingUrl: string) {
}
// ============================================
-// AUTOMATION EVENTS
+// AUTOMATION EVENTS (failures + chat-builder UX; CRUD outcomes are server-side)
// ============================================
-interface AutomationCreatedProps {
- workspace_id: number;
- automation_id: number;
- task_count?: number;
- trigger_type?: string;
- has_schedule?: boolean;
- chat_model_id?: number;
- image_gen_model_id?: number;
- vision_model_id?: number;
- tags_count?: number;
-}
-
-export function trackAutomationCreated(props: AutomationCreatedProps) {
- safeCapture("automation_created", compact(props));
-}
-
export function trackAutomationCreateFailed(props: { workspace_id?: number; error?: string }) {
safeCapture("automation_create_failed", compact(props));
}
-export function trackAutomationUpdated(props: {
- automation_id: number;
- workspace_id?: number;
- has_definition_change?: boolean;
- has_name_change?: boolean;
- has_description_change?: boolean;
- task_count?: number;
-}) {
- safeCapture("automation_updated", compact(props));
-}
-
-export function trackAutomationStatusChanged(props: {
- automation_id: number;
- workspace_id?: number;
- next_status: string;
-}) {
- safeCapture("automation_status_changed", compact(props));
-}
-
export function trackAutomationUpdateFailed(props: { automation_id: number; error?: string }) {
safeCapture("automation_update_failed", compact(props));
}
-export function trackAutomationDeleted(props: { automation_id: number; workspace_id?: number }) {
- safeCapture("automation_deleted", compact(props));
-}
-
export function trackAutomationDeleteFailed(props: { automation_id: number; error?: string }) {
safeCapture("automation_delete_failed", compact(props));
}
-export function trackAutomationTriggerAdded(props: {
- automation_id: number;
- trigger_id?: number;
- trigger_type?: string;
- enabled?: boolean;
- has_cron?: boolean;
-}) {
- safeCapture("automation_trigger_added", compact(props));
-}
-
export function trackAutomationTriggerAddFailed(props: { automation_id: number; error?: string }) {
safeCapture("automation_trigger_add_failed", compact(props));
}
-export function trackAutomationTriggerUpdated(props: {
- automation_id: number;
- trigger_id: number;
- change?: "enabled" | "params" | "other";
- enabled?: boolean;
-}) {
- safeCapture("automation_trigger_updated", compact(props));
-}
-
export function trackAutomationTriggerUpdateFailed(props: {
automation_id: number;
trigger_id: number;
@@ -701,13 +423,6 @@ export function trackAutomationTriggerUpdateFailed(props: {
safeCapture("automation_trigger_update_failed", compact(props));
}
-export function trackAutomationTriggerRemoved(props: {
- automation_id: number;
- trigger_id: number;
-}) {
- safeCapture("automation_trigger_removed", compact(props));
-}
-
export function trackAutomationTriggerRemoveFailed(props: {
automation_id: number;
trigger_id: number;
diff --git a/surfsense_web/messages/en.json b/surfsense_web/messages/en.json
index 50b92b1f7..c9b73a09a 100644
--- a/surfsense_web/messages/en.json
+++ b/surfsense_web/messages/en.json
@@ -664,7 +664,6 @@
"chat_deleted": "Chat deleted successfully",
"error_deleting_chat": "Failed to delete chat",
"delete": "Delete",
- "try_different_search": "Try a different search term",
"updated": "Updated",
"more_options": "More options",
"clear_search": "Clear search",
diff --git a/surfsense_web/messages/es.json b/surfsense_web/messages/es.json
index ada68e841..84696e86e 100644
--- a/surfsense_web/messages/es.json
+++ b/surfsense_web/messages/es.json
@@ -664,7 +664,6 @@
"chat_deleted": "Chat eliminado correctamente",
"error_deleting_chat": "Error al eliminar el chat",
"delete": "Eliminar",
- "try_different_search": "Intenta con un término de búsqueda diferente",
"updated": "Actualizado",
"more_options": "Más opciones",
"clear_search": "Limpiar búsqueda",
diff --git a/surfsense_web/messages/hi.json b/surfsense_web/messages/hi.json
index 3b59c58dd..361284c2b 100644
--- a/surfsense_web/messages/hi.json
+++ b/surfsense_web/messages/hi.json
@@ -664,7 +664,6 @@
"chat_deleted": "चैट सफलतापूर्वक हटाया गया",
"error_deleting_chat": "चैट हटाने में विफल",
"delete": "हटाएं",
- "try_different_search": "कोई अलग खोज शब्द आज़माएं",
"updated": "अपडेट किया गया",
"more_options": "और विकल्प",
"clear_search": "खोज साफ करें",
diff --git a/surfsense_web/messages/ko.json b/surfsense_web/messages/ko.json
index 3b5d88852..5dd65166d 100644
--- a/surfsense_web/messages/ko.json
+++ b/surfsense_web/messages/ko.json
@@ -664,7 +664,6 @@
"chat_deleted": "채팅이 성공적으로 삭제되었습니다",
"error_deleting_chat": "채팅 삭제에 실패했습니다",
"delete": "삭제",
- "try_different_search": "다른 검색어를 시도해보세요",
"updated": "업데이트됨",
"more_options": "추가 옵션",
"clear_search": "검색 지우기",
diff --git a/surfsense_web/messages/pt.json b/surfsense_web/messages/pt.json
index 30fd09792..5cd735180 100644
--- a/surfsense_web/messages/pt.json
+++ b/surfsense_web/messages/pt.json
@@ -664,7 +664,6 @@
"chat_deleted": "Chat excluído com sucesso",
"error_deleting_chat": "Falha ao excluir chat",
"delete": "Excluir",
- "try_different_search": "Tente um termo de pesquisa diferente",
"updated": "Atualizado",
"more_options": "Mais opções",
"clear_search": "Limpar pesquisa",
diff --git a/surfsense_web/messages/zh.json b/surfsense_web/messages/zh.json
index 0a3cd6905..669e16e2b 100644
--- a/surfsense_web/messages/zh.json
+++ b/surfsense_web/messages/zh.json
@@ -663,7 +663,6 @@
"chat_deleted": "对话删除成功",
"error_deleting_chat": "删除对话失败",
"delete": "删除",
- "try_different_search": "尝试其他搜索词",
"updated": "更新时间",
"more_options": "更多选项",
"clear_search": "清除搜索",