Skip to content

Commit a19c282

Browse files
1 parent 3af496a commit a19c282

2 files changed

Lines changed: 159 additions & 30 deletions

File tree

Source/Workbench/Electron/OTELBridge.ts

Lines changed: 62 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -81,42 +81,79 @@ const Flush = (): void => {
8181
scopeSpans: [
8282
{
8383
scope: { name: "land.otel.bridge", version: "1.0.0" },
84-
spans: Spans.map((S) => ({
85-
traceId: TraceId,
86-
spanId: S.SpanId,
87-
name: S.Name,
88-
kind: 1, // INTERNAL
89-
startTimeUnixNano: S.StartTimeUnixNano,
90-
endTimeUnixNano: S.EndTimeUnixNano,
91-
attributes: S.Detail
84+
spans: Spans.map((S) => {
85+
const IsError = S.Name.includes("error");
86+
const DetailObj = S.Detail as Record<string, unknown> | undefined;
87+
const Attributes = S.Detail
9288
? Object.entries(S.Detail).map(
9389
([K, V]) => ({
9490
key: K,
95-
value: { stringValue: String(V) },
91+
value: { stringValue: String(V).slice(0, 500) },
9692
}),
9793
)
98-
: [],
99-
status: S.Name.includes("error")
100-
? { code: 2 } // ERROR
101-
: { code: 1 }, // OK
102-
})),
94+
: [];
95+
const Events = IsError
96+
? [{
97+
name: "exception",
98+
timeUnixNano: S.StartTimeUnixNano,
99+
attributes: [
100+
{ key: "exception.type", value: { stringValue: S.Name } },
101+
{ key: "exception.message", value: { stringValue: String(DetailObj?.message || S.Name).slice(0, 500) } },
102+
],
103+
}]
104+
: [];
105+
return {
106+
traceId: TraceId,
107+
spanId: S.SpanId,
108+
name: S.Name,
109+
kind: 1,
110+
startTimeUnixNano: S.StartTimeUnixNano,
111+
endTimeUnixNano: S.EndTimeUnixNano,
112+
attributes: Attributes,
113+
events: Events,
114+
status: IsError
115+
? { code: 2, message: String(DetailObj?.message || "").slice(0, 200) }
116+
: { code: 1 },
117+
};
118+
}),
103119
},
104120
],
105121
},
106122
],
107123
};
108124

109-
// sendBeacon avoids CORS preflight - fire-and-forget, no OPTIONS request.
110-
// Content-Type is set to text/plain by sendBeacon which bypasses CORS,
111-
// but OTLP/HTTP accepts JSON regardless of Content-Type header.
112-
try {
113-
const Queued = navigator.sendBeacon(
114-
OTLPEndpoint,
115-
new Blob([JSON.stringify(Payload)], { type: "application/json" }),
116-
);
117-
if (!Queued) CollectorAvailable = false;
118-
} catch {
119-
CollectorAvailable = false;
125+
// Send via fetch (keepalive) to avoid CORS preflight.
126+
// Split into chunks of 20 spans max to stay under the 64KB keepalive limit.
127+
const AllSpans = Payload.resourceSpans[0].scopeSpans[0].spans;
128+
const ChunkSize = 20;
129+
130+
for (let I = 0; I < AllSpans.length; I += ChunkSize) {
131+
const ChunkPayload = {
132+
resourceSpans: [
133+
{
134+
...Payload.resourceSpans[0],
135+
scopeSpans: [
136+
{
137+
...Payload.resourceSpans[0].scopeSpans[0],
138+
spans: AllSpans.slice(I, I + ChunkSize),
139+
},
140+
],
141+
},
142+
],
143+
};
144+
145+
try {
146+
fetch(OTLPEndpoint, {
147+
method: "POST",
148+
body: JSON.stringify(ChunkPayload),
149+
headers: { "Content-Type": "application/json" },
150+
keepalive: true,
151+
}).catch(() => {
152+
CollectorAvailable = false;
153+
});
154+
} catch {
155+
CollectorAvailable = false;
156+
}
120157
}
121158
};
122159

