mirror of
https://github.com/MODSetter/SurfSense.git
synced 2026-05-31 19:45:15 +02:00
feat(automations): enhance tracking for automation lifecycle events
- Added tracking for automation creation, updates, deletions, and trigger modifications, including success and failure events. - Implemented event tracking in the automation creation process, including chat approval and rejection scenarios. - Updated the instrumentation client to ensure correct typing for PostHog integration. - Refactored existing mutation atoms to include tracking calls for automation-related actions, improving analytics capabilities.
This commit is contained in:
parent
b1b51ada89
commit
92b1d7a9f7
4 changed files with 316 additions and 14 deletions
|
|
@ -7,6 +7,21 @@ import type {
|
|||
TriggerUpdateRequest,
|
||||
} 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";
|
||||
import { queryClient } from "@/lib/query-client/client";
|
||||
|
||||
|
|
@ -33,13 +48,28 @@ export const createAutomationMutationAtom = atomWithMutation(() => ({
|
|||
mutationFn: async (request: AutomationCreateRequest) => {
|
||||
return automationsApiService.createAutomation(request);
|
||||
},
|
||||
onSuccess: (_, variables) => {
|
||||
onSuccess: (automation, variables) => {
|
||||
invalidateList(variables.search_space_id);
|
||||
toast.success("Automation created");
|
||||
trackAutomationCreated({
|
||||
search_space_id: variables.search_space_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,
|
||||
agent_llm_id: variables.definition.models?.agent_llm_id,
|
||||
image_generation_config_id: variables.definition.models?.image_generation_config_id,
|
||||
vision_llm_config_id: variables.definition.models?.vision_llm_config_id,
|
||||
tags_count: variables.definition.metadata?.tags?.length,
|
||||
});
|
||||
},
|
||||
onError: (error: Error) => {
|
||||
onError: (error: Error, variables) => {
|
||||
console.error("Error creating automation:", error);
|
||||
toast.error("Failed to create automation");
|
||||
trackAutomationCreateFailed({
|
||||
search_space_id: variables.search_space_id,
|
||||
error: error.message,
|
||||
});
|
||||
},
|
||||
}));
|
||||
|
||||
|
|
@ -52,10 +82,32 @@ export const updateAutomationMutationAtom = atomWithMutation(() => ({
|
|||
invalidateDetail(vars.automationId);
|
||||
invalidateList(automation.search_space_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,
|
||||
search_space_id: automation.search_space_id,
|
||||
next_status: vars.patch.status,
|
||||
});
|
||||
} else {
|
||||
trackAutomationUpdated({
|
||||
automation_id: vars.automationId,
|
||||
search_space_id: automation.search_space_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,
|
||||
});
|
||||
}
|
||||
},
|
||||
onError: (error: Error) => {
|
||||
onError: (error: Error, vars) => {
|
||||
console.error("Error updating automation:", error);
|
||||
toast.error("Failed to update automation");
|
||||
trackAutomationUpdateFailed({
|
||||
automation_id: vars.automationId,
|
||||
error: error.message,
|
||||
});
|
||||
},
|
||||
}));
|
||||
|
||||
|
|
@ -69,10 +121,18 @@ export const deleteAutomationMutationAtom = atomWithMutation(() => ({
|
|||
invalidateList(vars.searchSpaceId);
|
||||
invalidateDetail(vars.automationId);
|
||||
toast.success("Automation deleted");
|
||||
trackAutomationDeleted({
|
||||
automation_id: vars.automationId,
|
||||
search_space_id: vars.searchSpaceId,
|
||||
});
|
||||
},
|
||||
onError: (error: Error) => {
|
||||
onError: (error: Error, vars) => {
|
||||
console.error("Error deleting automation:", error);
|
||||
toast.error("Failed to delete automation");
|
||||
trackAutomationDeleteFailed({
|
||||
automation_id: vars.automationId,
|
||||
error: error.message,
|
||||
});
|
||||
},
|
||||
}));
|
||||
|
||||
|
|
@ -81,13 +141,24 @@ export const addTriggerMutationAtom = atomWithMutation(() => ({
|
|||
mutationFn: async (vars: { automationId: number; payload: TriggerCreateRequest }) => {
|
||||
return automationsApiService.addTrigger(vars.automationId, vars.payload);
|
||||
},
|
||||
onSuccess: (_, 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,
|
||||
});
|
||||
},
|
||||
onError: (error: Error) => {
|
||||
onError: (error: Error, vars) => {
|
||||
console.error("Error adding trigger:", error);
|
||||
toast.error("Failed to add trigger");
|
||||
trackAutomationTriggerAddFailed({
|
||||
automation_id: vars.automationId,
|
||||
error: error.message,
|
||||
});
|
||||
},
|
||||
}));
|
||||
|
||||
|
|
@ -103,10 +174,26 @@ 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,
|
||||
});
|
||||
},
|
||||
onError: (error: Error) => {
|
||||
onError: (error: Error, vars) => {
|
||||
console.error("Error updating trigger:", error);
|
||||
toast.error("Failed to update trigger");
|
||||
trackAutomationTriggerUpdateFailed({
|
||||
automation_id: vars.automationId,
|
||||
trigger_id: vars.triggerId,
|
||||
error: error.message,
|
||||
});
|
||||
},
|
||||
}));
|
||||
|
||||
|
|
@ -119,9 +206,18 @@ export const removeTriggerMutationAtom = atomWithMutation(() => ({
|
|||
onSuccess: (vars) => {
|
||||
invalidateDetail(vars.automationId);
|
||||
toast.success("Trigger removed");
|
||||
trackAutomationTriggerRemoved({
|
||||
automation_id: vars.automationId,
|
||||
trigger_id: vars.triggerId,
|
||||
});
|
||||
},
|
||||
onError: (error: Error) => {
|
||||
onError: (error: Error, vars) => {
|
||||
console.error("Error removing trigger:", error);
|
||||
toast.error("Failed to remove trigger");
|
||||
trackAutomationTriggerRemoveFailed({
|
||||
automation_id: vars.automationId,
|
||||
trigger_id: vars.triggerId,
|
||||
error: error.message,
|
||||
});
|
||||
},
|
||||
}));
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ import type { ToolCallMessagePartProps } from "@assistant-ui/react";
|
|||
import { useAtomValue } from "jotai";
|
||||
import { AlertCircle, CornerDownLeftIcon, ExternalLink, Pencil, Workflow } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import { useCallback, useEffect, useMemo, useState } from "react";
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||
import {
|
||||
AutomationModelFields,
|
||||
type AutomationModelSelection,
|
||||
|
|
@ -17,6 +17,13 @@ import { automationCreateRequest } from "@/contracts/types/automation.types";
|
|||
import type { HitlDecision, InterruptResult } from "@/features/chat-messages/hitl";
|
||||
import { isInterruptResult, useHitlDecision, useHitlPhase } from "@/features/chat-messages/hitl";
|
||||
import { useAutomationEligibleModels } from "@/hooks/use-automation-eligible-models";
|
||||
import {
|
||||
trackAutomationChatApproved,
|
||||
trackAutomationChatCreateFailed,
|
||||
trackAutomationChatCreateSucceeded,
|
||||
trackAutomationChatDraftEdited,
|
||||
trackAutomationChatRejected,
|
||||
} from "@/lib/posthog/events";
|
||||
import { AutomationDraftPreview } from "./automation-draft-preview";
|
||||
|
||||
const editArgsSchema = automationCreateRequest.omit({ search_space_id: true });
|
||||
|
|
@ -145,6 +152,19 @@ function ApprovalCard({ args, interruptData, onDecision }: ApprovalCardProps) {
|
|||
},
|
||||
},
|
||||
};
|
||||
const plan = Array.isArray(baseDefinition.plan) ? baseDefinition.plan : [];
|
||||
const triggers = Array.isArray(baseArgs.triggers) ? baseArgs.triggers : [];
|
||||
trackAutomationChatApproved({
|
||||
search_space_id: searchSpaceId ? Number(searchSpaceId) : undefined,
|
||||
edited: pendingEdits !== null,
|
||||
task_count: plan.length,
|
||||
trigger_type:
|
||||
(triggers[0] as { type?: string } | undefined)?.type ??
|
||||
(triggers.length ? undefined : "none"),
|
||||
agent_llm_id: resolvedModels.agentLlmId,
|
||||
image_generation_config_id: resolvedModels.imageConfigId,
|
||||
vision_llm_config_id: resolvedModels.visionConfigId,
|
||||
});
|
||||
onDecision({
|
||||
type: "edit",
|
||||
edited_action: {
|
||||
|
|
@ -163,13 +183,17 @@ function ApprovalCard({ args, interruptData, onDecision }: ApprovalCardProps) {
|
|||
args,
|
||||
pendingEdits,
|
||||
resolvedModels,
|
||||
searchSpaceId,
|
||||
]);
|
||||
|
||||
const handleReject = useCallback(() => {
|
||||
if (phase !== "pending" || !canReject || isEditing) return;
|
||||
setRejected();
|
||||
trackAutomationChatRejected({
|
||||
search_space_id: searchSpaceId ? Number(searchSpaceId) : undefined,
|
||||
});
|
||||
onDecision({ type: "reject", message: "User rejected the automation draft." });
|
||||
}, [phase, canReject, isEditing, setRejected, onDecision]);
|
||||
}, [phase, canReject, isEditing, setRejected, onDecision, searchSpaceId]);
|
||||
|
||||
useEffect(() => {
|
||||
if (isEditing) return;
|
||||
|
|
@ -242,6 +266,9 @@ function ApprovalCard({ args, interruptData, onDecision }: ApprovalCardProps) {
|
|||
onSave={(parsed) => {
|
||||
setPendingEdits(parsed);
|
||||
setIsEditing(false);
|
||||
trackAutomationChatDraftEdited({
|
||||
search_space_id: searchSpaceId ? Number(searchSpaceId) : undefined,
|
||||
});
|
||||
}}
|
||||
onCancel={() => setIsEditing(false)}
|
||||
/>
|
||||
|
|
@ -356,6 +383,17 @@ function JsonEditor({ initialValue, onSave, onCancel }: JsonEditorProps) {
|
|||
|
||||
function SavedCard({ result }: { result: SavedResult }) {
|
||||
const searchSpaceId = useAtomValue(activeSearchSpaceIdAtom);
|
||||
const tracked = useRef(false);
|
||||
useEffect(() => {
|
||||
if (tracked.current) return;
|
||||
tracked.current = true;
|
||||
trackAutomationChatCreateSucceeded({
|
||||
automation_id: result.automation_id,
|
||||
name: result.name,
|
||||
search_space_id: searchSpaceId ? Number(searchSpaceId) : undefined,
|
||||
});
|
||||
}, [result.automation_id, result.name, searchSpaceId]);
|
||||
|
||||
const detailHref = searchSpaceId
|
||||
? `/dashboard/${searchSpaceId}/automations/${result.automation_id}`
|
||||
: null;
|
||||
|
|
@ -388,6 +426,18 @@ function SavedCard({ result }: { result: SavedResult }) {
|
|||
}
|
||||
|
||||
function InvalidCard({ result }: { result: InvalidResult }) {
|
||||
const searchSpaceId = useAtomValue(activeSearchSpaceIdAtom);
|
||||
const tracked = useRef(false);
|
||||
useEffect(() => {
|
||||
if (tracked.current) return;
|
||||
tracked.current = true;
|
||||
trackAutomationChatCreateFailed({
|
||||
reason: "invalid",
|
||||
issue_count: result.issues.length,
|
||||
search_space_id: searchSpaceId ? Number(searchSpaceId) : undefined,
|
||||
});
|
||||
}, [result.issues.length, searchSpaceId]);
|
||||
|
||||
return (
|
||||
<div className="my-4 max-w-lg overflow-hidden rounded-2xl border bg-muted/30 select-none">
|
||||
<div className="px-5 pt-5 pb-4">
|
||||
|
|
@ -411,6 +461,18 @@ function InvalidCard({ result }: { result: InvalidResult }) {
|
|||
}
|
||||
|
||||
function ErrorCard({ result }: { result: ErrorResult }) {
|
||||
const searchSpaceId = useAtomValue(activeSearchSpaceIdAtom);
|
||||
const tracked = useRef(false);
|
||||
useEffect(() => {
|
||||
if (tracked.current) return;
|
||||
tracked.current = true;
|
||||
trackAutomationChatCreateFailed({
|
||||
reason: "error",
|
||||
message: result.message,
|
||||
search_space_id: searchSpaceId ? Number(searchSpaceId) : undefined,
|
||||
});
|
||||
}, [result.message, searchSpaceId]);
|
||||
|
||||
return (
|
||||
<div className="my-4 max-w-lg overflow-hidden rounded-2xl border bg-muted/30 select-none">
|
||||
<div className="px-5 pt-5 pb-4">
|
||||
|
|
|
|||
|
|
@ -88,9 +88,12 @@ async function initPostHog() {
|
|||
}
|
||||
return event;
|
||||
},
|
||||
loaded: (ph) => {
|
||||
loaded: () => {
|
||||
if (typeof window !== "undefined") {
|
||||
window.posthog = ph;
|
||||
// `loaded` hands back a `PostHogInterface`, but it's the same
|
||||
// singleton as the default import (typed `PostHog`); use that to
|
||||
// keep `window.posthog` correctly typed.
|
||||
window.posthog = posthog;
|
||||
}
|
||||
},
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import posthog from "posthog-js";
|
||||
import type { ChatErrorKind, ChatErrorSeverity, ChatFlow } from "@/lib/chat/chat-error-classifier";
|
||||
import type { ConnectorTelemetryMeta } from "@/lib/connector-telemetry";
|
||||
import { getConnectorTelemetryMeta } from "@/lib/connector-telemetry";
|
||||
|
||||
/**
|
||||
|
|
@ -19,6 +19,7 @@ import { getConnectorTelemetryMeta } from "@/lib/connector-telemetry";
|
|||
* - 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
|
||||
*/
|
||||
|
||||
|
|
@ -33,7 +34,7 @@ function safeCapture(event: string, properties?: Record<string, unknown>) {
|
|||
/**
|
||||
* Drop undefined values so PostHog doesn't log `"foo": undefined` noise.
|
||||
*/
|
||||
function compact<T extends Record<string, unknown>>(obj: T): Record<string, unknown> {
|
||||
function compact<T extends object>(obj: T): Record<string, unknown> {
|
||||
const out: Record<string, unknown> = {};
|
||||
for (const [k, v] of Object.entries(obj)) {
|
||||
if (v !== undefined) out[k] = v;
|
||||
|
|
@ -598,6 +599,146 @@ export function trackReferralLanding(refCode: string, landingUrl: string) {
|
|||
});
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// AUTOMATION EVENTS
|
||||
// ============================================
|
||||
|
||||
interface AutomationCreatedProps {
|
||||
search_space_id: number;
|
||||
automation_id: number;
|
||||
task_count?: number;
|
||||
trigger_type?: string;
|
||||
has_schedule?: boolean;
|
||||
agent_llm_id?: number;
|
||||
image_generation_config_id?: number;
|
||||
vision_llm_config_id?: number;
|
||||
tags_count?: number;
|
||||
}
|
||||
|
||||
export function trackAutomationCreated(props: AutomationCreatedProps) {
|
||||
safeCapture("automation_created", compact(props));
|
||||
}
|
||||
|
||||
export function trackAutomationCreateFailed(props: { search_space_id?: number; error?: string }) {
|
||||
safeCapture("automation_create_failed", compact(props));
|
||||
}
|
||||
|
||||
export function trackAutomationUpdated(props: {
|
||||
automation_id: number;
|
||||
search_space_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;
|
||||
search_space_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; search_space_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;
|
||||
error?: string;
|
||||
}) {
|
||||
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;
|
||||
error?: string;
|
||||
}) {
|
||||
safeCapture("automation_trigger_remove_failed", compact(props));
|
||||
}
|
||||
|
||||
interface AutomationChatDecisionProps {
|
||||
search_space_id?: number;
|
||||
edited?: boolean;
|
||||
task_count?: number;
|
||||
trigger_type?: string;
|
||||
agent_llm_id?: number;
|
||||
image_generation_config_id?: number;
|
||||
vision_llm_config_id?: number;
|
||||
}
|
||||
|
||||
export function trackAutomationChatApproved(props: AutomationChatDecisionProps) {
|
||||
safeCapture("automation_chat_approved", compact(props));
|
||||
}
|
||||
|
||||
export function trackAutomationChatRejected(props: { search_space_id?: number }) {
|
||||
safeCapture("automation_chat_rejected", compact(props));
|
||||
}
|
||||
|
||||
export function trackAutomationChatDraftEdited(props: { search_space_id?: number }) {
|
||||
safeCapture("automation_chat_draft_edited", compact(props));
|
||||
}
|
||||
|
||||
export function trackAutomationChatCreateSucceeded(props: {
|
||||
automation_id: number;
|
||||
name?: string;
|
||||
search_space_id?: number;
|
||||
}) {
|
||||
safeCapture("automation_chat_create_succeeded", compact(props));
|
||||
}
|
||||
|
||||
export function trackAutomationChatCreateFailed(props: {
|
||||
reason: "invalid" | "error";
|
||||
search_space_id?: number;
|
||||
issue_count?: number;
|
||||
message?: string;
|
||||
}) {
|
||||
safeCapture("automation_chat_create_failed", compact(props));
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// USER IDENTIFICATION
|
||||
// ============================================
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue