diff --git a/surfsense_web/app/dashboard/[search_space_id]/team/page.tsx b/surfsense_web/app/dashboard/[search_space_id]/team/page.tsx index 80f477001..8dd6d839b 100644 --- a/surfsense_web/app/dashboard/[search_space_id]/team/page.tsx +++ b/surfsense_web/app/dashboard/[search_space_id]/team/page.tsx @@ -115,6 +115,7 @@ import type { UpdateRoleRequest, } from "@/contracts/types/roles.types"; import { invitesApiService } from "@/lib/apis/invites-api.service"; +import { trackSearchSpaceInviteSent } from "@/lib/posthog/events"; import { rolesApiService } from "@/lib/apis/roles-api.service"; import { cacheKeys } from "@/lib/query-client/cache-keys"; import { cn } from "@/lib/utils"; @@ -1088,10 +1089,12 @@ function InvitesTab({ function CreateInviteDialog({ roles, onCreateInvite, + searchSpaceId, className, }: { roles: Role[]; onCreateInvite: (data: CreateInviteRequest["data"]) => Promise; + searchSpaceId: number; className?: string; }) { const [open, setOpen] = useState(false); @@ -1114,6 +1117,16 @@ function CreateInviteDialog({ const invite = await onCreateInvite(data); setCreatedInvite(invite); + + // Track invite sent event + const roleName = roleId && roleId !== "default" + ? roles.find((r) => r.id.toString() === roleId)?.name + : undefined; + trackSearchSpaceInviteSent(searchSpaceId, { + roleName, + hasExpiry: !!expiresAt, + hasMaxUses: !!maxUses, + }); } catch (error) { console.error("Failed to create invite:", error); } finally { diff --git a/surfsense_web/app/invite/[invite_code]/page.tsx b/surfsense_web/app/invite/[invite_code]/page.tsx index 30e93c022..8afbb7b4e 100644 --- a/surfsense_web/app/invite/[invite_code]/page.tsx +++ b/surfsense_web/app/invite/[invite_code]/page.tsx @@ -33,6 +33,11 @@ import { import type { AcceptInviteResponse } from "@/contracts/types/invites.types"; import { invitesApiService } from "@/lib/apis/invites-api.service"; import { getBearerToken } from "@/lib/auth-utils"; +import { + trackSearchSpaceInviteAccepted, + trackSearchSpaceInviteDeclined, + trackSearchSpaceUserAdded, +} from "@/lib/posthog/events"; import { cacheKeys } from "@/lib/query-client/cache-keys"; export default function InviteAcceptPage() { @@ -91,6 +96,18 @@ export default function InviteAcceptPage() { if (result) { setAccepted(true); setAcceptedData(result); + + // Track invite accepted and user added events + trackSearchSpaceInviteAccepted( + result.search_space_id, + result.search_space_name, + result.role_name + ); + trackSearchSpaceUserAdded( + result.search_space_id, + result.search_space_name, + result.role_name + ); } } catch (err: any) { setError(err.message || "Failed to accept invite"); @@ -99,6 +116,12 @@ export default function InviteAcceptPage() { } }; + const handleDecline = () => { + // Track invite declined event + trackSearchSpaceInviteDeclined(inviteInfo?.search_space_name); + router.push("/dashboard"); + }; + const handleLoginRedirect = () => { // Store the invite code to redirect back after login localStorage.setItem("pending_invite_code", inviteCode); @@ -327,7 +350,7 @@ export default function InviteAcceptPage() { diff --git a/surfsense_web/app/sitemap.ts b/surfsense_web/app/sitemap.ts index 63e2b5753..4be30eb82 100644 --- a/surfsense_web/app/sitemap.ts +++ b/surfsense_web/app/sitemap.ts @@ -1,60 +1,173 @@ import type { MetadataRoute } from "next"; +// Returns a date rounded to the current hour (updates only once per hour) +function getHourlyDate(): Date { + const now = new Date(); + now.setMinutes(0, 0, 0); + return now; +} + export default function sitemap(): MetadataRoute.Sitemap { + const lastModified = getHourlyDate(); + return [ { url: "https://www.surfsense.com/", - lastModified: new Date(), + lastModified, changeFrequency: "yearly", priority: 1, }, { url: "https://www.surfsense.com/contact", - lastModified: new Date(), + lastModified, changeFrequency: "yearly", priority: 1, }, { url: "https://www.surfsense.com/pricing", - lastModified: new Date(), + lastModified, changeFrequency: "yearly", priority: 0.9, }, { url: "https://www.surfsense.com/privacy", - lastModified: new Date(), + lastModified, changeFrequency: "monthly", priority: 0.9, }, { url: "https://www.surfsense.com/terms", - lastModified: new Date(), + lastModified, changeFrequency: "monthly", priority: 0.9, }, + // Documentation pages { url: "https://www.surfsense.com/docs", - lastModified: new Date(), + lastModified, changeFrequency: "weekly", priority: 0.9, }, { url: "https://www.surfsense.com/docs/installation", - lastModified: new Date(), + lastModified, changeFrequency: "weekly", priority: 0.9, }, { url: "https://www.surfsense.com/docs/docker-installation", - lastModified: new Date(), + lastModified, changeFrequency: "weekly", priority: 0.9, }, { url: "https://www.surfsense.com/docs/manual-installation", - lastModified: new Date(), + lastModified, changeFrequency: "weekly", priority: 0.9, }, + // Connector documentation + { + url: "https://www.surfsense.com/docs/connectors/airtable", + lastModified, + changeFrequency: "weekly", + priority: 0.8, + }, + { + url: "https://www.surfsense.com/docs/connectors/bookstack", + lastModified, + changeFrequency: "weekly", + priority: 0.8, + }, + { + url: "https://www.surfsense.com/docs/connectors/circleback", + lastModified, + changeFrequency: "weekly", + priority: 0.8, + }, + { + url: "https://www.surfsense.com/docs/connectors/clickup", + lastModified, + changeFrequency: "weekly", + priority: 0.8, + }, + { + url: "https://www.surfsense.com/docs/connectors/confluence", + lastModified, + changeFrequency: "weekly", + priority: 0.8, + }, + { + url: "https://www.surfsense.com/docs/connectors/discord", + lastModified, + changeFrequency: "weekly", + priority: 0.8, + }, + { + url: "https://www.surfsense.com/docs/connectors/elasticsearch", + lastModified, + changeFrequency: "weekly", + priority: 0.8, + }, + { + url: "https://www.surfsense.com/docs/connectors/github", + lastModified, + changeFrequency: "weekly", + priority: 0.8, + }, + { + url: "https://www.surfsense.com/docs/connectors/gmail", + lastModified, + changeFrequency: "weekly", + priority: 0.8, + }, + { + url: "https://www.surfsense.com/docs/connectors/google-calendar", + lastModified, + changeFrequency: "weekly", + priority: 0.8, + }, + { + url: "https://www.surfsense.com/docs/connectors/google-drive", + lastModified, + changeFrequency: "weekly", + priority: 0.8, + }, + { + url: "https://www.surfsense.com/docs/connectors/jira", + lastModified, + changeFrequency: "weekly", + priority: 0.8, + }, + { + url: "https://www.surfsense.com/docs/connectors/linear", + lastModified, + changeFrequency: "weekly", + priority: 0.8, + }, + { + url: "https://www.surfsense.com/docs/connectors/luma", + lastModified, + changeFrequency: "weekly", + priority: 0.8, + }, + { + url: "https://www.surfsense.com/docs/connectors/notion", + lastModified, + changeFrequency: "weekly", + priority: 0.8, + }, + { + url: "https://www.surfsense.com/docs/connectors/slack", + lastModified, + changeFrequency: "weekly", + priority: 0.8, + }, + { + url: "https://www.surfsense.com/docs/connectors/web-crawler", + lastModified, + changeFrequency: "weekly", + priority: 0.8, + }, ]; } diff --git a/surfsense_web/changelog/content/2026-01-08.mdx b/surfsense_web/changelog/content/2026-01-08.mdx new file mode 100644 index 000000000..0827e2182 --- /dev/null +++ b/surfsense_web/changelog/content/2026-01-08.mdx @@ -0,0 +1,48 @@ +--- +title: "SurfSense v0.0.11 - Connectors And More Connectors" +description: "SurfSense v0.0.11 delivers powerful new integrations for our AI enterprise search platform, including Google Drive and Circleback connectors, multi-account support, and a fully responsive mobile interface." +date: "2026-01-08" +tags: ["Mobile", "UX", "Integrations", "Connectors"] +version: "0.0.11" +--- + +SurfSense v0.0.11 - Connectors And More Connectors + +## What's New in v0.0.11 + +This release focuses on **connectivity and ease of use** for your enterprise search software. We've begun a comprehensive UX overhaul, streamlining how you connect your data sources, alongside a fully responsive mobile interface that lets you access SurfSense's AI enterprise search capabilities from anywhere. + +### New Features + +- **Mobile-Ready Interface**: A fully responsive UI implementation allows you to search and collaborate seamlessly from your mobile device, bringing enterprise search solutions to your pocket. +- **Streamlined Connector Management**: We've simplified the connector setup and management pages as part of a larger, ongoing UX overhaul for a smoother experience. +- **Google Drive Integration**: Added a dedicated connector for Google Drive, featuring granular file selection to index only what you need for precise enterprise search. +- **Circleback Support**: Introducing a new connector for Circleback to integrate your meeting notes and insights into your unified knowledge base. +- **Simplified OAuth Authentication**: All supported connectors have been migrated to OAuth, making setup faster and more secure across your enterprise search software stack. +- **Multi-Account Support**: Connect multiple accounts for the same service (e.g., Personal and Work Google Drives) to unify all your data sources in one AI-powered search hub. + + + + Bug Fixes + +
    +
  • Fixed a login issue affecting specific Google accounts on surfsense.com
  • +
  • Resolved most Docker self-hosting configuration issues for easier deployment
  • +
+
+
+ + For Self-Hosters + +
    +
  • Docker configuration has been streamlined for smoother self-hosted deployments
  • +
  • OAuth setup is now consistent across all connectors
  • +
+
+
+
+ +SurfSense is an open-source AI enterprise search solution that connects all your knowledge sources, from Google Drive to Slack to meeting notes, in one intelligent, federated search platform. Whether you're looking for enterprise search software for your team or a personal knowledge assistant, SurfSense delivers powerful enterprise search solutions with the flexibility of self-hosting. + +🚀 Connect more, search smarter! + diff --git a/surfsense_web/components/assistant-ui/connector-popup.tsx b/surfsense_web/components/assistant-ui/connector-popup.tsx index 43307db24..80f56df38 100644 --- a/surfsense_web/components/assistant-ui/connector-popup.tsx +++ b/surfsense_web/components/assistant-ui/connector-popup.tsx @@ -248,7 +248,7 @@ export const ConnectorIndicator: FC = () => { onBack={handleBackFromEdit} onQuickIndex={ editingConnector.connector_type !== "GOOGLE_DRIVE_CONNECTOR" - ? () => handleQuickIndexConnector(editingConnector.id) + ? () => handleQuickIndexConnector(editingConnector.id, editingConnector.connector_type) : undefined } onConfigChange={setConnectorConfig} diff --git a/surfsense_web/components/assistant-ui/connector-popup/hooks/use-connector-dialog.ts b/surfsense_web/components/assistant-ui/connector-popup/hooks/use-connector-dialog.ts index 2c8248255..00b108a84 100644 --- a/surfsense_web/components/assistant-ui/connector-popup/hooks/use-connector-dialog.ts +++ b/surfsense_web/components/assistant-ui/connector-popup/hooks/use-connector-dialog.ts @@ -15,6 +15,14 @@ import { EnumConnectorName } from "@/contracts/enums/connector"; import type { SearchSourceConnector } from "@/contracts/types/connector.types"; import { searchSourceConnector } from "@/contracts/types/connector.types"; import { authenticatedFetch } from "@/lib/auth-utils"; +import { + trackConnectorConnected, + trackConnectorDeleted, + trackIndexWithDateRangeOpened, + trackIndexWithDateRangeStarted, + trackPeriodicIndexingStarted, + trackQuickIndexClicked, +} from "@/lib/posthog/events"; import { cacheKeys } from "@/lib/query-client/cache-keys"; import { queryClient } from "@/lib/query-client/client"; import type { IndexingConfigState } from "../constants/connector-constants"; @@ -309,6 +317,13 @@ export const useConnectorDialog = () => { if (newConnector) { const connectorValidation = searchSourceConnector.safeParse(newConnector); if (connectorValidation.success) { + // Track connector connected event for OAuth connectors + trackConnectorConnected( + Number(searchSpaceId), + oauthConnector.connectorType, + newConnector.id + ); + const config = validateIndexingConfigState({ connectorType: oauthConnector.connectorType, connectorId: newConnector.id, @@ -419,6 +434,13 @@ export const useConnectorDialog = () => { if (connector) { const connectorValidation = searchSourceConnector.safeParse(connector); if (connectorValidation.success) { + // Track webcrawler connector connected + trackConnectorConnected( + Number(searchSpaceId), + EnumConnectorName.WEBCRAWLER_CONNECTOR, + connector.id + ); + const config = validateIndexingConfigState({ connectorType: EnumConnectorName.WEBCRAWLER_CONNECTOR, connectorId: connector.id, @@ -514,6 +536,9 @@ export const useConnectorDialog = () => { // Store connectingConnectorType before clearing it const currentConnectorType = connectingConnectorType; + // Track connector connected event for non-OAuth connectors + trackConnectorConnected(Number(searchSpaceId), currentConnectorType, connector.id); + // Find connector title from constants const connectorInfo = OTHER_CONNECTORS.find( (c) => c.connectorType === currentConnectorType @@ -848,46 +873,70 @@ export const useConnectorDialog = () => { }); } - toast.success(`${indexingConfig.connectorTitle} indexing started`, { - description: periodicEnabled - ? `Periodic sync enabled every ${getFrequencyLabel(frequencyMinutes)}.` - : "You can continue working while we sync your data.", - }); + // Track index with date range started event + trackIndexWithDateRangeStarted( + Number(searchSpaceId), + indexingConfig.connectorType, + indexingConfig.connectorId, + { + hasStartDate: !!startDate, + hasEndDate: !!endDate, + } + ); - // Update URL - the effect will handle closing the modal and clearing state - const url = new URL(window.location.href); - url.searchParams.delete("modal"); - url.searchParams.delete("tab"); - url.searchParams.delete("success"); - url.searchParams.delete("connector"); - url.searchParams.delete("view"); - router.replace(url.pathname + url.search, { scroll: false }); - - refreshConnectors(); - queryClient.invalidateQueries({ - queryKey: cacheKeys.logs.summary(Number(searchSpaceId)), - }); - } catch (error) { - console.error("Error starting indexing:", error); - toast.error("Failed to start indexing"); - } finally { - setIsStartingIndexing(false); + // Track periodic indexing started if enabled + if ( + periodicEnabled && + indexingConfig.connectorType !== "GOOGLE_DRIVE_CONNECTOR" + ) { + trackPeriodicIndexingStarted( + Number(searchSpaceId), + indexingConfig.connectorType, + indexingConfig.connectorId, + parseInt(frequencyMinutes, 10) + ); } - }, - [ - indexingConfig, - searchSpaceId, - startDate, - endDate, - indexConnector, - updateConnector, - periodicEnabled, - frequencyMinutes, - getFrequencyLabel, - router, - indexingConnectorConfig, - ] - ); + + toast.success(`${indexingConfig.connectorTitle} indexing started`, { + description: periodicEnabled + ? `Periodic sync enabled every ${getFrequencyLabel(frequencyMinutes)}.` + : "You can continue working while we sync your data.", + }); + + // Update URL - the effect will handle closing the modal and clearing state + const url = new URL(window.location.href); + url.searchParams.delete("modal"); + url.searchParams.delete("tab"); + url.searchParams.delete("success"); + url.searchParams.delete("connector"); + url.searchParams.delete("view"); + router.replace(url.pathname + url.search, { scroll: false }); + + refreshConnectors(); + queryClient.invalidateQueries({ + queryKey: cacheKeys.logs.summary(Number(searchSpaceId)), + }); + } catch (error) { + console.error("Error starting indexing:", error); + toast.error("Failed to start indexing"); + } finally { + setIsStartingIndexing(false); + } + }, + [ + indexingConfig, + searchSpaceId, + startDate, + endDate, + indexConnector, + updateConnector, + periodicEnabled, + frequencyMinutes, + getFrequencyLabel, + router, + indexingConnectorConfig, + ] +); // Handle skipping indexing const handleSkipIndexing = useCallback(() => { @@ -914,6 +963,15 @@ export const useConnectorDialog = () => { return; } + // Track index with date range opened event + if (connector.is_indexable) { + trackIndexWithDateRangeOpened( + Number(searchSpaceId), + connector.connector_type, + connector.id + ); + } + setEditingConnector(connector); setConnectorName(connector.name); // Load existing periodic sync settings (disabled for Google Drive and non-indexable connectors) @@ -1049,46 +1107,76 @@ export const useConnectorDialog = () => { indexingDescription = "Re-indexing started with new date range."; } - toast.success(`${editingConnector.name} updated successfully`, { - description: periodicEnabled - ? `Periodic sync ${frequency ? `enabled every ${getFrequencyLabel(frequencyMinutes)}` : "enabled"}. ${indexingDescription}` - : indexingDescription, - }); - - // Update URL - the effect will handle closing the modal and clearing state - const url = new URL(window.location.href); - url.searchParams.delete("modal"); - url.searchParams.delete("tab"); - url.searchParams.delete("view"); - url.searchParams.delete("connectorId"); - router.replace(url.pathname + url.search, { scroll: false }); - - refreshConnectors(); - queryClient.invalidateQueries({ - queryKey: cacheKeys.logs.summary(Number(searchSpaceId)), - }); - } catch (error) { - console.error("Error saving connector:", error); - toast.error("Failed to save connector changes"); - } finally { - setIsSaving(false); + // Track indexing started if re-indexing was performed + if ( + editingConnector.is_indexable && + (indexingDescription.includes("Re-indexing") || indexingDescription.includes("indexing")) + ) { + trackIndexWithDateRangeStarted( + Number(searchSpaceId), + editingConnector.connector_type, + editingConnector.id, + { + hasStartDate: !!startDateStr, + hasEndDate: !!endDateStr, + } + ); } - }, - [ - editingConnector, - searchSpaceId, - startDate, - endDate, - indexConnector, - updateConnector, - periodicEnabled, - frequencyMinutes, - getFrequencyLabel, - router, - connectorConfig, - connectorName, - ] - ); + + // Track periodic indexing if enabled (for non-Google Drive connectors) + if ( + periodicEnabled && + editingConnector.is_indexable && + editingConnector.connector_type !== "GOOGLE_DRIVE_CONNECTOR" + ) { + trackPeriodicIndexingStarted( + Number(searchSpaceId), + editingConnector.connector_type, + editingConnector.id, + frequency || parseInt(frequencyMinutes, 10) + ); + } + + toast.success(`${editingConnector.name} updated successfully`, { + description: periodicEnabled + ? `Periodic sync ${frequency ? `enabled every ${getFrequencyLabel(frequencyMinutes)}` : "enabled"}. ${indexingDescription}` + : indexingDescription, + }); + + // Update URL - the effect will handle closing the modal and clearing state + const url = new URL(window.location.href); + url.searchParams.delete("modal"); + url.searchParams.delete("tab"); + url.searchParams.delete("view"); + url.searchParams.delete("connectorId"); + router.replace(url.pathname + url.search, { scroll: false }); + + refreshConnectors(); + queryClient.invalidateQueries({ + queryKey: cacheKeys.logs.summary(Number(searchSpaceId)), + }); + } catch (error) { + console.error("Error saving connector:", error); + toast.error("Failed to save connector changes"); + } finally { + setIsSaving(false); + } + }, + [ + editingConnector, + searchSpaceId, + startDate, + endDate, + indexConnector, + updateConnector, + periodicEnabled, + frequencyMinutes, + getFrequencyLabel, + router, + connectorConfig, + connectorName, + ] +); // Handle disconnecting connector const handleDisconnectConnector = useCallback( @@ -1101,6 +1189,13 @@ export const useConnectorDialog = () => { id: editingConnector.id, }); + // Track connector deleted event + trackConnectorDeleted( + Number(searchSpaceId), + editingConnector.connector_type, + editingConnector.id + ); + toast.success(`${editingConnector.name} disconnected successfully`); // Update URL - the effect will handle closing the modal and clearing state @@ -1127,9 +1222,14 @@ export const useConnectorDialog = () => { // Handle quick index (index without date picker, uses backend defaults) const handleQuickIndexConnector = useCallback( - async (connectorId: number) => { + async (connectorId: number, connectorType?: string) => { if (!searchSpaceId) return; + // Track quick index clicked event + if (connectorType) { + trackQuickIndexClicked(Number(searchSpaceId), connectorType, connectorId); + } + try { await indexConnector({ connector_id: connectorId, diff --git a/surfsense_web/lib/posthog/events.ts b/surfsense_web/lib/posthog/events.ts index fae713f80..5f633192b 100644 --- a/surfsense_web/lib/posthog/events.ts +++ b/surfsense_web/lib/posthog/events.ts @@ -271,6 +271,144 @@ export function trackSourcesTabViewed(searchSpaceId: number, tab: string) { }); } +// ============================================ +// SEARCH SPACE INVITE EVENTS +// ============================================ + +export function trackSearchSpaceInviteSent( + searchSpaceId: number, + options?: { + roleName?: string; + hasExpiry?: boolean; + hasMaxUses?: boolean; + } +) { + posthog.capture("search_space_invite_sent", { + search_space_id: searchSpaceId, + role_name: options?.roleName, + has_expiry: options?.hasExpiry ?? false, + has_max_uses: options?.hasMaxUses ?? false, + }); +} + +export function trackSearchSpaceInviteAccepted( + searchSpaceId: number, + searchSpaceName: string, + roleName?: string | null +) { + posthog.capture("search_space_invite_accepted", { + search_space_id: searchSpaceId, + search_space_name: searchSpaceName, + role_name: roleName, + }); +} + +export function trackSearchSpaceInviteDeclined(searchSpaceName?: string) { + posthog.capture("search_space_invite_declined", { + search_space_name: searchSpaceName, + }); +} + +export function trackSearchSpaceUserAdded( + searchSpaceId: number, + searchSpaceName: string, + roleName?: string | null +) { + posthog.capture("search_space_user_added", { + search_space_id: searchSpaceId, + search_space_name: searchSpaceName, + role_name: roleName, + }); +} + +// ============================================ +// CONNECTOR CONNECTION EVENTS +// ============================================ + +export function trackConnectorConnected( + searchSpaceId: number, + connectorType: string, + connectorId?: number +) { + posthog.capture("connector_connected", { + search_space_id: searchSpaceId, + connector_type: connectorType, + connector_id: connectorId, + }); +} + +// ============================================ +// INDEXING EVENTS +// ============================================ + +export function trackIndexWithDateRangeOpened( + searchSpaceId: number, + connectorType: string, + connectorId: number +) { + posthog.capture("index_with_date_range_opened", { + search_space_id: searchSpaceId, + connector_type: connectorType, + connector_id: connectorId, + }); +} + +export function trackIndexWithDateRangeStarted( + searchSpaceId: number, + connectorType: string, + connectorId: number, + options?: { + hasStartDate?: boolean; + hasEndDate?: boolean; + } +) { + posthog.capture("index_with_date_range_started", { + search_space_id: searchSpaceId, + connector_type: connectorType, + connector_id: connectorId, + has_start_date: options?.hasStartDate ?? false, + has_end_date: options?.hasEndDate ?? false, + }); +} + +export function trackQuickIndexClicked( + searchSpaceId: number, + connectorType: string, + connectorId: number +) { + posthog.capture("quick_index_clicked", { + search_space_id: searchSpaceId, + connector_type: connectorType, + connector_id: connectorId, + }); +} + +export function trackConfigurePeriodicIndexingOpened( + searchSpaceId: number, + connectorType: string, + connectorId: number +) { + posthog.capture("configure_periodic_indexing_opened", { + search_space_id: searchSpaceId, + connector_type: connectorType, + connector_id: connectorId, + }); +} + +export function trackPeriodicIndexingStarted( + searchSpaceId: number, + connectorType: string, + connectorId: number, + frequencyMinutes: number +) { + posthog.capture("periodic_indexing_started", { + search_space_id: searchSpaceId, + connector_type: connectorType, + connector_id: connectorId, + frequency_minutes: frequencyMinutes, + }); +} + // ============================================ // USER IDENTIFICATION // ============================================ diff --git a/surfsense_web/public/changelog/0.0.11/header.gif b/surfsense_web/public/changelog/0.0.11/header.gif new file mode 100644 index 000000000..1c22a9242 Binary files /dev/null and b/surfsense_web/public/changelog/0.0.11/header.gif differ