Source/Workbench/Electron/PostHogBridge.ts

Lines changed: 97 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -151,6 +151,7 @@ const Initialize = async (): Promise<void> => {
151151
$exception_source: Event.filename,
152152
$exception_lineno: Event.lineno,
153153
$exception_colno: Event.colno,
154+
$exception_origin: "window.onerror",
154155
},
155156
);
156157
});
@@ -159,14 +160,79 @@ const Initialize = async (): Promise<void> => {
159160
const Reason = Event.reason;
160161
if (!Reason) return;
161162
const Message = String(Reason.message || Reason);
163+
if (Message.includes("Canceled")) return;
164+
PH.captureException(
165+
Reason instanceof Error ? Reason : new Error(Message),
166+
{ $exception_origin: "unhandledrejection" },
167+
);
168+
});
169+
170+
// Intercept console.error to capture VS Code internal errors
171+
const OriginalConsoleError = console.error;
172+
let ConsoleErrorCount = 0;
173+
console.error = (...Args: unknown[]) => {
174+
OriginalConsoleError.apply(console, Args);
175+
ConsoleErrorCount++;
176+
const Message = Args.map(String).join(" ").slice(0, 500);
162177
if (
163178
Message.includes("Canceled") ||
164-
Message.includes("FileNotFound") ||
165-
Message.includes("No such file or directory")
179+
Message.includes("[PostHog.js]") ||
180+
Message.includes("sourceMappingURL")
166181
)
167182
return;
168-
PH.captureException(Reason instanceof Error ? Reason : new Error(Message));
169-
});
183+
try {
184+
performance.mark(`land:console:error`, {
185+
detail: { message: Message, count: ConsoleErrorCount },
186+
});
187+
} catch {}
188+
PH.captureException(new Error(Message), {
189+
$exception_origin: "console.error",
190+
$exception_count: ConsoleErrorCount,
191+
});
192+
};
193+
194+
// Intercept console.warn for VS Code warnings
195+
const OriginalConsoleWarn = console.warn;
196+
let ConsoleWarnCount = 0;
197+
console.warn = (...Args: unknown[]) => {
198+
OriginalConsoleWarn.apply(console, Args);
199+
ConsoleWarnCount++;
200+
const Message = Args.map(String).join(" ").slice(0, 500);
201+
try {
202+
performance.mark(`land:console:warn`, {
203+
detail: { message: Message, count: ConsoleWarnCount },
204+
});
205+
} catch {}
206+
};
207+
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.
170236

171237
// Capture boot timing
172238
window.addEventListener("load", () => {
@@ -183,10 +249,36 @@ const Initialize = async (): Promise<void> => {
183249
}
184250
});
185251

252+
// Capture resource loading errors (failed scripts, stylesheets, images)
253+
window.addEventListener(
254+
"error",
255+
(Event) => {
256+
const Target = Event.target as HTMLElement;
257+
if (Target && Target !== window && "src" in Target) {
258+
PH.capture("land:resource:error", {
259+
tag: Target.tagName,
260+
src: (Target as HTMLScriptElement).src?.slice(0, 200),
261+
});
262+
try {
263+
performance.mark(`land:resource:error`, {
264+
detail: {
265+
tag: Target.tagName,
266+
src: (Target as HTMLScriptElement).src?.slice(0, 200),
267+
},
268+
});
269+
} catch {}
270+
}
271+
},
272+
true, // Capture phase — catches resource errors that don't bubble
273+
);
274+
186275
// Flush on page hide
187276
addEventListener("visibilitychange", () => {
188277
if (document.visibilityState === "hidden") {
189-
PH.capture("land:session:end");
278+
PH.capture("land:session:end", {
279+
console_errors: ConsoleErrorCount,
280+
console_warns: ConsoleWarnCount,
281+
});
190282
}
191283
});
192284

0 commit comments

Comments
 (0)