11/**
2- * Build-baked PostHog analytics bridge (debug builds only) .
2+ * PostHog analytics bridge — semantic events by component .
33 *
44 * Guarded by import.meta.env.DEV — Vite dead-code-eliminates in production.
5- * Captures:
6- * - All land:* performance marks (same as OTELBridge but for PostHog)
7- * - Unhandled errors and rejections (error tracking)
8- * - Page lifecycle events (load, visibility)
9- * - Custom IPC command timing
105 *
11- * PostHog project: codeeditorland (debug only — no production telemetry).
12- * Uses posthog-js loaded from CDN to avoid bundling in production.
6+ * Event taxonomy (filterable in PostHog by $component):
7+ * land:exthost:* — Extension host lifecycle, activation, errors
8+ * land:cocoon:* — Cocoon sidecar gRPC, bootstrap, health
9+ * land:wind:* — Wind service layer, Effect-TS bootstrap
10+ * land:sky:* — Sky rendering, Astro, Workbench DOM
11+ * land:ipc:* — IPC channel calls and failures
12+ * land:vscode:* — VS Code workbench internals
13+ * land:console:* — Intercepted console.error/warn
14+ * land:resource:* — Failed script/image/CSS loads
15+ * land:boot:* — Boot timing, navigation performance
16+ * land:session:* — Session start/end
17+ *
18+ * All events carry $component for PostHog filtering.
19+ * Marks are batched per-component (max 10 per flush, 2s window).
20+ * Errors always sent immediately via captureException.
1321 */
1422
1523const PostHogAPIKey = "phc_mCwHy7LgvbnEqh6a2DyMiLUJcaZvmmj7JNmmpQzvr7mA" ;
1624const PostHogHost = "https://eu.i.posthog.com" ;
1725
18- // Load posthog-js from CDN — no npm dependency, tree-shaken in prod
1926const LoadPostHog = async ( ) : Promise < any > => {
2027 try {
21- // Check if already loaded (e.g. by another script)
2228 if ( ( window as any ) . posthog ) return ( window as any ) . posthog ;
23-
24- // Dynamic script injection — no bundler dependency
25- // Gracefully returns null if CSP blocks the CDN or network fails
2629 return await new Promise ( ( Resolve ) => {
2730 const Script = document . createElement ( "script" ) ;
2831 Script . src = "https://eu-assets.i.posthog.com/static/array.js" ;
@@ -54,67 +57,108 @@ const LoadPostHog = async (): Promise<any> => {
5457 Resolve ( null ) ;
5558 }
5659 } ;
57- Script . onerror = ( ) => Resolve ( null ) ; // CSP block or network fail — degrade silently
60+ Script . onerror = ( ) => Resolve ( null ) ;
5861 document . head . appendChild ( Script ) ;
5962 } ) ;
6063 } catch {
6164 return null ;
6265 }
6366} ;
6467
65- // Initialize PostHog and start capturing
68+ // Component mapping: mark prefix → PostHog $component value
69+ const ComponentMap : Record < string , string > = {
70+ exthost : "extension-host" ,
71+ cocoon : "cocoon" ,
72+ wind : "wind" ,
73+ sky : "sky" ,
74+ ipc : "ipc" ,
75+ vscode : "vscode" ,
76+ console : "vscode" ,
77+ resource : "sky" ,
78+ boot : "sky" ,
79+ session : "all" ,
80+ } ;
81+
82+ interface BufferedMark {
83+ Name : string ;
84+ Component : string ;
85+ Category : string ;
86+ Action : string ;
87+ TimestampMs : number ;
88+ DurationMs : number ;
89+ Detail : unknown ;
90+ }
91+
6692const Initialize = async ( ) : Promise < void > => {
6793 const PH = await LoadPostHog ( ) ;
6894 if ( ! PH ) return ;
6995
70- // Batch performance marks to avoid rate limiting.
71- // Collects marks for 2 seconds, then flushes as a single event.
72- let MarkBuffer : Array < {
73- Name : string ;
74- Category : string ;
75- Action : string ;
76- TimestampMs : number ;
77- DurationMs : number ;
78- Detail : unknown ;
79- } > = [ ] ;
80- let FlushTimer : ReturnType < typeof setTimeout > | null = null ;
96+ // Per-component buffers — flushed independently
97+ const Buffers = new Map < string , BufferedMark [ ] > ( ) ;
98+ const Timers = new Map < string , ReturnType < typeof setTimeout > > ( ) ;
99+ const MaxPerFlush = 10 ; // Stay well under 64KB per request
81100
82- const FlushMarks = ( ) => {
83- FlushTimer = null ;
84- if ( MarkBuffer . length === 0 ) return ;
101+ const FlushComponent = ( Component : string ) => {
102+ Timers . delete ( Component ) ;
103+ const Buffer = Buffers . get ( Component ) ;
104+ if ( ! Buffer || Buffer . length === 0 ) return ;
85105
86- const Marks = MarkBuffer ;
87- MarkBuffer = [ ] ;
106+ const Marks = Buffer . splice ( 0 ) ;
88107
89- PH . capture ( "land:boot_marks" , {
90- marks : Marks ,
91- mark_count : Marks . length ,
92- first_mark_ms : Marks [ 0 ] ?. TimestampMs ,
93- last_mark_ms : Marks [ Marks . length - 1 ] ?. TimestampMs ,
94- } ) ;
108+ // Split into chunks of MaxPerFlush
109+ for ( let I = 0 ; I < Marks . length ; I += MaxPerFlush ) {
110+ const Chunk = Marks . slice ( I , I + MaxPerFlush ) ;
111+ PH . capture ( `land:${ Component } :marks` , {
112+ $component : Component ,
113+ marks : Chunk ,
114+ mark_count : Chunk . length ,
115+ first_mark_ms : Chunk [ 0 ] ?. TimestampMs ,
116+ last_mark_ms : Chunk [ Chunk . length - 1 ] ?. TimestampMs ,
117+ } ) ;
118+ }
119+ } ;
120+
121+ const FlushAll = ( ) => {
122+ for ( const Component of Buffers . keys ( ) ) {
123+ FlushComponent ( Component ) ;
124+ }
125+ } ;
126+
127+ const BufferMark = ( Mark : BufferedMark ) => {
128+ const Component = Mark . Component ;
129+ if ( ! Buffers . has ( Component ) ) Buffers . set ( Component , [ ] ) ;
130+ Buffers . get ( Component ) ! . push ( Mark ) ;
131+
132+ if ( ! Timers . has ( Component ) ) {
133+ Timers . set ( Component , setTimeout ( ( ) => FlushComponent ( Component ) , 2000 ) ) ;
134+ }
95135 } ;
96136
137+ // PerformanceObserver — routes to component buffers
97138 const Observer = new PerformanceObserver ( ( List ) => {
98139 for ( const Entry of List . getEntries ( ) ) {
99140 if ( ! Entry . name . startsWith ( "land:" ) ) continue ;
100141
101- const IsError = Entry . name . includes ( "error" ) ;
102142 const Parts = Entry . name . split ( ":" ) ;
103143 const Category = Parts [ 1 ] || "unknown" ;
104144 const Action = Parts . slice ( 2 ) . join ( ":" ) ;
145+ const Component = ComponentMap [ Category ] || "all" ;
146+ const IsError = Entry . name . includes ( "error" ) ;
105147
106148 if ( IsError ) {
107- // Errors always sent immediately
149+ // Errors sent immediately with full component context
108150 PH . captureException ( new Error ( Entry . name ) , {
151+ $component : Component ,
109152 $exception_type : `land:${ Category } ` ,
110153 $exception_message : Action ,
154+ $exception_origin : "performance.mark" ,
111155 timestamp_ms : performance . timeOrigin + Entry . startTime ,
112156 detail : ( Entry as any ) . detail ,
113157 } ) ;
114158 } else {
115- // Buffer regular marks
116- MarkBuffer . push ( {
159+ BufferMark ( {
117160 Name : Entry . name ,
161+ Component,
118162 Category,
119163 Action,
120164 TimestampMs : performance . timeOrigin + Entry . startTime ,
@@ -124,30 +168,24 @@ const Initialize = async (): Promise<void> => {
124168 : 0 ,
125169 Detail : ( Entry as any ) . detail ,
126170 } ) ;
127-
128- if ( ! FlushTimer ) {
129- FlushTimer = setTimeout ( FlushMarks , 2000 ) ;
130- }
131171 }
132172 }
133173 } ) ;
134174
135175 Observer . observe ( { type : "mark" , buffered : true } ) ;
136176 Observer . observe ( { type : "measure" , buffered : true } ) ;
137177
138- // Flush remaining marks on page hide
139178 addEventListener ( "visibilitychange" , ( ) => {
140- if ( document . visibilityState === "hidden" && MarkBuffer . length > 0 ) {
141- FlushMarks ( ) ;
142- }
179+ if ( document . visibilityState === "hidden" ) FlushAll ( ) ;
143180 } ) ;
144181
145- // Capture unhandled errors
182+ // === Error capture: window.onerror ===
146183 window . addEventListener ( "error" , ( Event ) => {
147184 if ( ! Event . message || Event . message === "Script error." ) return ;
148185 PH . captureException (
149186 Event . error || new Error ( Event . message ) ,
150187 {
188+ $component : "vscode" ,
151189 $exception_source : Event . filename ,
152190 $exception_lineno : Event . lineno ,
153191 $exception_colno : Event . colno ,
@@ -156,18 +194,22 @@ const Initialize = async (): Promise<void> => {
156194 ) ;
157195 } ) ;
158196
197+ // === Error capture: unhandled promise rejections ===
159198 window . addEventListener ( "unhandledrejection" , ( Event ) => {
160199 const Reason = Event . reason ;
161200 if ( ! Reason ) return ;
162201 const Message = String ( Reason . message || Reason ) ;
163202 if ( Message . includes ( "Canceled" ) ) return ;
164203 PH . captureException (
165204 Reason instanceof Error ? Reason : new Error ( Message ) ,
166- { $exception_origin : "unhandledrejection" } ,
205+ {
206+ $component : "vscode" ,
207+ $exception_origin : "unhandledrejection" ,
208+ } ,
167209 ) ;
168210 } ) ;
169211
170- // Intercept console.error to capture VS Code internal errors
212+ // === Error capture: console.error → PostHog + OTEL ===
171213 const OriginalConsoleError = console . error ;
172214 let ConsoleErrorCount = 0 ;
173215 console . error = ( ...Args : unknown [ ] ) => {
@@ -181,86 +223,64 @@ const Initialize = async (): Promise<void> => {
181223 )
182224 return ;
183225 try {
184- performance . mark ( ` land:console:error` , {
226+ performance . mark ( " land:console:error" , {
185227 detail : { message : Message , count : ConsoleErrorCount } ,
186228 } ) ;
187229 } catch { }
188230 PH . captureException ( new Error ( Message ) , {
231+ $component : "vscode" ,
189232 $exception_origin : "console.error" ,
190233 $exception_count : ConsoleErrorCount ,
191234 } ) ;
192235 } ;
193236
194- // Intercept console.warn for VS Code warnings
237+ // === Warning capture: console.warn → OTEL only ===
195238 const OriginalConsoleWarn = console . warn ;
196239 let ConsoleWarnCount = 0 ;
197240 console . warn = ( ...Args : unknown [ ] ) => {
198241 OriginalConsoleWarn . apply ( console , Args ) ;
199242 ConsoleWarnCount ++ ;
200243 const Message = Args . map ( String ) . join ( " " ) . slice ( 0 , 500 ) ;
201244 try {
202- performance . mark ( ` land:console:warn` , {
245+ performance . mark ( " land:console:warn" , {
203246 detail : { message : Message , count : ConsoleWarnCount } ,
204247 } ) ;
205248 } catch { }
206249 } ;
207250
208- // Hook into VS Code's error handler if available
209- const HookVSCodeErrors = ( ) => {
210- const OnUnexpectedError = ( window as any ) . _VSCODE_onUnexpectedError ;
211- if ( typeof OnUnexpectedError === "function" ) return ;
212-
213- // VS Code sets window.onerror and has its own error infrastructure.
214- // We hook via a global that the workbench checks after bootstrap.
215- ( window as any ) . _LAND_ERROR_HOOK = ( Error : unknown ) => {
216- const Message = Error instanceof Error
217- ? Error . message
218- : String ( Error ) ;
219- PH . captureException (
220- Error instanceof Error ? Error : new Error ( Message ) ,
221- { $exception_origin : "vscode.onUnexpectedError" } ,
222- ) ;
223- try {
224- performance . mark ( `land:vscode:error` , {
225- detail : { message : Message . slice ( 0 , 200 ) } ,
226- } ) ;
227- } catch { }
228- } ;
229- } ;
230- HookVSCodeErrors ( ) ;
231-
232- // Capture IPC failures via performance marks
233- // TauriMainProcessService already emits land:ipc:* marks for all calls.
234- // Errors are marked as land:ipc:*:error — already captured by the
235- // PerformanceObserver above.
236-
237- // Capture boot timing
238- window . addEventListener ( "load" , ( ) => {
239- const Navigation = performance . getEntriesByType (
240- "navigation" ,
241- ) [ 0 ] as PerformanceNavigationTiming ;
242- if ( Navigation ) {
243- PH . capture ( "land:boot:timing" , {
244- dom_interactive_ms : Navigation . domInteractive ,
245- dom_complete_ms : Navigation . domComplete ,
246- load_event_ms : Navigation . loadEventEnd ,
247- ttfb_ms : Navigation . responseStart - Navigation . requestStart ,
251+ // === VS Code error hook ===
252+ ( window as any ) . _LAND_ERROR_HOOK = ( Error : unknown ) => {
253+ const Message =
254+ Error instanceof globalThis . Error ? Error . message : String ( Error ) ;
255+ PH . captureException (
256+ Error instanceof globalThis . Error
257+ ? Error
258+ : new globalThis . Error ( Message ) ,
259+ {
260+ $component : "vscode" ,
261+ $exception_origin : "vscode.onUnexpectedError" ,
262+ } ,
263+ ) ;
264+ try {
265+ performance . mark ( "land:vscode:error" , {
266+ detail : { message : Message . slice ( 0 , 200 ) } ,
248267 } ) ;
249- }
250- } ) ;
268+ } catch { }
269+ } ;
251270
252- // Capture resource loading errors (failed scripts, stylesheets, images)
271+ // === Resource load failures (capture phase) ===
253272 window . addEventListener (
254273 "error" ,
255274 ( Event ) => {
256275 const Target = Event . target as HTMLElement ;
257276 if ( Target && Target !== window && "src" in Target ) {
258277 PH . capture ( "land:resource:error" , {
278+ $component : "sky" ,
259279 tag : Target . tagName ,
260280 src : ( Target as HTMLScriptElement ) . src ?. slice ( 0 , 200 ) ,
261281 } ) ;
262282 try {
263- performance . mark ( ` land:resource:error` , {
283+ performance . mark ( " land:resource:error" , {
264284 detail : {
265285 tag : Target . tagName ,
266286 src : ( Target as HTMLScriptElement ) . src ?. slice ( 0 , 200 ) ,
@@ -269,20 +289,37 @@ const Initialize = async (): Promise<void> => {
269289 } catch { }
270290 }
271291 } ,
272- true , // Capture phase — catches resource errors that don't bubble
292+ true ,
273293 ) ;
274294
275- // Flush on page hide
295+ // === Boot timing ===
296+ window . addEventListener ( "load" , ( ) => {
297+ const Navigation = performance . getEntriesByType (
298+ "navigation" ,
299+ ) [ 0 ] as PerformanceNavigationTiming ;
300+ if ( Navigation ) {
301+ PH . capture ( "land:boot:timing" , {
302+ $component : "sky" ,
303+ dom_interactive_ms : Navigation . domInteractive ,
304+ dom_complete_ms : Navigation . domComplete ,
305+ load_event_ms : Navigation . loadEventEnd ,
306+ ttfb_ms : Navigation . responseStart - Navigation . requestStart ,
307+ } ) ;
308+ }
309+ } ) ;
310+
311+ // === Session lifecycle ===
276312 addEventListener ( "visibilitychange" , ( ) => {
277313 if ( document . visibilityState === "hidden" ) {
278314 PH . capture ( "land:session:end" , {
315+ $component : "all" ,
279316 console_errors : ConsoleErrorCount ,
280317 console_warns : ConsoleWarnCount ,
281318 } ) ;
282319 }
283320 } ) ;
284321
285- PH . capture ( "land:session:start" ) ;
322+ PH . capture ( "land:session:start" , { $component : "all" } ) ;
286323} ;
287324
288325if ( import . meta. env . DEV ) {
0 commit comments