feat(backend): integrate PostHog analytics for enhanced observability

- Added PostHog configuration options to .env.example files for both Docker and Surfsense backend.
- Introduced PostHog dependency in pyproject.toml.
- Implemented analytics middleware to capture various events across the application, including user authentication, automation runs, and API requests.
- Enhanced existing routes and services to emit analytics events, providing insights into user interactions and system performance.
- Ensured graceful shutdown of analytics clients in worker processes and application lifecycles.
This commit is contained in:
DESKTOP-RTLN3BA\$punk 2026-07-22 22:16:28 -07:00
parent ca4f231577
commit dbedf0cfa5
47 changed files with 1618 additions and 513 deletions

View file

@ -13,7 +13,7 @@ import { Spinner } from "@/components/ui/spinner";
import { getAuthErrorDetails, isNetworkError } from "@/lib/auth-errors";
import { getPostLoginRedirectPath } from "@/lib/auth-utils";
import { ValidationError } from "@/lib/error";
import { trackLoginAttempt, trackLoginFailure, trackLoginSuccess } from "@/lib/posthog/events";
import { trackLoginAttempt, trackLoginFailure } from "@/lib/posthog/events";
export function LocalLoginForm() {
const t = useTranslations("auth");
@ -45,8 +45,8 @@ export function LocalLoginForm() {
grant_type: "password",
});
// Track successful login
trackLoginSuccess("local");
// auth_login_success is now emitted server-side
// (UserManager.on_after_login) — authoritative vs. optimistic.
// Small delay to show success message
setTimeout(() => {

View file

@ -15,11 +15,7 @@ import { Spinner } from "@/components/ui/spinner";
import { useSession } from "@/hooks/use-session";
import { getAuthErrorDetails, isNetworkError, shouldRetry } from "@/lib/auth-errors";
import { AppError, ValidationError } from "@/lib/error";
import {
trackRegistrationAttempt,
trackRegistrationFailure,
trackRegistrationSuccess,
} from "@/lib/posthog/events";
import { trackRegistrationAttempt, trackRegistrationFailure } from "@/lib/posthog/events";
import { AmbientBackground } from "../login/AmbientBackground";
export default function RegisterPage() {
@ -81,8 +77,8 @@ export default function RegisterPage() {
is_verified: false,
});
// Track successful registration
trackRegistrationSuccess();
// auth_registration_success is now emitted server-side
// (UserManager.on_after_register) — authoritative vs. optimistic.
// Success toast
toast.success(t("register_success"), {

View file

@ -96,7 +96,6 @@ import type { Role } from "@/contracts/types/roles.types";
import { invitesApiService } from "@/lib/apis/invites-api.service";
import { rolesApiService } from "@/lib/apis/roles-api.service";
import { formatRelativeDate } from "@/lib/format-date";
import { trackWorkspaceInviteSent, trackWorkspaceUsersViewed } from "@/lib/posthog/events";
import { cacheKeys } from "@/lib/query-client/cache-keys";
import { cn } from "@/lib/utils";
@ -226,12 +225,7 @@ export function TeamContent({ workspaceId }: TeamContentProps) {
const canPrev = pageIndex > 0;
const canNext = displayEnd < totalItems;
useEffect(() => {
if (members.length > 0 && !membersLoading) {
const ownerCount = members.filter((m) => m.is_owner).length;
trackWorkspaceUsersViewed(workspaceId, members.length, ownerCount);
}
}, [members, membersLoading, workspaceId]);
// workspace_users_viewed removed — redundant with $pageview.
if (accessLoading || membersLoading) {
return (
@ -342,11 +336,7 @@ export function TeamContent({ workspaceId }: TeamContentProps) {
Invite members
</Button>
) : (
<CreateInviteDialog
roles={roles}
onCreateInvite={handleCreateInvite}
workspaceId={workspaceId}
/>
<CreateInviteDialog roles={roles} onCreateInvite={handleCreateInvite} />
)}
{invitesLoading ? (
<Button
@ -617,11 +607,9 @@ function MemberRow({
function CreateInviteDialog({
roles,
onCreateInvite,
workspaceId,
}: {
roles: Role[];
onCreateInvite: (data: CreateInviteRequest["data"]) => Promise<Invite>;
workspaceId: number;
}) {
const [open, setOpen] = useState(false);
const [creating, setCreating] = useState(false);
@ -653,12 +641,7 @@ function CreateInviteDialog({
const invite = await onCreateInvite(data);
setCreatedInvite(invite);
const roleName = roleId ? roles.find((r) => r.id.toString() === roleId)?.name : undefined;
trackWorkspaceInviteSent(workspaceId, {
roleName,
hasExpiry: !!expiresAt,
hasMaxUses: !!maxUses,
});
// workspace_invite_sent is now emitted server-side (rbac_routes.create_invite).
} catch (error) {
console.error("Failed to create invite:", error);
toast.error("Failed to create invite. Please try again.");

View file

@ -16,7 +16,7 @@ import { motion } from "motion/react";
import Image from "next/image";
import Link from "next/link";
import { useParams, useRouter } from "next/navigation";
import { use, useCallback, useEffect, useState } from "react";
import { useCallback, useEffect, useState } from "react";
import { toast } from "sonner";
import { acceptInviteMutationAtom } from "@/atoms/invites/invites-mutation.atoms";
import { Button } from "@/components/ui/button";
@ -33,11 +33,7 @@ import type { AcceptInviteResponse } from "@/contracts/types/invites.types";
import { useSession } from "@/hooks/use-session";
import { invitesApiService } from "@/lib/apis/invites-api.service";
import { setRedirectPath } from "@/lib/auth-utils";
import {
trackWorkspaceInviteAccepted,
trackWorkspaceInviteDeclined,
trackWorkspaceUserAdded,
} from "@/lib/posthog/events";
import { trackWorkspaceInviteDeclined } from "@/lib/posthog/events";
import { cacheKeys } from "@/lib/query-client/cache-keys";
export default function InviteAcceptPage() {
@ -96,9 +92,9 @@ export default function InviteAcceptPage() {
setAccepted(true);
setAcceptedData(result);
// Track invite accepted and user added events
trackWorkspaceInviteAccepted(result.workspace_id, result.workspace_name, result.role_name);
trackWorkspaceUserAdded(result.workspace_id, result.workspace_name, result.role_name);
// workspace_invite_accepted + workspace_user_added are now emitted
// server-side (rbac_routes.accept_invite) — the server redirect is
// the authoritative join point.
}
} catch (err: any) {
setError(err.message || "Failed to accept invite");