Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 35 additions & 0 deletions packages/shared/src/analytics-events.ts
Original file line number Diff line number Diff line change
Expand Up @@ -738,6 +738,33 @@ export interface SignalSourceConnectedProperties {
via_setup_wizard: boolean;
}

// Agents page events (the `/code/agents` configuration surface)
export type AgentsActionType =
| "run_setup_agent"
| "change_autostart_priority"
| "open_mcp_servers";

export interface AgentsViewedProperties {
/** Whether code access (GitHub) is connected — gates responder configuration. */
has_github_integration: boolean;
/** Total number of responder source products on the page. */
responder_total_count: number;
/** How many of those responders are currently enabled. */
responder_enabled_count: number;
/** User's PR auto-start threshold priority (P0–P4), or null when set to "Never". */
autostart_priority: string | null;
/** Whether the agent-driven setup entry point is shown (feature-flagged). */
setup_task_available: boolean;
}

export interface AgentsActionProperties {
action_type: AgentsActionType;
/** New threshold for `change_autostart_priority` (P0–P4, or null for "Never"). */
autostart_priority?: string | null;
/** Whether `run_setup_agent` successfully created the setup task. */
success?: boolean;
}

// Subscription / billing events

export type UpgradePromptShownSurface = "usage_limit_modal" | "upgrade_dialog";
Expand Down Expand Up @@ -885,6 +912,10 @@ export const ANALYTICS_EVENTS = {
INBOX_REPORT_SCROLLED: "Inbox report scrolled",
SIGNAL_SOURCE_CONNECTED: "Signal source connected",

// Agents page events
AGENTS_VIEWED: "Agents viewed",
AGENTS_ACTION: "Agents action",

// Scout events
SCOUT_FLEET_VIEWED: "Scout fleet viewed",
SCOUT_DETAIL_VIEWED: "Scout detail viewed",
Expand Down Expand Up @@ -1015,6 +1046,10 @@ export type EventPropertyMap = {
[ANALYTICS_EVENTS.INBOX_REPORT_SCROLLED]: InboxReportScrolledProperties;
[ANALYTICS_EVENTS.SIGNAL_SOURCE_CONNECTED]: SignalSourceConnectedProperties;

// Agents page events
[ANALYTICS_EVENTS.AGENTS_VIEWED]: AgentsViewedProperties;
[ANALYTICS_EVENTS.AGENTS_ACTION]: AgentsActionProperties;

// Scout events
[ANALYTICS_EVENTS.SCOUT_FLEET_VIEWED]: ScoutFleetViewedProperties;
[ANALYTICS_EVENTS.SCOUT_DETAIL_VIEWED]: ScoutDetailViewedProperties;
Expand Down
65 changes: 65 additions & 0 deletions packages/ui/src/features/agents/hooks/useTrackAgentsViewed.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import { ANALYTICS_EVENTS } from "@posthog/shared/analytics-events";
import { track } from "@posthog/ui/shell/analytics";
import { useEffect, useRef } from "react";

export interface TrackAgentsViewedInput {
/** Gate the event until responder / integration / autonomy data has settled. */
isLoading: boolean;
/**
* True when a backing fetch errored. An errored request also leaves
* `isLoading` false, so without this gate the event would fire with default
* values (e.g. `has_github_integration: false`) and `firedRef` would lock that
* bogus view in for the rest of the component's lifetime — mirrors the
* `!isSuccess` gate in `useTrackInboxViewed`.
*/
isError: boolean;
hasGithubIntegration: boolean;
responderTotalCount: number;
responderEnabledCount: number;
/** P0–P4, or null when the user's auto-start threshold is "Never". */
autostartPriority: string | null;
setupTaskAvailable: boolean;
}

/**
* Fires `AGENTS_VIEWED` once per visit to the `/code/agents` configuration page,
* after the responder/integration/autonomy data settles, with the state the user
* sees on load. Mirrors `useTrackInboxViewed`; mounted from `ConfigureAgentsSection`
* where the data already lives, so it fires once and survives re-renders.
*/
export function useTrackAgentsViewed(input: TrackAgentsViewedInput): void {
const {
isLoading,
isError,
hasGithubIntegration,
responderTotalCount,
responderEnabledCount,
autostartPriority,
setupTaskAvailable,
} = input;

const firedRef = useRef(false);
useEffect(() => {
if (firedRef.current) return;
// Don't fire (and lock `firedRef`) until the data settled successfully: an
// errored fetch also clears `isLoading`, and firing then would capture a
// bogus default view that a later refetch can't correct.
if (isLoading || isError) return;
firedRef.current = true;
track(ANALYTICS_EVENTS.AGENTS_VIEWED, {
Comment thread
andrewm4894 marked this conversation as resolved.
has_github_integration: hasGithubIntegration,
responder_total_count: responderTotalCount,
responder_enabled_count: responderEnabledCount,
autostart_priority: autostartPriority,
setup_task_available: setupTaskAvailable,
});
}, [
isLoading,
isError,
hasGithubIntegration,
responderTotalCount,
responderEnabledCount,
autostartPriority,
setupTaskAvailable,
]);
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import {
REPORT_MODEL_RESOLVER,
type ReportModelResolver,
} from "@posthog/core/inbox/identifiers";
import { classifyIntegrations } from "@posthog/core/integrations/selectors";
import {
TASK_SERVICE,
type TaskCreationInput,
Expand All @@ -13,13 +14,18 @@ import { Button } from "@posthog/quill";
import { ANALYTICS_EVENTS, getCloudUrlFromRegion } from "@posthog/shared";
import { SELF_DRIVING_SETUP_TASK_FLAG } from "@posthog/shared/constants";
import type { SignalReportPriority } from "@posthog/shared/types";
import { useTrackAgentsViewed } from "@posthog/ui/features/agents/hooks/useTrackAgentsViewed";
import { useAuthStateValue } from "@posthog/ui/features/auth/store";
import { useFeatureFlag } from "@posthog/ui/features/feature-flags/useFeatureFlag";
import { DataSourceSetup } from "@posthog/ui/features/inbox/components/DataSourceSetup";
import {
ResponderAgentRoster,
ResponderAgentRosterSkeleton,
} from "@posthog/ui/features/inbox/components/ResponderAgentRoster";
import {
RESPONDER_AGENT_GROUPS,
type ResponderAgentSource,
} from "@posthog/ui/features/inbox/components/responderAgentMeta";
import { resolveDefaultModel } from "@posthog/ui/features/inbox/hooks/resolveDefaultModel";
import { useSignalSourceManager } from "@posthog/ui/features/inbox/hooks/useSignalSourceManager";
import {
Expand Down Expand Up @@ -71,6 +77,18 @@ Inspect the connected PostHog project and repository, figure out which Self-driv

const log = logger.scope("agents-setup-task");

/**
* Source products that count as Responders on this page. `displayValues` also
* carries the legacy `signals_scout` toggle, but scouts render separately in
* `ScoutsFleetSection` and are excluded from the responder roster — so they must
* not inflate the responder counts in `AGENTS_VIEWED`.
*/
const RESPONDER_SOURCE_PRODUCTS = new Set<ResponderAgentSource>(
RESPONDER_AGENT_GROUPS.flatMap((group) =>
group.agents.map((agent) => agent.source),
),
);

export function ConfigureAgentsSection() {
const {
displayValues,
Expand All @@ -88,12 +106,40 @@ export function ConfigureAgentsSection() {
} = useSignalSourceManager();
const { hasGithubIntegration, isLoadingIntegrations } =
useRepositoryIntegration();
const { isLoading: isLoadingSlackIntegrations } = useIntegrations();
const {
isLoading: isLoadingSlackIntegrations,
isError: isIntegrationsError,
data: integrationsData,
} = useIntegrations();
const isLoadingSlack = isLoadingIntegrations || isLoadingSlackIntegrations;
const showSetupTask = useFeatureFlag(SELF_DRIVING_SETUP_TASK_FLAG);
const userAutostartPriority =
userAutonomyConfig?.autostart_priority ?? NEVER_AUTOSTART_VALUE;

// Derive from the query data, not the store-backed `hasGithubIntegration`: the
// store is hydrated by a passive effect that lags the query by a render, so the
// store value can still read `false` on the render where the query settles —
// exactly when the view event fires. Classifying the query data avoids the lag.
const trackedHasGithubIntegration = classifyIntegrations(
integrationsData ?? [],
).hasGithubIntegration;
// Count only Responder sources; `displayValues` also includes `signals_scout`,
// which renders separately and would otherwise inflate the responder counts.
const responderEntries = Object.entries(displayValues).filter(([source]) =>
RESPONDER_SOURCE_PRODUCTS.has(source as ResponderAgentSource),
);

useTrackAgentsViewed({
isLoading: isLoading || isLoadingIntegrations || userAutonomyConfigLoading,
isError: isIntegrationsError,
hasGithubIntegration: trackedHasGithubIntegration,
responderTotalCount: responderEntries.length,
responderEnabledCount: responderEntries.filter(([, enabled]) => enabled)
.length,
autostartPriority: userAutonomyConfig?.autostart_priority ?? null,
setupTaskAvailable: showSetupTask,
Comment thread
andrewm4894 marked this conversation as resolved.
});

return (
<Flex direction="column" gap="8">
{showSetupTask ? <SetupTaskSection /> : null}
Expand Down Expand Up @@ -210,11 +256,14 @@ export function ConfigureAgentsSection() {
options={USER_AUTOSTART_OPTIONS}
ariaLabel="PR auto-start threshold"
className="min-w-[260px] max-w-[300px]"
onValueChange={(value) =>
void handleUpdateUserAutonomyPriority(
value === NEVER_AUTOSTART_VALUE ? null : value,
)
}
onValueChange={(value) => {
const priority = value === NEVER_AUTOSTART_VALUE ? null : value;
track(ANALYTICS_EVENTS.AGENTS_ACTION, {
action_type: "change_autostart_priority",
autostart_priority: priority,
});
void handleUpdateUserAutonomyPriority(priority);
}}
/>
)}
</Flex>
Expand All @@ -226,6 +275,11 @@ export function ConfigureAgentsSection() {
>
<Link
to="/mcp-servers"
onClick={() =>
track(ANALYTICS_EVENTS.AGENTS_ACTION, {
action_type: "open_mcp_servers",
})
}
className="flex items-center justify-between gap-3 rounded-(--radius-2) border border-border bg-(--color-panel-solid) px-4 py-3.5 no-underline transition-colors duration-150 hover:border-(--gray-6) hover:bg-(--gray-2)"
>
<Flex align="center" gap="3" className="min-w-0">
Expand Down Expand Up @@ -270,23 +324,36 @@ function SetupTaskSection() {
);

const handleStartSetup = useCallback(async () => {
// A click that fails a precondition is still a failed setup attempt; emit
// `run_setup_agent` with success:false so these don't drop out of the funnel
// and bias the success rate upward. (The re-entrancy and still-loading guards
// below are not attempts, so they don't fire.)
const trackSetupFailure = () =>
track(ANALYTICS_EVENTS.AGENTS_ACTION, {
action_type: "run_setup_agent",
success: false,
});

if (isStartingSetupTask) return;
if (isLoadingRepos) {
toast.error("Still loading GitHub repositories");
return;
}
if (!hasGithubIntegration || !setupRepository) {
trackSetupFailure();
toast.error("Connect GitHub before starting Self-driving setup");
return;
}
if (!cloudRegion) {
trackSetupFailure();
toast.error("Sign in to start Self-driving setup");
return;
}

const githubUserIntegrationId =
getUserIntegrationIdForRepo(setupRepository);
if (!githubUserIntegrationId) {
trackSetupFailure();
toast.error("Connect a GitHub integration with repository access");
return;
}
Expand All @@ -312,6 +379,7 @@ function SetupTaskSection() {

if (!model) {
sonnerToast.dismiss(toastId);
trackSetupFailure();
toast.error("Failed to start Self-driving setup", {
description:
"Couldn't resolve a default model. Open the task page once and pick a model, then try again.",
Expand All @@ -337,6 +405,10 @@ function SetupTaskSection() {
});

sonnerToast.dismiss(toastId);
track(ANALYTICS_EVENTS.AGENTS_ACTION, {
action_type: "run_setup_agent",
success: result.success,
});
Comment thread
andrewm4894 marked this conversation as resolved.
if (result.success) {
Comment thread
andrewm4894 marked this conversation as resolved.
track(ANALYTICS_EVENTS.TASK_CREATED, {
auto_run: true,
Expand All @@ -359,6 +431,10 @@ function SetupTaskSection() {
}
} catch (error) {
sonnerToast.dismiss(toastId);
track(ANALYTICS_EVENTS.AGENTS_ACTION, {
action_type: "run_setup_agent",
success: false,
});
const description =
error instanceof Error ? error.message : "Unknown error";
toast.error("Failed to start Self-driving setup", { description });
Expand Down
Loading