@@ -63,6 +63,9 @@ export type TelemetryEvent =
6363 | "summarizer_failure"
6464 | "crash"
6565 | "auth_failure"
66+ | "auth_success"
67+ | "mcp_connect"
68+ | "mcp_oauth"
6669 // PostHog Surveys event name (space included). Intentional operator feedback
6770 // from /feedback — can ship when ambient product telemetry is off; still
6871 // blocked by env kill switches. See captureIntentional.
@@ -199,6 +202,15 @@ const EVENT_PROPERTY_ALLOWLIST: Record<TelemetryEvent, readonly string[]> = {
199202 // Which provider rejected the credentials, not why — the rejection detail is
200203 // provider-authored text and error_class means a JS constructor name.
201204 auth_failure : [ "auth_provider" ] ,
205+ // Which provider completed sign-in. Same enum as auth_failure, no message:
206+ // a success has no rejection detail to report.
207+ auth_success : [ "auth_provider" ] ,
208+ // Per-server connect outcome. transport is the stdio/http connect path and
209+ // result is the outcome enum — never the server name, URL, or error text.
210+ mcp_connect : [ "transport" , "result" ] ,
211+ // Per-attempt browser-OAuth outcome. result only — never the
212+ // authorization URL, state, server name, or error text.
213+ mcp_oauth : [ "result" ] ,
202214 // Intentional /feedback survey response (PostHog custom survey capture shape).
203215 // Free text is only sent because the operator typed it for that purpose.
204216 // turn_trace_id links to the last $ai_generation in this session when known.
@@ -226,7 +238,8 @@ export function aiSpansEnabled(env: NodeJS.ProcessEnv = process.env): boolean {
226238
227239/**
228240 * Sample rate for successful `$ai_generation` events (0–1). Default 1.0 (no
229- * drop). Errors (`$ai_is_error: true`), `crash`, and `auth_failure` always ship.
241+ * drop). Errors (`$ai_is_error: true`), `crash`, `auth_failure`,
242+ * `auth_success`, `mcp_connect`, and `mcp_oauth` always ship.
230243 */
231244export function generationSampleRate (
232245 env : NodeJS . ProcessEnv = process . env ,
@@ -529,3 +542,77 @@ export function createTelemetry(options: CreateTelemetryOptions): Telemetry {
529542 discard,
530543 } ;
531544}
545+
546+ // ---------------------------------------------------------------------------
547+ // Enum-only product events (CL-9017). These helpers live beside the schema
548+ // rather than the call sites so the values they can send stay visible next
549+ // to the allowlists that bound them: fixed enums in, nothing else out.
550+ // Sampling gates only `$ai_generation`, so every event here always ships.
551+ // ---------------------------------------------------------------------------
552+
553+ export type McpTransport = "stdio" | "http" ;
554+
555+ export type McpConnectOutcome = "ok" | "auth" | "timeout" | "fail" ;
556+
557+ export interface McpConnectReport {
558+ readonly transport : McpTransport ;
559+ readonly result : McpConnectOutcome ;
560+ }
561+
562+ // Structural subset of the connect result: the report carries the outcome
563+ // enum, never the server name or the error text it was classified from.
564+ export interface McpConnectClassificationInput {
565+ readonly ok : boolean ;
566+ readonly serverName ?: string ;
567+ readonly error ?: string ;
568+ readonly authPending ?: boolean ;
569+ }
570+
571+ const MCP_CONNECT_TIMEOUT_MESSAGE = / a b o r t | t i m e o u t | t i m e d o u t | e x p i r / i;
572+
573+ export function classifyMcpConnectOutcome (
574+ result : McpConnectClassificationInput ,
575+ ) : McpConnectOutcome {
576+ if ( result . ok ) return "ok" ;
577+ if ( result . authPending === true ) return "auth" ;
578+ // Substring match, not word match: AbortError surfaces as "The operation
579+ // was aborted", and only the timeout-vs-fail bucket rides on this — the
580+ // error text itself never ships.
581+ if (
582+ result . error !== undefined &&
583+ MCP_CONNECT_TIMEOUT_MESSAGE . test ( result . error )
584+ )
585+ return "timeout" ;
586+ return "fail" ;
587+ }
588+
589+ export function captureMcpConnect (
590+ telemetry : Telemetry ,
591+ report : McpConnectReport ,
592+ ) : void {
593+ telemetry . capture ( "mcp_connect" , {
594+ transport : report . transport ,
595+ result : report . result ,
596+ } ) ;
597+ }
598+
599+ export type McpOauthOutcome = "completed" | "cancelled" | "timeout" ;
600+
601+ // Only an explicit abort (aborted signal, AbortError) is "cancelled". Every
602+ // other browser-wait end without a code — wait expiry, attempt cap, denial,
603+ // teardown — is "timeout", so each attempt maps to exactly one outcome.
604+ export function classifyMcpOauthOutcome (
605+ error : unknown ,
606+ aborted : boolean ,
607+ ) : McpOauthOutcome {
608+ if ( aborted ) return "cancelled" ;
609+ if ( error instanceof Error && error . name === "AbortError" ) return "cancelled" ;
610+ return "timeout" ;
611+ }
612+
613+ export function captureMcpOauth (
614+ telemetry : Telemetry ,
615+ result : McpOauthOutcome ,
616+ ) : void {
617+ telemetry . capture ( "mcp_oauth" , { result } ) ;
618+ }
0 commit comments