feat: updated sitemap, added more posthog events & added new changelog

- Implemented a function to return the current hour for lastModified timestamps in the sitemap.
- Added multiple new URLs to the sitemap, including documentation and connector pages.
- Integrated tracking for search space invite events, including sent, accepted, declined, and user added events.
- Enhanced connector management with tracking for connection, deletion, and indexing events.
This commit is contained in:
DESKTOP-RTLN3BA\$punk 2026-01-08 13:10:48 -08:00
parent 2fd38615e8
commit 789197d41c
8 changed files with 524 additions and 89 deletions

View file

@ -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<Invite>;
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 {

View file

@ -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() {
<Button
variant="outline"
className="flex-1"
onClick={() => router.push("/dashboard")}
onClick={handleDecline}
>
Cancel
</Button>

View file

@ -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,
},
];
}

View file

@ -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"
---
<img src="/changelog/0.0.11/header.gif" alt="SurfSense v0.0.11 - Connectors And More Connectors" className="rounded-lg w-full" />
## 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.
<Accordion type="multiple" className="w-full not-prose">
<AccordionItem value="item-1">
<AccordionTrigger>Bug Fixes</AccordionTrigger>
<AccordionContent className="flex flex-col gap-4 text-balance">
<ul className="list-disc space-y-2 pl-4">
<li>Fixed a login issue affecting specific Google accounts on surfsense.com</li>
<li>Resolved most Docker self-hosting configuration issues for easier deployment</li>
</ul>
</AccordionContent>
</AccordionItem>
<AccordionItem value="item-2">
<AccordionTrigger>For Self-Hosters</AccordionTrigger>
<AccordionContent className="flex flex-col gap-4 text-balance">
<ul className="list-disc space-y-2 pl-4">
<li>Docker configuration has been streamlined for smoother self-hosted deployments</li>
<li>OAuth setup is now consistent across all connectors</li>
</ul>
</AccordionContent>
</AccordionItem>
</Accordion>
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!

View file

@ -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}

View file

@ -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,

View file

@ -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
// ============================================

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 MiB