11import * as Sentry from "@sentry/react" ;
22import posthog from "posthog-js" ;
3+ import { DEFAULT_PROVIDERS } from "@/lib/ai/providers/types" ;
34
45let initialized = false ;
56
@@ -22,35 +23,75 @@ export function filterObservabilityBreadcrumb<T extends ConsoleBreadcrumb>(bread
2223 return isReactCaughtRenderError ? null : breadcrumb ;
2324}
2425
25- const PRODUCT_PROPERTY_KEYS = new Set ( [
26- "durationMs" ,
27- "errorName" ,
28- "feature" ,
29- "format" ,
30- "model" ,
31- "provider" ,
32- "roundIndex" ,
33- "speechCount" ,
34- "statusCode" ,
35- "streaming" ,
36- "tool" ,
37- "winner" ,
26+ const PRODUCT_EVENT_NAMES = new Set ( [
27+ "ai_request_failed" ,
28+ "ai_request_started" ,
29+ "ai_request_succeeded" ,
30+ "content_exported" ,
31+ "sparring_round_completed" ,
32+ "sparring_session_judged" ,
33+ "tool_opened" ,
3834] ) ;
3935
36+ const STRING_PROPERTY_VALUES : Record < string , ReadonlySet < string > > = {
37+ errorName : new Set ( [
38+ "AIAuthError" ,
39+ "AIModelRestrictionError" ,
40+ "AIProviderError" ,
41+ "AIRateLimitError" ,
42+ "AIResponseError" ,
43+ "AISubscriptionError" ,
44+ "Error" ,
45+ "TimeoutError" ,
46+ "TypeError" ,
47+ "UnknownError" ,
48+ ] ) ,
49+ feature : new Set ( [ "ask" , "attack" , "brainstorm" , "crossfire" , "factoids" , "generate" , "sparring" ] ) ,
50+ format : new Set ( [ "docx" , "google_docs" , "ld" , "pdf" , "pf" , "standard" ] ) ,
51+ model : new Set ( DEFAULT_PROVIDERS . flatMap ( ( provider ) => provider . models . map ( ( model ) => model . id ) ) ) ,
52+ provider : new Set ( DEFAULT_PROVIDERS . map ( ( provider ) => provider . id ) ) ,
53+ tool : new Set ( [
54+ "admin" ,
55+ "ask" ,
56+ "attack" ,
57+ "brainstorm" ,
58+ "crossfire" ,
59+ "factoids" ,
60+ "flowchart" ,
61+ "generate" ,
62+ "history" ,
63+ "library" ,
64+ "settings" ,
65+ "sparring" ,
66+ "timer" ,
67+ ] ) ,
68+ winner : new Set ( [ "ai" , "tie" , "user" ] ) ,
69+ } ;
70+
4071type TelemetryAttribute = string | number | boolean ;
4172
73+ /** Retains only bounded values from the product telemetry schema. */
4274function safeProductProperties ( properties ?: Record < string , unknown > ) : Record < string , TelemetryAttribute > {
4375 if ( ! properties ) return { } ;
4476
4577 const safeProperties : Record < string , TelemetryAttribute > = { } ;
4678 for ( const [ key , value ] of Object . entries ( properties ) ) {
47- if ( ! PRODUCT_PROPERTY_KEYS . has ( key ) ) continue ;
48- if ( typeof value === "string" || typeof value === "boolean" ) safeProperties [ key ] = value ;
49- if ( typeof value === "number" && Number . isFinite ( value ) ) safeProperties [ key ] = value ;
79+ if ( typeof value === "string" && STRING_PROPERTY_VALUES [ key ] ?. has ( value ) ) safeProperties [ key ] = value ;
80+ if ( key === "streaming" && typeof value === "boolean" ) safeProperties [ key ] = value ;
81+ if ( key === "durationMs" && typeof value === "number" && Number . isFinite ( value ) && value >= 0 && value <= 3_600_000 ) {
82+ safeProperties [ key ] = value ;
83+ }
84+ if ( ( key === "roundIndex" || key === "speechCount" ) && Number . isInteger ( value ) && Number ( value ) >= 0 && Number ( value ) <= 100 ) {
85+ safeProperties [ key ] = Number ( value ) ;
86+ }
87+ if ( key === "statusCode" && Number . isInteger ( value ) && Number ( value ) >= 100 && Number ( value ) <= 599 ) {
88+ safeProperties [ key ] = Number ( value ) ;
89+ }
5090 }
5191 return safeProperties ;
5292}
5393
94+ /** Initializes configured observability SDKs once with explicit privacy controls. */
5495export function initializeObservability ( ) : void {
5596 if ( initialized ) return ;
5697 initialized = true ;
@@ -100,7 +141,9 @@ export function initializeObservability(): void {
100141 }
101142}
102143
144+ /** Reports a named product event only when its name and properties match the closed schema. */
103145export function captureProductEvent ( name : string , properties ?: Record < string , unknown > ) : void {
146+ if ( ! PRODUCT_EVENT_NAMES . has ( name ) ) return ;
104147 const safeProperties = safeProductProperties ( properties ) ;
105148
106149 if ( import . meta. env . VITE_POSTHOG_KEY ) {
@@ -128,6 +171,7 @@ export function captureProductEvent(name: string, properties?: Record<string, un
128171 }
129172}
130173
174+ /** Reports a safely constructed exception without allowing telemetry failures into product flows. */
131175export function captureException ( error : unknown , context ?: Record < string , unknown > ) : void {
132176 if ( ! import . meta. env . VITE_SENTRY_DSN ) return ;
133177
0 commit comments