251 lines · 10.8 KB · TypeScript
RawDownload
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 ─────────────────────────────────────────────────────────  /** The full-page picker was opened. Replaces the connect dialog, whose   *  `integration_connect_dialog_opened` this supersedes — keep both readable   *  in dashboards spanning the change. */  integration_browse_opened: { via: "header" | "empty-state" | "sidebar" };  integration_detect_submitted: {    success: boolean;    detected_kind?: string;    confidence?: string;  };  /** Thumbs on the AI-generated credential guidance panel — the accuracy   *  signal for the registry's machine-written setup text. */  credential_guidance_rated: {    domain: string;    credential_label: string;    vote: "up" | "down";  };  integration_add_started: {    plugin_key: string;    via: "detect" | "manual" | "preset" | "command_palette" | "catalog";    preset_id?: string;    catalog_domain?: 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;    flow: "byo" | "dcr" | "cimd";    success: boolean;    dcr_fallback?: 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 };  mcp_install_tool_mode_changed: { tool_mode: "codemode" | "passthrough" };   // ── 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}</>;};