mirror of
https://github.com/lightpanda-io/browser.git
synced 2026-06-11 09:35:59 -04:00
Merge pull request #2150 from lightpanda-io/eventcounts
Add EventCounts API
This commit is contained in:
@@ -107,6 +107,9 @@ pub fn dispatchOpts(self: *EventManager, target: *EventTarget, event: *Event, co
|
||||
event.acquireRef();
|
||||
defer _ = event.releaseRef(self.page._session);
|
||||
|
||||
// Increment event count for Event Timing API
|
||||
self.page.window._performance._event_counts.increment(event._type_string.str());
|
||||
|
||||
if (comptime IS_DEBUG) {
|
||||
log.debug(.event, "eventManager.dispatch", .{ .type = event._type_string.str(), .bubbles = event._bubbles });
|
||||
}
|
||||
|
||||
@@ -859,6 +859,7 @@ pub const JsApis = flattenTypes(&.{
|
||||
@import("../webapi/URL.zig"),
|
||||
@import("../webapi/Window.zig"),
|
||||
@import("../webapi/Performance.zig"),
|
||||
@import("../webapi/EventCounts.zig"),
|
||||
@import("../webapi/PluginArray.zig"),
|
||||
@import("../webapi/MutationObserver.zig"),
|
||||
@import("../webapi/IntersectionObserver.zig"),
|
||||
|
||||
@@ -393,3 +393,141 @@
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<script id=eventCounts_exists>
|
||||
{
|
||||
const ec = performance.eventCounts;
|
||||
testing.expectEqual('object', typeof ec);
|
||||
testing.expectEqual(false, ec === null);
|
||||
testing.expectEqual(false, ec === undefined);
|
||||
}
|
||||
</script>
|
||||
|
||||
<script id=eventCounts_same_object>
|
||||
{
|
||||
// performance.eventCounts should return the same object on each access
|
||||
testing.expectEqual(true, performance.eventCounts === performance.eventCounts);
|
||||
}
|
||||
</script>
|
||||
|
||||
<script id=eventCounts_size>
|
||||
{
|
||||
// EventCounts tracks 36 event types per Event Timing spec
|
||||
const ec = performance.eventCounts;
|
||||
testing.expectEqual('number', typeof ec.size);
|
||||
testing.expectEqual(36, ec.size);
|
||||
}
|
||||
</script>
|
||||
|
||||
<script id=eventCounts_has_tracked_events>
|
||||
{
|
||||
const ec = performance.eventCounts;
|
||||
// These should be tracked
|
||||
testing.expectEqual(true, ec.has("click"));
|
||||
testing.expectEqual(true, ec.has("keydown"));
|
||||
testing.expectEqual(true, ec.has("keyup"));
|
||||
testing.expectEqual(true, ec.has("mousedown"));
|
||||
testing.expectEqual(true, ec.has("mouseup"));
|
||||
testing.expectEqual(true, ec.has("pointerdown"));
|
||||
testing.expectEqual(true, ec.has("input"));
|
||||
testing.expectEqual(true, ec.has("touchstart"));
|
||||
}
|
||||
</script>
|
||||
|
||||
<script id=eventCounts_has_non_tracked_events>
|
||||
{
|
||||
const ec = performance.eventCounts;
|
||||
// These should NOT be tracked
|
||||
testing.expectEqual(false, ec.has("load"));
|
||||
testing.expectEqual(false, ec.has("scroll"));
|
||||
testing.expectEqual(false, ec.has("resize"));
|
||||
testing.expectEqual(false, ec.has("DOMContentLoaded"));
|
||||
testing.expectEqual(false, ec.has("nonexistent"));
|
||||
}
|
||||
</script>
|
||||
|
||||
<script id=eventCounts_get_returns_number>
|
||||
{
|
||||
const ec = performance.eventCounts;
|
||||
// get() should return a number for tracked events
|
||||
testing.expectEqual('number', typeof ec.get("click"));
|
||||
testing.expectEqual('number', typeof ec.get("keydown"));
|
||||
}
|
||||
</script>
|
||||
|
||||
<script id=eventCounts_get_returns_zero_for_non_tracked>
|
||||
{
|
||||
const ec = performance.eventCounts;
|
||||
// get() should return 0 for non-tracked events (not undefined)
|
||||
testing.expectEqual(0, ec.get("load"));
|
||||
testing.expectEqual(0, ec.get("nonexistent"));
|
||||
}
|
||||
</script>
|
||||
|
||||
<script id=eventCounts_keys>
|
||||
{
|
||||
const ec = performance.eventCounts;
|
||||
const keys = Array.from(ec.keys());
|
||||
testing.expectEqual(36, keys.length);
|
||||
testing.expectEqual(true, keys.includes("click"));
|
||||
testing.expectEqual(true, keys.includes("keydown"));
|
||||
testing.expectEqual(true, keys.includes("mousedown"));
|
||||
}
|
||||
</script>
|
||||
|
||||
<script id=eventCounts_values>
|
||||
{
|
||||
const ec = performance.eventCounts;
|
||||
const values = Array.from(ec.values());
|
||||
testing.expectEqual(36, values.length);
|
||||
// All values should be numbers
|
||||
for (const v of values) {
|
||||
testing.expectEqual('number', typeof v);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<script id=eventCounts_entries>
|
||||
{
|
||||
const ec = performance.eventCounts;
|
||||
const entries = Array.from(ec.entries());
|
||||
testing.expectEqual(36, entries.length);
|
||||
// Each entry should be [string, number]
|
||||
for (const e of entries) {
|
||||
testing.expectEqual(true, Array.isArray(e));
|
||||
testing.expectEqual(2, e.length);
|
||||
testing.expectEqual('string', typeof e[0]);
|
||||
testing.expectEqual('number', typeof e[1]);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<script id=eventCounts_forEach>
|
||||
{
|
||||
const ec = performance.eventCounts;
|
||||
let count = 0;
|
||||
let sawClick = false;
|
||||
ec.forEach((value, key, obj) => {
|
||||
count++;
|
||||
testing.expectEqual('number', typeof value);
|
||||
testing.expectEqual('string', typeof key);
|
||||
testing.expectEqual(ec, obj);
|
||||
if (key === "click") sawClick = true;
|
||||
});
|
||||
testing.expectEqual(36, count);
|
||||
testing.expectEqual(true, sawClick);
|
||||
}
|
||||
</script>
|
||||
|
||||
<script id=eventCounts_iterator>
|
||||
{
|
||||
const ec = performance.eventCounts;
|
||||
let count = 0;
|
||||
for (const [key, value] of ec) {
|
||||
count++;
|
||||
testing.expectEqual('string', typeof key);
|
||||
testing.expectEqual('number', typeof value);
|
||||
}
|
||||
testing.expectEqual(36, count);
|
||||
}
|
||||
</script>
|
||||
|
||||
172
src/browser/webapi/EventCounts.zig
Normal file
172
src/browser/webapi/EventCounts.zig
Normal file
@@ -0,0 +1,172 @@
|
||||
// Copyright (C) 2023-2026 Lightpanda (Selecy SAS)
|
||||
//
|
||||
// Francis Bouvier <francis@lightpanda.io>
|
||||
// Pierre Tachoire <pierre@lightpanda.io>
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as
|
||||
// published by the Free Software Foundation, either version 3 of the
|
||||
// License, or (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
const std = @import("std");
|
||||
const js = @import("../js/js.zig");
|
||||
const Page = @import("../Page.zig");
|
||||
const log = @import("../../log.zig");
|
||||
|
||||
pub fn registerTypes() []const type {
|
||||
return &.{
|
||||
EventCounts,
|
||||
KeyIterator,
|
||||
ValueIterator,
|
||||
EntryIterator,
|
||||
};
|
||||
}
|
||||
|
||||
const EventCounts = @This();
|
||||
|
||||
// Event types tracked per the Event Timing spec
|
||||
// https://w3c.github.io/event-timing/#sec-events-exposed
|
||||
const tracked_event_types = [_][]const u8{
|
||||
"auxclick",
|
||||
"click",
|
||||
"contextmenu",
|
||||
"dblclick",
|
||||
"mousedown",
|
||||
"mouseenter",
|
||||
"mouseleave",
|
||||
"mouseout",
|
||||
"mouseover",
|
||||
"mouseup",
|
||||
"pointerover",
|
||||
"pointerenter",
|
||||
"pointerdown",
|
||||
"pointerup",
|
||||
"pointercancel",
|
||||
"pointerout",
|
||||
"pointerleave",
|
||||
"gotpointercapture",
|
||||
"lostpointercapture",
|
||||
"touchstart",
|
||||
"touchend",
|
||||
"touchcancel",
|
||||
"keydown",
|
||||
"keypress",
|
||||
"keyup",
|
||||
"beforeinput",
|
||||
"input",
|
||||
"compositionstart",
|
||||
"compositionupdate",
|
||||
"compositionend",
|
||||
"dragstart",
|
||||
"dragend",
|
||||
"dragenter",
|
||||
"dragleave",
|
||||
"dragover",
|
||||
"drop",
|
||||
};
|
||||
|
||||
// Counts stored in a fixed array
|
||||
_counts: [tracked_event_types.len]u32 = [_]u32{0} ** tracked_event_types.len,
|
||||
|
||||
pub fn increment(self: *EventCounts, event_type: []const u8) void {
|
||||
if (getIndex(event_type)) |idx| {
|
||||
self._counts[idx] +|= 1;
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get(self: *const EventCounts, event_type: []const u8) u32 {
|
||||
if (getIndex(event_type)) |idx| {
|
||||
return self._counts[idx];
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
pub fn has(_: *const EventCounts, event_type: []const u8) bool {
|
||||
return getIndex(event_type) != null;
|
||||
}
|
||||
|
||||
pub fn getSize(_: *const EventCounts) u32 {
|
||||
return tracked_event_types.len;
|
||||
}
|
||||
|
||||
pub fn keys(self: *EventCounts, page: *Page) !*KeyIterator {
|
||||
return .init(.{ .event_counts = self }, page);
|
||||
}
|
||||
|
||||
pub fn values(self: *EventCounts, page: *Page) !*ValueIterator {
|
||||
return .init(.{ .event_counts = self }, page);
|
||||
}
|
||||
|
||||
pub fn entries(self: *EventCounts, page: *Page) !*EntryIterator {
|
||||
return .init(.{ .event_counts = self }, page);
|
||||
}
|
||||
|
||||
pub fn forEach(self: *EventCounts, cb_: js.Function, js_this_: ?js.Object) !void {
|
||||
const cb = if (js_this_) |js_this| try cb_.withThis(js_this) else cb_;
|
||||
|
||||
for (tracked_event_types, self._counts) |event_type, count| {
|
||||
var caught: js.TryCatch.Caught = undefined;
|
||||
cb.tryCall(void, .{ count, event_type, self }, &caught) catch {
|
||||
log.debug(.js, "forEach callback", .{ .caught = caught, .source = "EventCounts" });
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
fn getIndex(event_type: []const u8) ?usize {
|
||||
for (tracked_event_types, 0..) |t, i| {
|
||||
if (std.mem.eql(u8, t, event_type)) {
|
||||
return i;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
pub const JsApi = struct {
|
||||
pub const bridge = js.Bridge(EventCounts);
|
||||
|
||||
pub const Meta = struct {
|
||||
pub const name = "EventCounts";
|
||||
pub const prototype_chain = bridge.prototypeChain();
|
||||
pub var class_id: bridge.ClassId = undefined;
|
||||
};
|
||||
|
||||
pub const get = bridge.function(EventCounts.get, .{});
|
||||
pub const has = bridge.function(EventCounts.has, .{});
|
||||
pub const size = bridge.accessor(EventCounts.getSize, null, .{});
|
||||
pub const keys = bridge.function(EventCounts.keys, .{});
|
||||
pub const values = bridge.function(EventCounts.values, .{});
|
||||
pub const entries = bridge.function(EventCounts.entries, .{});
|
||||
pub const forEach = bridge.function(EventCounts.forEach, .{});
|
||||
pub const symbol_iterator = bridge.iterator(EventCounts.entries, .{});
|
||||
};
|
||||
|
||||
// Iterator implementation
|
||||
pub const Iterator = struct {
|
||||
index: u32 = 0,
|
||||
event_counts: *EventCounts,
|
||||
|
||||
pub const Entry = struct { []const u8, u32 };
|
||||
|
||||
pub fn next(self: *Iterator, _: *const Page) ?Iterator.Entry {
|
||||
const index = self.index;
|
||||
if (index >= tracked_event_types.len) {
|
||||
return null;
|
||||
}
|
||||
self.index = index + 1;
|
||||
|
||||
return .{ tracked_event_types[index], self.event_counts._counts[index] };
|
||||
}
|
||||
};
|
||||
|
||||
const GenericIterator = @import("collections/iterator.zig").Entry;
|
||||
pub const KeyIterator = GenericIterator(Iterator, "0");
|
||||
pub const ValueIterator = GenericIterator(Iterator, "1");
|
||||
pub const EntryIterator = GenericIterator(Iterator, null);
|
||||
@@ -2,6 +2,8 @@ const js = @import("../js/js.zig");
|
||||
const Page = @import("../Page.zig");
|
||||
const datetime = @import("../../datetime.zig");
|
||||
|
||||
const EventCounts = @import("EventCounts.zig");
|
||||
|
||||
pub fn registerTypes() []const type {
|
||||
return &.{ Performance, Entry, Mark, Measure, PerformanceTiming, PerformanceNavigation };
|
||||
}
|
||||
@@ -14,6 +16,7 @@ _time_origin: u64,
|
||||
_entries: std.ArrayList(*Entry) = .{},
|
||||
_timing: PerformanceTiming = .{},
|
||||
_navigation: PerformanceNavigation = .{},
|
||||
_event_counts: EventCounts = .{},
|
||||
|
||||
/// Get high-resolution timestamp in microseconds, rounded to 5μs increments
|
||||
/// to match browser behavior (prevents fingerprinting)
|
||||
@@ -54,6 +57,10 @@ pub fn getNavigation(self: *Performance) *PerformanceNavigation {
|
||||
return &self._navigation;
|
||||
}
|
||||
|
||||
pub fn getEventCounts(self: *Performance) *EventCounts {
|
||||
return &self._event_counts;
|
||||
}
|
||||
|
||||
pub fn mark(
|
||||
self: *Performance,
|
||||
name: []const u8,
|
||||
@@ -277,6 +284,7 @@ pub const JsApi = struct {
|
||||
pub const timeOrigin = bridge.accessor(Performance.getTimeOrigin, null, .{});
|
||||
pub const timing = bridge.accessor(Performance.getTiming, null, .{});
|
||||
pub const navigation = bridge.accessor(Performance.getNavigation, null, .{});
|
||||
pub const eventCounts = bridge.accessor(Performance.getEventCounts, null, .{});
|
||||
};
|
||||
|
||||
pub const Entry = struct {
|
||||
|
||||
Reference in New Issue
Block a user