mirror of
https://github.com/MODSetter/SurfSense.git
synced 2026-07-26 23:51:14 +02:00
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:
parent
ca4f231577
commit
dbedf0cfa5
47 changed files with 1618 additions and 513 deletions
|
|
@ -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(() => {
|
||||
|
|
|
|||
|
|
@ -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"), {
|
||||
|
|
|
|||
|
|
@ -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.");
|
||||
|
|
|
|||
|
|
@ -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");
|
||||
|
|
|
|||
|
|
@ -8,18 +8,11 @@ import type {
|
|||
} from "@/contracts/types/automation.types";
|
||||
import { automationsApiService } from "@/lib/apis/automations-api.service";
|
||||
import {
|
||||
trackAutomationCreated,
|
||||
trackAutomationCreateFailed,
|
||||
trackAutomationDeleted,
|
||||
trackAutomationDeleteFailed,
|
||||
trackAutomationStatusChanged,
|
||||
trackAutomationTriggerAdded,
|
||||
trackAutomationTriggerAddFailed,
|
||||
trackAutomationTriggerRemoved,
|
||||
trackAutomationTriggerRemoveFailed,
|
||||
trackAutomationTriggerUpdated,
|
||||
trackAutomationTriggerUpdateFailed,
|
||||
trackAutomationUpdated,
|
||||
trackAutomationUpdateFailed,
|
||||
} from "@/lib/posthog/events";
|
||||
import { cacheKeys } from "@/lib/query-client/cache-keys";
|
||||
|
|
@ -48,20 +41,10 @@ export const createAutomationMutationAtom = atomWithMutation(() => ({
|
|||
mutationFn: async (request: AutomationCreateRequest) => {
|
||||
return automationsApiService.createAutomation(request);
|
||||
},
|
||||
onSuccess: (automation, variables) => {
|
||||
onSuccess: (_automation, variables) => {
|
||||
invalidateList(variables.workspace_id);
|
||||
toast.success("Automation created");
|
||||
trackAutomationCreated({
|
||||
workspace_id: variables.workspace_id,
|
||||
automation_id: automation.id,
|
||||
task_count: variables.definition.plan.length,
|
||||
trigger_type: variables.triggers?.[0]?.type ?? "none",
|
||||
has_schedule: (variables.triggers?.length ?? 0) > 0,
|
||||
chat_model_id: variables.definition.models?.chat_model_id,
|
||||
image_gen_model_id: variables.definition.models?.image_gen_model_id,
|
||||
vision_model_id: variables.definition.models?.vision_model_id,
|
||||
tags_count: variables.definition.metadata?.tags?.length,
|
||||
});
|
||||
// automation_created is now emitted server-side (AutomationService.create).
|
||||
},
|
||||
onError: (error: Error, variables) => {
|
||||
console.error("Error creating automation:", error);
|
||||
|
|
@ -82,24 +65,8 @@ export const updateAutomationMutationAtom = atomWithMutation(() => ({
|
|||
invalidateDetail(vars.automationId);
|
||||
invalidateList(automation.workspace_id);
|
||||
toast.success("Automation updated");
|
||||
// A status-only patch (pause/resume/archive) is a distinct action from a
|
||||
// definition/name edit, so split it into its own event.
|
||||
if (vars.patch.status && !vars.patch.definition) {
|
||||
trackAutomationStatusChanged({
|
||||
automation_id: vars.automationId,
|
||||
workspace_id: automation.workspace_id,
|
||||
next_status: vars.patch.status,
|
||||
});
|
||||
} else {
|
||||
trackAutomationUpdated({
|
||||
automation_id: vars.automationId,
|
||||
workspace_id: automation.workspace_id,
|
||||
has_definition_change: !!vars.patch.definition,
|
||||
has_name_change: vars.patch.name != null,
|
||||
has_description_change: vars.patch.description !== undefined,
|
||||
task_count: vars.patch.definition?.plan?.length,
|
||||
});
|
||||
}
|
||||
// automation_updated / automation_status_changed are now emitted
|
||||
// server-side (AutomationService.update).
|
||||
},
|
||||
onError: (error: Error, vars) => {
|
||||
console.error("Error updating automation:", error);
|
||||
|
|
@ -121,10 +88,7 @@ export const deleteAutomationMutationAtom = atomWithMutation(() => ({
|
|||
invalidateList(vars.workspaceId);
|
||||
invalidateDetail(vars.automationId);
|
||||
toast.success("Automation deleted");
|
||||
trackAutomationDeleted({
|
||||
automation_id: vars.automationId,
|
||||
workspace_id: vars.workspaceId,
|
||||
});
|
||||
// automation_deleted is now emitted server-side (AutomationService.delete).
|
||||
},
|
||||
onError: (error: Error, vars) => {
|
||||
console.error("Error deleting automation:", error);
|
||||
|
|
@ -141,16 +105,10 @@ export const addTriggerMutationAtom = atomWithMutation(() => ({
|
|||
mutationFn: async (vars: { automationId: number; payload: TriggerCreateRequest }) => {
|
||||
return automationsApiService.addTrigger(vars.automationId, vars.payload);
|
||||
},
|
||||
onSuccess: (trigger, vars) => {
|
||||
onSuccess: (_trigger, vars) => {
|
||||
invalidateDetail(vars.automationId);
|
||||
toast.success("Trigger added");
|
||||
trackAutomationTriggerAdded({
|
||||
automation_id: vars.automationId,
|
||||
trigger_id: trigger.id,
|
||||
trigger_type: trigger.type,
|
||||
enabled: trigger.enabled,
|
||||
has_cron: !!trigger.params?.cron,
|
||||
});
|
||||
// automation_trigger_added is now emitted server-side (TriggerService.add).
|
||||
},
|
||||
onError: (error: Error, vars) => {
|
||||
console.error("Error adding trigger:", error);
|
||||
|
|
@ -174,17 +132,7 @@ export const updateTriggerMutationAtom = atomWithMutation(() => ({
|
|||
onSuccess: (_, vars) => {
|
||||
invalidateDetail(vars.automationId);
|
||||
toast.success("Trigger updated");
|
||||
const change: "enabled" | "params" | "other" = vars.patch.params
|
||||
? "params"
|
||||
: vars.patch.enabled !== undefined && vars.patch.enabled !== null
|
||||
? "enabled"
|
||||
: "other";
|
||||
trackAutomationTriggerUpdated({
|
||||
automation_id: vars.automationId,
|
||||
trigger_id: vars.triggerId,
|
||||
change,
|
||||
enabled: vars.patch.enabled ?? undefined,
|
||||
});
|
||||
// automation_trigger_updated is now emitted server-side (TriggerService.update).
|
||||
},
|
||||
onError: (error: Error, vars) => {
|
||||
console.error("Error updating trigger:", error);
|
||||
|
|
@ -206,10 +154,7 @@ export const removeTriggerMutationAtom = atomWithMutation(() => ({
|
|||
onSuccess: (vars) => {
|
||||
invalidateDetail(vars.automationId);
|
||||
toast.success("Trigger removed");
|
||||
trackAutomationTriggerRemoved({
|
||||
automation_id: vars.automationId,
|
||||
trigger_id: vars.triggerId,
|
||||
});
|
||||
// automation_trigger_removed is now emitted server-side (TriggerService.remove).
|
||||
},
|
||||
onError: (error: Error, vars) => {
|
||||
console.error("Error removing trigger:", error);
|
||||
|
|
|
|||
|
|
@ -21,8 +21,6 @@ import { OAUTH_RESULT_COOKIE, parseOAuthCallbackResult } from "@/contracts/types
|
|||
import { authenticatedFetch } from "@/lib/auth-fetch";
|
||||
import { buildBackendUrl } from "@/lib/env-config";
|
||||
import {
|
||||
trackConnectorConnected,
|
||||
trackConnectorDeleted,
|
||||
trackConnectorSetupFailure,
|
||||
trackConnectorSetupStarted,
|
||||
trackIndexWithDateRangeOpened,
|
||||
|
|
@ -296,11 +294,9 @@ export const useConnectorDialog = () => {
|
|||
if (newConnector && oauthConnector) {
|
||||
const connectorValidation = searchSourceConnector.safeParse(newConnector);
|
||||
if (connectorValidation.success) {
|
||||
trackConnectorConnected(
|
||||
Number(workspaceId),
|
||||
oauthConnector.connectorType,
|
||||
newConnector.id
|
||||
);
|
||||
// connector_connected is now emitted server-side (after_insert
|
||||
// listener in search_source_connectors_routes.py) — covers the
|
||||
// OAuth callback redirect the frontend often never observes.
|
||||
|
||||
const isLiveConnector = LIVE_CONNECTOR_TYPES.has(oauthConnector.connectorType);
|
||||
|
||||
|
|
@ -436,12 +432,7 @@ export const useConnectorDialog = () => {
|
|||
if (connector) {
|
||||
const connectorValidation = searchSourceConnector.safeParse(connector);
|
||||
if (connectorValidation.success) {
|
||||
// Track webcrawler connector connected
|
||||
trackConnectorConnected(
|
||||
Number(workspaceId),
|
||||
EnumConnectorName.WEBCRAWLER_CONNECTOR,
|
||||
connector.id
|
||||
);
|
||||
// connector_connected is now emitted server-side (after_insert).
|
||||
|
||||
const config = validateIndexingConfigState({
|
||||
connectorType: EnumConnectorName.WEBCRAWLER_CONNECTOR,
|
||||
|
|
@ -540,8 +531,7 @@ export const useConnectorDialog = () => {
|
|||
// Store connectingConnectorType before clearing it
|
||||
const currentConnectorType = connectingConnectorType;
|
||||
|
||||
// Track connector connected event for non-OAuth connectors
|
||||
trackConnectorConnected(Number(workspaceId), currentConnectorType, connector.id);
|
||||
// connector_connected is now emitted server-side (after_insert).
|
||||
|
||||
// Find connector title from constants
|
||||
const connectorInfo = OTHER_CONNECTORS.find(
|
||||
|
|
@ -1229,12 +1219,8 @@ export const useConnectorDialog = () => {
|
|||
id: editingConnector.id,
|
||||
});
|
||||
|
||||
// Track connector deleted event
|
||||
trackConnectorDeleted(
|
||||
Number(workspaceId),
|
||||
editingConnector.connector_type,
|
||||
editingConnector.id
|
||||
);
|
||||
// connector_deleted is now emitted server-side
|
||||
// (search_source_connectors_routes.delete_search_source_connector).
|
||||
|
||||
toast.success(
|
||||
editingConnector.connector_type === "MCP_CONNECTOR"
|
||||
|
|
|
|||
|
|
@ -28,7 +28,6 @@ import {
|
|||
} from "@/components/ui/form";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Spinner } from "@/components/ui/spinner";
|
||||
import { trackWorkspaceCreated } from "@/lib/posthog/events";
|
||||
import { cacheKeys } from "@/lib/query-client/cache-keys";
|
||||
import { queryClient } from "@/lib/query-client/client";
|
||||
|
||||
|
|
@ -68,7 +67,8 @@ export function CreateWorkspaceDialog({ open, onOpenChange }: CreateWorkspaceDia
|
|||
description: values.description || "",
|
||||
});
|
||||
|
||||
trackWorkspaceCreated(result.id, values.name);
|
||||
// workspace_created is now emitted server-side (workspaces_routes.py)
|
||||
// so PAT/MCP-created workspaces are also counted.
|
||||
|
||||
// Seed the gate's query so it resolves without a loader flash, and
|
||||
// route straight to onboarding vs. new-chat on the first hop.
|
||||
|
|
|
|||
|
|
@ -4,7 +4,6 @@ import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
|||
import { Check, ExternalLink } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import { useParams } from "next/navigation";
|
||||
import { useEffect } from "react";
|
||||
import { toast } from "sonner";
|
||||
import { USER_QUERY_KEY } from "@/atoms/user/user-query.atoms";
|
||||
import { Button } from "@/components/ui/button";
|
||||
|
|
@ -15,11 +14,7 @@ import { Spinner } from "@/components/ui/spinner";
|
|||
import type { IncentiveTaskInfo } from "@/contracts/types/incentive-tasks.types";
|
||||
import { incentiveTasksApiService } from "@/lib/apis/incentive-tasks-api.service";
|
||||
import { stripeApiService } from "@/lib/apis/stripe-api.service";
|
||||
import {
|
||||
trackIncentivePageViewed,
|
||||
trackIncentiveTaskClicked,
|
||||
trackIncentiveTaskCompleted,
|
||||
} from "@/lib/posthog/events";
|
||||
import { trackIncentiveTaskClicked } from "@/lib/posthog/events";
|
||||
import { getWorkspaceIdParam } from "@/lib/route-params";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
|
|
@ -35,9 +30,7 @@ export function EarnCreditsContent() {
|
|||
const queryClient = useQueryClient();
|
||||
const workspaceId = getWorkspaceIdParam(params) ?? "";
|
||||
|
||||
useEffect(() => {
|
||||
trackIncentivePageViewed();
|
||||
}, []);
|
||||
// incentive_page_viewed removed — redundant with $pageview.
|
||||
|
||||
const { data, isLoading } = useQuery({
|
||||
queryKey: ["incentive-tasks"],
|
||||
|
|
@ -51,13 +44,11 @@ export function EarnCreditsContent() {
|
|||
|
||||
const completeMutation = useMutation({
|
||||
mutationFn: incentiveTasksApiService.completeTask,
|
||||
onSuccess: (response, taskType) => {
|
||||
onSuccess: (response) => {
|
||||
if (response.success) {
|
||||
toast.success(response.message);
|
||||
const task = data?.tasks.find((t) => t.task_type === taskType);
|
||||
if (task) {
|
||||
trackIncentiveTaskCompleted(taskType, task.credit_micros_reward);
|
||||
}
|
||||
// incentive_task_completed is now emitted server-side
|
||||
// (incentive_tasks_routes.complete_task) where credit is granted.
|
||||
queryClient.invalidateQueries({ queryKey: ["incentive-tasks"] });
|
||||
queryClient.invalidateQueries({ queryKey: USER_QUERY_KEY });
|
||||
}
|
||||
|
|
|
|||
|
|
@ -29,11 +29,7 @@ import { Switch } from "@/components/ui/switch";
|
|||
import type { ProcessingMode } from "@/contracts/types/document.types";
|
||||
import { useElectronAPI } from "@/hooks/use-platform";
|
||||
import { documentsApiService } from "@/lib/apis/documents-api.service";
|
||||
import {
|
||||
trackDocumentUploadFailure,
|
||||
trackDocumentUploadStarted,
|
||||
trackDocumentUploadSuccess,
|
||||
} from "@/lib/posthog/events";
|
||||
import { trackDocumentUploadStarted } from "@/lib/posthog/events";
|
||||
import {
|
||||
getAcceptedFileTypes,
|
||||
getSupportedExtensions,
|
||||
|
|
@ -380,13 +376,14 @@ export function DocumentUploadTab({
|
|||
setUploadProgress(Math.round((uploaded / total) * 100));
|
||||
}
|
||||
|
||||
trackDocumentUploadSuccess(Number(workspaceId), total);
|
||||
// Ingestion outcome is now emitted server-side
|
||||
// (document_processing_completed/_failed in document_tasks.py); the
|
||||
// upload POST succeeding only means the file was accepted, not processed.
|
||||
toast(t("upload_initiated"), { description: t("upload_initiated_desc") });
|
||||
setFolderUpload(null);
|
||||
onSuccess?.();
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : "Upload failed";
|
||||
trackDocumentUploadFailure(Number(workspaceId), message);
|
||||
toast(t("upload_error"), {
|
||||
description: `${t("upload_error_desc")}: ${message}`,
|
||||
});
|
||||
|
|
@ -421,7 +418,7 @@ export function DocumentUploadTab({
|
|||
onSuccess: () => {
|
||||
if (progressIntervalRef.current) clearInterval(progressIntervalRef.current);
|
||||
setUploadProgress(100);
|
||||
trackDocumentUploadSuccess(Number(workspaceId), files.length);
|
||||
// Ingestion outcome now server-side (document_processing_*).
|
||||
toast(t("upload_initiated"), { description: t("upload_initiated_desc") });
|
||||
onSuccess?.();
|
||||
},
|
||||
|
|
@ -429,7 +426,6 @@ export function DocumentUploadTab({
|
|||
if (progressIntervalRef.current) clearInterval(progressIntervalRef.current);
|
||||
setUploadProgress(0);
|
||||
const message = error instanceof Error ? error.message : "Upload failed";
|
||||
trackDocumentUploadFailure(Number(workspaceId), message);
|
||||
toast(t("upload_error"), {
|
||||
description: `${t("upload_error_desc")}: ${message}`,
|
||||
});
|
||||
|
|
|
|||
|
|
@ -156,6 +156,40 @@ transport.
|
|||
Celery runtime, and runtime gauges appear within one export interval.
|
||||
6. Confirm logs emitted inside a traced request show non-zero trace and span IDs.
|
||||
|
||||
## Product Analytics (PostHog)
|
||||
|
||||
Separate from OpenTelemetry, the backend can emit server-side product events to
|
||||
PostHog. This is the authoritative source for outcome events (chats, document
|
||||
ingestion, connector indexing, billing, automations) because it captures traffic
|
||||
the browser never sees — MCP clients, personal-access-token scripts, and Celery
|
||||
background jobs. It is fully opt-in and mirrors the OTel contract: with
|
||||
`POSTHOG_API_KEY` unset, every capture is a silent no-op.
|
||||
|
||||
Use the **same** project key as the frontend's `NEXT_PUBLIC_POSTHOG_KEY` so
|
||||
server events merge onto the same PostHog persons the web app identifies by user
|
||||
id. Add these to `surfsense_backend/.env` (local) or `docker/.env` (production);
|
||||
they reach the API, Celery worker, and beat services via `env_file`, so no
|
||||
compose changes are needed:
|
||||
|
||||
```dotenv
|
||||
POSTHOG_API_KEY=phc_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
|
||||
POSTHOG_HOST=https://us.i.posthog.com
|
||||
POSTHOG_AI_PRIVACY_MODE=true
|
||||
```
|
||||
|
||||
`POSTHOG_AI_PRIVACY_MODE` defaults to `true`; set it to `false` only if you want
|
||||
LLM prompt and completion bodies shipped to PostHog's AI observability views.
|
||||
|
||||
Every backend event is stamped `source=backend` (so it is distinguishable from
|
||||
frontend captures), carries `auth_method` / `client` for surface attribution, and
|
||||
sends `disable_geoip=true` so the server IP never overwrites a person's real
|
||||
location. LangGraph chat turns additionally emit `$ai_generation` / `$ai_span`
|
||||
traces via the PostHog LangChain callback handler, keyed by turn and chat id.
|
||||
|
||||
Keep event properties low-cardinality. Never attach user content — workspace
|
||||
names, connector titles, document titles, prompts, or raw queries — as event
|
||||
properties; they carry no aggregation value and are a privacy risk.
|
||||
|
||||
## Out Of Scope
|
||||
|
||||
- Frontend/browser OpenTelemetry.
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -39,6 +39,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<string, string>;
|
||||
|
|
@ -281,29 +305,20 @@ 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;
|
||||
}
|
||||
|
||||
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 (validation, authz, 404)
|
||||
// are expected behavior — capturing them was billable error-tracking
|
||||
// noise. AuthenticationError (401) is a 4xx and stays excluded.
|
||||
if (error instanceof AppError && error.status >= 500) {
|
||||
captureApiException(error, url, options?.method);
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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<string, unknown>) {
|
||||
|
|
@ -43,17 +39,13 @@ function compact<T extends object>(obj: T): Record<string, unknown> {
|
|||
}
|
||||
|
||||
// ============================================
|
||||
// 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;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue