import * as React from "react"; // ---------------------------------------------------------------------------// Product-analytics seam — same DI shape as ./error-reporting: a module-level// client set by a provider the HOST mounts, and a free `trackEvent` function// callsites import directly (works outside React, e.g. oauth-popup callbacks).// No client mounted (local, self-host, cloudflare, tests) → every call is a// no-op. Cloud mounts `AnalyticsProvider` with a posthog-backed client.//// `AnalyticsEvents` is the single catalog of every product event: names are// `object_verb` snake_case, properties are snake_case. A callsite with a typo// or an undeclared property is a type error, so the catalog can't drift from// the instrumentation.//// BROWSER-ONLY by design: during SSR the host mounts no client (cloud's is// undefined when `window` is absent), and on shared-module-scope runtimes// (Cloudflare Workers) every SSR render resets the singleton to null. Server-// side product events need their own seam — do not route them through this one.//// PROPERTY RULES — properties must never carry:// - secrets, tokens, credential values, copied clipboard contents// - emails, person/org names, or any user-entered free text// - connection names or tool ADDRESSES (both embed user-entered label text;// integration slugs and spec-derived tool names are fine)// - policy patterns (user-entered globs) — use `pattern_kind` instead// Identity attaches via posthog identify/group (the host's concern), never as// event properties.// --------------------------------------------------------------------------- type Owner = "org" | "user"; export interface AnalyticsEvents { // ── Integrations ───────────────────────────────────────────────────────── integration_connect_dialog_opened: {}; integration_detect_submitted: { success: boolean; detected_kind?: string; confidence?: string; }; integration_add_started: { plugin_key: string; via: "detect" | "manual" | "preset" | "command_palette"; preset_id?: string; }; integration_added: { plugin_key: string; integration_slug?: string }; integration_add_cancelled: { plugin_key: string }; integration_removed: { integration_slug: string; success: boolean }; integration_refreshed: { integration_slug: string; connection_count: number; success: boolean; }; integration_renamed: { integration_slug: string; success: boolean }; // ── Connections & auth ─────────────────────────────────────────────────── connection_add_opened: { integration_slug: string; has_oauth_method: boolean; has_api_key_method: boolean; }; connection_credential_submitted: { integration_slug: string; owner: Owner; credential_origin: "paste" | "onepassword"; success: boolean; }; connection_oauth_started: { integration_slug: string; owner: Owner; /** Which connect route the click took. `enterprise` is MCP * Enterprise-Managed Authorization: the ID-JAG chain against the * organization's identity provider, with no per-server consent. */ flow: "byo" | "dcr" | "cimd" | "enterprise"; success: boolean; dcr_fallback?: boolean; /** `enterprise` only: whether this connect had to walk the member through a * work-identity sign-in first. Distinguishes the one-time setup cost from * the steady state, which is the number that says whether the profile is * delivering "no consent screen". */ work_identity_linked?: boolean; }; connection_reconnected: { integration_slug: string; owner: Owner; success: boolean; }; connection_removed: { integration_slug: string; owner: Owner; success: boolean; }; oauth_completed: { success: boolean }; oauth_popup_blocked: {}; oauth_client_registered: { owner: Owner; grant: string; via_dcr: boolean; success: boolean; }; oauth_client_removed: { owner: Owner }; custom_auth_method_created: { integration_slug: string; kind: string }; custom_auth_method_removed: { integration_slug: string }; // ── Tools ──────────────────────────────────────────────────────────────── tool_selected: { integration_slug: string; tool_name: string }; tool_run_submitted: { integration_slug: string; tool_name: string; args_mode: "form" | "json"; result: "completed" | "paused" | "failed"; is_error?: boolean; }; tool_id_copied: { integration_slug: string; tool_name: string }; tool_policy_set: { action: string; pattern_kind: "exact" | "group"; owner: Owner; }; tool_policy_cleared: { pattern_kind: "exact" | "group"; owner: Owner }; // ── Policies page ──────────────────────────────────────────────────────── policy_created: { action: string; owner: Owner; success: boolean }; policy_action_changed: { action: string; owner: Owner; success: boolean }; policy_removed: { owner: Owner; success: boolean }; policy_reordered: { owner: Owner; direction: "up" | "down"; success: boolean; }; // ── Artifacts page ─────────────────────────────────────────────────────── artifact_opened: { surface: "list" | "deep_link" }; artifact_renamed: { success: boolean }; artifact_removed: { success: boolean }; // ── API keys ───────────────────────────────────────────────────────────── api_key_created: { success: boolean }; api_key_revoked: { success: boolean }; api_key_copied: { kind: "value" | "bearer_header" }; org_api_key_created: { success: boolean }; org_api_key_revoked: { success: boolean }; org_api_key_copied: { kind: "value" | "bearer_header" }; // ── Organization ───────────────────────────────────────────────────────── org_renamed: { success: boolean }; org_member_invited: { role: string; success: boolean }; org_member_role_changed: { role: string; success: boolean }; org_member_removed: { success: boolean }; // ── Executions & approvals ─────────────────────────────────────────────── resume_approval_submitted: { action: "accept" | "decline" | "cancel"; interaction_kind?: string; chained_to_next?: boolean; success: boolean; }; resume_return_prompt_copied: { action: "accept" | "decline" | "cancel" }; // ── MCP install / onboarding ───────────────────────────────────────────── mcp_install_command_copied: { transport: "http" | "stdio"; elicitation_mode?: string; surface: "integrations" | "setup_mcp"; }; mcp_install_transport_switched: { transport: "http" | "stdio" }; mcp_install_elicitation_mode_changed: { elicitation_mode: string }; mcp_install_artifacts_toggled: { artifacts: boolean }; mcp_install_search_tools_toggled: { search_tools: boolean }; // ── Command palette ────────────────────────────────────────────────────── command_palette_navigated: { kind: "integration" | "add_integration" | "preset"; plugin_key?: string; }; // ── Docs / help ────────────────────────────────────────────────────────── docs_opened: { surface: "sidebar" }; // ── Cloud: auth & onboarding ───────────────────────────────────────────── login_cta_clicked: {}; signed_out: {}; org_created: { success: boolean }; org_deleted: { success: boolean }; org_switched: { success: boolean }; org_invitation_accepted: { success: boolean }; setup_mcp_completed: {}; setup_mcp_skipped: {}; // ── Cloud: billing & support ───────────────────────────────────────────── billing_plan_selected: { plan_id: string; action: "activate" | "upgrade" | "downgrade"; }; billing_manage_opened: {}; billing_cancel_plan_clicked: { plan_id: string }; support_opened: {}; support_link_clicked: { label: string }; org_domain_added: { success: boolean }; org_domain_removed: { success: boolean };} export type AnalyticsEventName = keyof AnalyticsEvents; /** * The host-supplied sink. Cloud backs this with `posthog.capture`; hosts that * mount no provider get the no-op default. */export type AnalyticsClient = <Name extends AnalyticsEventName>( name: Name, properties: AnalyticsEvents[Name],) => void; let currentAnalyticsClient: AnalyticsClient | null = null; /** * Imperative injection point — what `AnalyticsProvider` uses, and the hook for * non-React hosts (or tests). Pass `null` to restore the no-op default. */export const setAnalyticsClient = (client: AnalyticsClient | null): void => { currentAnalyticsClient = client;}; /** * Record one product event. Safe to call from anywhere (React handlers, * Effect callbacks, plain modules); a host without a mounted client makes * this a no-op. Events with no properties may omit the second argument. */export const trackEvent = <Name extends AnalyticsEventName>( name: Name, ...rest: {} extends AnalyticsEvents[Name] ? [properties?: AnalyticsEvents[Name]] : [properties: AnalyticsEvents[Name]]): void => { currentAnalyticsClient?.(name, rest[0] ?? ({} as AnalyticsEvents[Name]));}; /** * Declarative mount for React hosts — sets the module-level client during * render, exactly like `FrontendErrorReporterProvider` does for error * reporting. Mount once at the app root, ABOVE any tree that fires events * (in cloud that is the document root, not ExecutorProvider, because the * login/onboarding routes render outside the authenticated shell). */export const AnalyticsProvider = (props: React.PropsWithChildren<{ client?: AnalyticsClient }>) => { currentAnalyticsClient = props.client ?? null; return <>{props.children}</>;};
import * as React from "react";
// ---------------------------------------------------------------------------
// Product-analytics seam — same DI shape as ./error-reporting: a module-level
// client set by a provider the HOST mounts, and a free `trackEvent` function
// callsites import directly (works outside React, e.g. oauth-popup callbacks).
// No client mounted (local, self-host, cloudflare, tests) → every call is a
// no-op. Cloud mounts `AnalyticsProvider` with a posthog-backed client.
//
// `AnalyticsEvents` is the single catalog of every product event: names are
// `object_verb` snake_case, properties are snake_case. A callsite with a typo
// or an undeclared property is a type error, so the catalog can't drift from
// the instrumentation.
//
// BROWSER-ONLY by design: during SSR the host mounts no client (cloud's is
// undefined when `window` is absent), and on shared-module-scope runtimes
// (Cloudflare Workers) every SSR render resets the singleton to null. Server-
// side product events need their own seam — do not route them through this one.
//
// PROPERTY RULES — properties must never carry:
// - secrets, tokens, credential values, copied clipboard contents
// - emails, person/org names, or any user-entered free text
// - connection names or tool ADDRESSES (both embed user-entered label text;
// integration slugs and spec-derived tool names are fine)
// - policy patterns (user-entered globs) — use `pattern_kind` instead
// Identity attaches via posthog identify/group (the host's concern), never as
// event properties.
// ---------------------------------------------------------------------------
type Owner = "org" | "user";
export interface AnalyticsEvents {
// ── Integrations ─────────────────────────────────────────────────────────
integration_connect_dialog_opened: {};
integration_detect_submitted: {
success: boolean;
detected_kind?: string;
confidence?: string;
};
integration_add_started: {
plugin_key: string;
via: "detect" | "manual" | "preset" | "command_palette";
preset_id?: string;
};
integration_added: { plugin_key: string; integration_slug?: string };
integration_add_cancelled: { plugin_key: string };
integration_removed: { integration_slug: string; success: boolean };
integration_refreshed: {
integration_slug: string;
connection_count: number;
success: boolean;
};
integration_renamed: { integration_slug: string; success: boolean };
// ── Connections & auth ───────────────────────────────────────────────────
connection_add_opened: {
integration_slug: string;
has_oauth_method: boolean;
has_api_key_method: boolean;
};
connection_credential_submitted: {
integration_slug: string;
owner: Owner;
credential_origin: "paste" | "onepassword";
success: boolean;
};
connection_oauth_started: {
integration_slug: string;
owner: Owner;
/** Which connect route the click took. `enterprise` is MCP
* Enterprise-Managed Authorization: the ID-JAG chain against the
* organization's identity provider, with no per-server consent. */
flow: "byo" | "dcr" | "cimd" | "enterprise";
success: boolean;
dcr_fallback?: boolean;
/** `enterprise` only: whether this connect had to walk the member through a
* work-identity sign-in first. Distinguishes the one-time setup cost from
* the steady state, which is the number that says whether the profile is
* delivering "no consent screen". */
work_identity_linked?: boolean;
};
connection_reconnected: {
integration_slug: string;
owner: Owner;
success: boolean;
};
connection_removed: {
integration_slug: string;
owner: Owner;
success: boolean;
};
oauth_completed: { success: boolean };
oauth_popup_blocked: {};
oauth_client_registered: {
owner: Owner;
grant: string;
via_dcr: boolean;
success: boolean;
};
oauth_client_removed: { owner: Owner };
custom_auth_method_created: { integration_slug: string; kind: string };
custom_auth_method_removed: { integration_slug: string };
// ── Tools ────────────────────────────────────────────────────────────────
tool_selected: { integration_slug: string; tool_name: string };
tool_run_submitted: {
integration_slug: string;
tool_name: string;
args_mode: "form" | "json";
result: "completed" | "paused" | "failed";
is_error?: boolean;
};
tool_id_copied: { integration_slug: string; tool_name: string };
tool_policy_set: {
action: string;
pattern_kind: "exact" | "group";
owner: Owner;
};
tool_policy_cleared: { pattern_kind: "exact" | "group"; owner: Owner };
// ── Policies page ────────────────────────────────────────────────────────
policy_created: { action: string; owner: Owner; success: boolean };
policy_action_changed: { action: string; owner: Owner; success: boolean };
policy_removed: { owner: Owner; success: boolean };
policy_reordered: {
owner: Owner;
direction: "up" | "down";
success: boolean;
};
// ── Artifacts page ───────────────────────────────────────────────────────
artifact_opened: { surface: "list" | "deep_link" };
artifact_renamed: { success: boolean };
artifact_removed: { success: boolean };
// ── API keys ─────────────────────────────────────────────────────────────
api_key_created: { success: boolean };
api_key_revoked: { success: boolean };
api_key_copied: { kind: "value" | "bearer_header" };
org_api_key_created: { success: boolean };
org_api_key_revoked: { success: boolean };
org_api_key_copied: { kind: "value" | "bearer_header" };
// ── Organization ─────────────────────────────────────────────────────────
org_renamed: { success: boolean };
org_member_invited: { role: string; success: boolean };
org_member_role_changed: { role: string; success: boolean };
org_member_removed: { success: boolean };
// ── Executions & approvals ───────────────────────────────────────────────
resume_approval_submitted: {
action: "accept" | "decline" | "cancel";
interaction_kind?: string;
chained_to_next?: boolean;
success: boolean;
};
resume_return_prompt_copied: { action: "accept" | "decline" | "cancel" };
// ── MCP install / onboarding ─────────────────────────────────────────────
mcp_install_command_copied: {
transport: "http" | "stdio";
elicitation_mode?: string;
surface: "integrations" | "setup_mcp";
};
mcp_install_transport_switched: { transport: "http" | "stdio" };
mcp_install_elicitation_mode_changed: { elicitation_mode: string };
mcp_install_artifacts_toggled: { artifacts: boolean };
mcp_install_search_tools_toggled: { search_tools: boolean };
// ── Command palette ──────────────────────────────────────────────────────
command_palette_navigated: {
kind: "integration" | "add_integration" | "preset";
plugin_key?: string;
};
// ── Docs / help ──────────────────────────────────────────────────────────
docs_opened: { surface: "sidebar" };
// ── Cloud: auth & onboarding ─────────────────────────────────────────────
login_cta_clicked: {};
signed_out: {};
org_created: { success: boolean };
org_deleted: { success: boolean };
org_switched: { success: boolean };
org_invitation_accepted: { success: boolean };
setup_mcp_completed: {};
setup_mcp_skipped: {};
// ── Cloud: billing & support ─────────────────────────────────────────────
billing_plan_selected: {
plan_id: string;
action: "activate" | "upgrade" | "downgrade";
};
billing_manage_opened: {};
billing_cancel_plan_clicked: { plan_id: string };
support_opened: {};
support_link_clicked: { label: string };
org_domain_added: { success: boolean };
org_domain_removed: { success: boolean };
}
export type AnalyticsEventName = keyof AnalyticsEvents;
/**
* The host-supplied sink. Cloud backs this with `posthog.capture`; hosts that
* mount no provider get the no-op default.
*/
export type AnalyticsClient = <Name extends AnalyticsEventName>(
name: Name,
properties: AnalyticsEvents[Name],
) => void;
let currentAnalyticsClient: AnalyticsClient | null = null;
/**
* Imperative injection point — what `AnalyticsProvider` uses, and the hook for
* non-React hosts (or tests). Pass `null` to restore the no-op default.
*/
export const setAnalyticsClient = (client: AnalyticsClient | null): void => {
currentAnalyticsClient = client;
};
/**
* Record one product event. Safe to call from anywhere (React handlers,
* Effect callbacks, plain modules); a host without a mounted client makes
* this a no-op. Events with no properties may omit the second argument.
*/
export const trackEvent = <Name extends AnalyticsEventName>(
name: Name,
...rest: {} extends AnalyticsEvents[Name]
? [properties?: AnalyticsEvents[Name]]
: [properties: AnalyticsEvents[Name]]
): void => {
currentAnalyticsClient?.(name, rest[0] ?? ({} as AnalyticsEvents[Name]));
};
/**
* Declarative mount for React hosts — sets the module-level client during
* render, exactly like `FrontendErrorReporterProvider` does for error
* reporting. Mount once at the app root, ABOVE any tree that fires events
* (in cloud that is the document root, not ExecutorProvider, because the
* login/onboarding routes render outside the authenticated shell).
*/
export const AnalyticsProvider = (props: React.PropsWithChildren<{ client?: AnalyticsClient }>) => {
currentAnalyticsClient = props.client ?? null;
return <>{props.children}</>;
};