mirror of
https://github.com/lightpanda-io/browser.git
synced 2026-07-30 09:16:07 -04:00
Merge pull request #3063 from lightpanda-io/js-error-metrics
metrics: Add JS error count
This commit is contained in:
@@ -28,6 +28,7 @@ cdp_commands: Counter = .{},
|
||||
cdp_unknown_commands: Counter = .{},
|
||||
js_heap_limits: Counter = .{},
|
||||
script_errors: Counter = .{},
|
||||
js_errors: CounterEnum("kind", enum { js_exception, other }) = .{},
|
||||
arena_hit: CounterEnum("size", @import("ArenaPool.zig").BucketSize) = .{},
|
||||
arena_miss: CounterEnum("size", @import("ArenaPool.zig").BucketSize) = .{},
|
||||
navigate: CounterEnum("type", @import("telemetry/telemetry.zig").Event.Navigate.Context) = .{},
|
||||
@@ -82,6 +83,7 @@ const help = .{
|
||||
.cdp_unknown_commands = "CDP commands rejected for an unknown domain or method",
|
||||
.js_heap_limits = "Pages terminated for reaching the V8 heap limit",
|
||||
.script_errors = "Scripts that failed to evaluate, e.g. an uncaught top-level exception",
|
||||
.js_errors = "Uncaught JS errors (script exceptions, listener/callback throws, unhandled promise rejections); kind=js_exception is a thrown JS value, other is an internal failure (e.g. compilation error, terminated execution)",
|
||||
.arena_hit = "Arena pool acquisitions served from the free list",
|
||||
.arena_miss = "Arena pool acquisitions that had to allocate a new arena",
|
||||
.navigate = "Navigations by initiating frame type",
|
||||
|
||||
@@ -352,6 +352,7 @@ fn dispatchNode(self: *EventManager, target: *Node, event: *Event, comptime opts
|
||||
// don't propagate" rule as addEventListener listeners — see Listener.run.
|
||||
var caught: js.TryCatch.Caught = undefined;
|
||||
const handler_return: ?js.Value = ls.toLocal(inline_handler).tryCallWithThis(js.Value, target_et, .{event}, &caught) catch |err| ret: {
|
||||
frame._page.recordJsError(err);
|
||||
log.warn(.event, "inline handler", .{ .err = err, .caught = caught });
|
||||
break :ret null;
|
||||
};
|
||||
@@ -408,6 +409,7 @@ fn dispatchNode(self: *EventManager, target: *Node, event: *Event, comptime opts
|
||||
|
||||
var caught: js.TryCatch.Caught = undefined;
|
||||
const handler_return: ?js.Value = ls.toLocal(inline_handler).tryCallWithThis(js.Value, current_target, .{event}, &caught) catch |err| ret: {
|
||||
frame._page.recordJsError(err);
|
||||
log.warn(.event, "inline handler", .{ .err = err, .caught = caught });
|
||||
break :ret null;
|
||||
};
|
||||
|
||||
@@ -290,6 +290,7 @@ pub fn dispatchDirect(
|
||||
event._current_target = target;
|
||||
var caught: js.TryCatch.Caught = undefined;
|
||||
_ = func.tryCallWithThis(void, target, .{event}, &caught) catch |err| {
|
||||
page.recordJsError(err);
|
||||
if (err == error.JsException) {
|
||||
event._listeners_did_throw = true;
|
||||
} else {
|
||||
@@ -453,6 +454,7 @@ pub const Listener = struct {
|
||||
.string => |string| {
|
||||
const str = try arena.dupeZ(u8, string.str());
|
||||
local.eval(str, null) catch |err| {
|
||||
local.ctx.page.recordJsError(err);
|
||||
if (err == error.JsException) {
|
||||
event._listeners_did_throw = true;
|
||||
} else {
|
||||
@@ -517,7 +519,8 @@ pub const Listener = struct {
|
||||
.frame => |frame| frame.window.reportError(exc, frame) catch |err| {
|
||||
log.warn(.event, "listener report error", .{ .err = err });
|
||||
},
|
||||
.worker => {},
|
||||
// No worker error-event plumbing here (yet); still count it.
|
||||
.worker => local.ctx.page.recordJsError(error.JsException),
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -64,6 +64,10 @@ dom_version: usize = 0,
|
||||
// when it was called
|
||||
broadcast_sequence: u64 = 0,
|
||||
|
||||
// Uncaught JS errors attributed to this Page (all of its frames and workers):
|
||||
// Not exhaustive: some swallowed-callback paths aren't routed here.
|
||||
js_error_count: usize = 0,
|
||||
|
||||
// DOM object factory scoped to this Page's documents.
|
||||
factory: Factory,
|
||||
|
||||
@@ -228,6 +232,11 @@ pub fn deinit(self: *Page) void {
|
||||
session.arena_pool.release(self.frame_arena);
|
||||
}
|
||||
|
||||
pub fn recordJsError(self: *Page, err: anyerror) void {
|
||||
self.js_error_count += 1;
|
||||
lp.metrics.js_errors.incr(if (err == error.JsException) .js_exception else .other);
|
||||
}
|
||||
|
||||
pub fn getArena(self: *Page, size_or_bucket: anytype, debug: []const u8) !Allocator {
|
||||
return self.session.getArena(size_or_bucket, debug);
|
||||
}
|
||||
@@ -393,3 +402,15 @@ fn appendFrameExecutions(frame: *Frame, origin: []const u8, arena: Allocator, li
|
||||
try appendFrameExecutions(child, origin, arena, list);
|
||||
}
|
||||
}
|
||||
|
||||
const testing = @import("../testing.zig");
|
||||
|
||||
test "Page: js_error_count" {
|
||||
defer testing.reset();
|
||||
// One uncaught top-level script exception, one uncaught timer-callback
|
||||
// exception.
|
||||
const page = try testing.pageTest("page_js_error.html", .{});
|
||||
defer page.close();
|
||||
|
||||
try testing.expectEqual(2, page.frame().?._page.js_error_count);
|
||||
}
|
||||
|
||||
9
src/browser/tests/page_js_error.html
Normal file
9
src/browser/tests/page_js_error.html
Normal file
@@ -0,0 +1,9 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<body>
|
||||
<!-- Fixture for the Page.js_error_count test: one uncaught top-level
|
||||
exception and one uncaught timer-callback exception. -->
|
||||
<script>throw new Error("boom");</script>
|
||||
<script>setTimeout(() => { throw new Error("boom timer"); }, 1);</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -293,6 +293,7 @@ fn loadInitialScript(self: *SharedWorkerGlobalScope, script: []const u8) !void {
|
||||
return;
|
||||
}
|
||||
|
||||
js_context.page.recordJsError(err);
|
||||
const caught = try_catch.caughtOrError(self._script_arena.?, err);
|
||||
log.err(.browser, "shared worker script error", .{ .url = self._url, .caught = caught });
|
||||
return;
|
||||
@@ -302,6 +303,7 @@ fn loadInitialScript(self: *SharedWorkerGlobalScope, script: []const u8) !void {
|
||||
return;
|
||||
}
|
||||
|
||||
js_context.page.recordJsError(err);
|
||||
const caught = try_catch.caughtOrError(self._script_arena.?, err);
|
||||
log.err(.browser, "shared worker module error", .{ .url = self._url, .caught = caught });
|
||||
return;
|
||||
|
||||
@@ -212,6 +212,7 @@ const ScheduleCallback = struct {
|
||||
.idle => {
|
||||
const IdleDeadline = @import("IdleDeadline.zig");
|
||||
ls.toLocal(self.cb).call(void, .{IdleDeadline{}}) catch |err| {
|
||||
self.exec.page.recordJsError(err);
|
||||
log.warn(.js, "idleCallback", .{ .name = self.name, .err = err });
|
||||
};
|
||||
},
|
||||
@@ -221,11 +222,13 @@ const ScheduleCallback = struct {
|
||||
.worker => |worker| worker._performance.now(),
|
||||
};
|
||||
ls.toLocal(self.cb).call(void, .{now}) catch |err| {
|
||||
self.exec.page.recordJsError(err);
|
||||
log.warn(.js, "RAF", .{ .name = self.name, .err = err });
|
||||
};
|
||||
},
|
||||
.normal => {
|
||||
ls.toLocal(self.cb).call(void, self.params) catch |err| {
|
||||
self.exec.page.recordJsError(err);
|
||||
log.warn(.js, "timer", .{ .name = self.name, .err = err });
|
||||
};
|
||||
},
|
||||
|
||||
@@ -579,6 +579,8 @@ pub fn reportError(self: *Window, err: js.Value, frame: *Frame) !void {
|
||||
return;
|
||||
}
|
||||
|
||||
frame._page.recordJsError(error.JsException);
|
||||
|
||||
const target = self.asEventTarget();
|
||||
if (!frame._event_manager.hasDirectListeners(target, "error", self._on_error)) {
|
||||
if (comptime builtin.is_test == false) {
|
||||
@@ -1031,6 +1033,10 @@ pub fn unhandledPromiseRejection(self: *Window, no_handler: bool, rejection: js.
|
||||
break :blk .{ "rejectionhandled", self._on_rejection_handled };
|
||||
};
|
||||
|
||||
if (no_handler) {
|
||||
frame._page.recordJsError(error.JsException);
|
||||
}
|
||||
|
||||
const target = self.asEventTarget();
|
||||
if (frame._event_manager.hasDirectListeners(target, event_name, attribute_callback)) {
|
||||
const event = (try @import("event/PromiseRejectionEvent.zig").init(event_name, .{
|
||||
|
||||
@@ -241,6 +241,7 @@ fn loadInitialScript(self: *Worker, script: []const u8) !void {
|
||||
return;
|
||||
}
|
||||
|
||||
js_context.page.recordJsError(err);
|
||||
const caught = try_catch.caughtOrError(self._script_arena.?, err);
|
||||
log.err(.browser, "worker script error", .{ .url = self._url, .caught = caught });
|
||||
self.fireErrorEvent(caught.exception orelse @errorName(err), null);
|
||||
@@ -251,6 +252,7 @@ fn loadInitialScript(self: *Worker, script: []const u8) !void {
|
||||
return;
|
||||
}
|
||||
|
||||
js_context.page.recordJsError(err);
|
||||
const caught = try_catch.caughtOrError(self._script_arena.?, err);
|
||||
log.err(.browser, "worker module error", .{ .url = self._url, .caught = caught });
|
||||
self.fireErrorEvent(caught.exception orelse @errorName(err), null);
|
||||
|
||||
@@ -360,6 +360,10 @@ pub fn unhandledPromiseRejection(self: *WorkerGlobalScope, no_handler: bool, rej
|
||||
break :blk .{ "rejectionhandled", self._on_rejection_handled };
|
||||
};
|
||||
|
||||
if (no_handler) {
|
||||
self._page.recordJsError(error.JsException);
|
||||
}
|
||||
|
||||
const target = self.asEventTarget();
|
||||
if (self._event_manager.hasDirectListeners(target, event_name, attribute_callback)) {
|
||||
const event = (try @import("event/PromiseRejectionEvent.zig").init(event_name, .{
|
||||
@@ -428,6 +432,7 @@ fn importScript(self: *WorkerGlobalScope, arena: Allocator, url: [:0]const u8) !
|
||||
defer try_catch.deinit();
|
||||
|
||||
_ = ls.local.eval(response.body.items, url) catch |err| {
|
||||
self._page.recordJsError(err);
|
||||
const caught = try_catch.caughtOrError(arena, err);
|
||||
log.err(.browser, "importScript", .{ .url = resolved_url, .caught = caught });
|
||||
return;
|
||||
@@ -437,6 +442,8 @@ fn importScript(self: *WorkerGlobalScope, arena: Allocator, url: [:0]const u8) !
|
||||
}
|
||||
|
||||
pub fn reportError(self: *WorkerGlobalScope, err: JS.Value) !void {
|
||||
self._page.recordJsError(error.JsException);
|
||||
|
||||
const error_event = try ErrorEvent.initTrusted(comptime .wrap("error"), .{
|
||||
.@"error" = try err.persist(),
|
||||
.message = err.toStringSlice() catch "Unknown error",
|
||||
|
||||
@@ -247,7 +247,8 @@ pub fn forEach(self: *DOMTokenList, cb_: js.Function, js_this_: ?js.Object, fram
|
||||
continue;
|
||||
}
|
||||
var caught: js.TryCatch.Caught = undefined;
|
||||
cb.tryCall(void, .{ token, i, self }, &caught) catch {
|
||||
cb.tryCall(void, .{ token, i, self }, &caught) catch |err| {
|
||||
frame._page.recordJsError(err);
|
||||
log.debug(.js, "forEach callback", .{ .caught = caught, .source = "DOMTokenList" });
|
||||
return;
|
||||
};
|
||||
|
||||
@@ -98,7 +98,8 @@ pub fn forEach(self: *NodeList, cb: js.Function, frame: *Frame) !void {
|
||||
const node = try self.getAtIndex(@intCast(i), frame) orelse return;
|
||||
|
||||
var caught: js.TryCatch.Caught = undefined;
|
||||
cb.tryCall(void, .{ node, i, self }, &caught) catch {
|
||||
cb.tryCall(void, .{ node, i, self }, &caught) catch |err| {
|
||||
frame._page.recordJsError(err);
|
||||
log.debug(.js, "forEach callback", .{ .caught = caught, .source = "nodelist" });
|
||||
return;
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user