bee-ladybug 0.6.4 → 0.6.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.MD +9 -0
- package/package.json +1 -1
- package/src/adapters/AntAdapter.js +110 -32
- package/src/core/BeeLadybugCore.js +1 -1
- package/src/index.d.ts +1 -0
package/README.MD
CHANGED
|
@@ -1,10 +1,19 @@
|
|
|
1
1
|
|
|
2
2
|
# BeeLadybug
|
|
3
|
+
[](https://www.npmjs.com/package/bee-ladybug)
|
|
4
|
+
[](https://www.npmjs.com/package/bee-ladybug)
|
|
5
|
+
[](https://www.npmjs.com/package/bee-ladybug)
|
|
6
|
+
[](https://www.jsdelivr.com/package/npm/bee-ladybug)
|
|
7
|
+

|
|
8
|
+
[](https://badge.socket.dev/npm/package/bee-ladybug/0.6.5)
|
|
9
|
+
[](https://bundlephobia.com/package/bee-ladybug)
|
|
10
|
+
[](https://security.snyk.io/package/npm/bee-ladybug)
|
|
3
11
|
|
|
4
12
|
<p align="center">
|
|
5
13
|
<img src="https://beeenginejs.com/wp-content/uploads/2026/10/beeladybug-logo-animato-ezgif.com-effects.gif" alt="BeeEngine Logo" width="900" />
|
|
6
14
|
</p>
|
|
7
15
|
|
|
16
|
+
|
|
8
17
|
Universal telemetry and visual inspection overlay.
|
|
9
18
|
|
|
10
19
|
BeeLadybug is **not** a Canvas widget. It is a small open-source debugger
|
package/package.json
CHANGED
|
@@ -38,6 +38,7 @@ export class AntAdapter {
|
|
|
38
38
|
this.source = cfg.source;
|
|
39
39
|
|
|
40
40
|
this.#attached = false;
|
|
41
|
+
this.#active = false;
|
|
41
42
|
this.#observer = null;
|
|
42
43
|
this.#counts = new WeakMap();
|
|
43
44
|
this.#hot = [];
|
|
@@ -45,6 +46,8 @@ export class AntAdapter {
|
|
|
45
46
|
}
|
|
46
47
|
|
|
47
48
|
#attached;
|
|
49
|
+
/** False while the ANT button is off: setRoot() must not restart the observer then. */
|
|
50
|
+
#active;
|
|
48
51
|
#observer;
|
|
49
52
|
#counts;
|
|
50
53
|
#hot;
|
|
@@ -53,17 +56,19 @@ export class AntAdapter {
|
|
|
53
56
|
attach() {
|
|
54
57
|
if (this.#attached) return this;
|
|
55
58
|
this.#attached = true;
|
|
56
|
-
this.#
|
|
59
|
+
this.#turnOn();
|
|
57
60
|
this.core?.registerAdapter('ant', {
|
|
58
|
-
enable: () => this.#
|
|
61
|
+
enable: () => this.#turnOn(),
|
|
59
62
|
disable: () => this.#disable(),
|
|
60
63
|
label: 'ANT',
|
|
61
64
|
snapshot: () => this.snapshot()
|
|
62
65
|
});
|
|
63
|
-
this
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
66
|
+
if (this.#observer) {
|
|
67
|
+
this.core?.sendData('log', {
|
|
68
|
+
source: this.source,
|
|
69
|
+
message: 'AntAdapter attached. Watching DOM mutation rate.'
|
|
70
|
+
});
|
|
71
|
+
}
|
|
67
72
|
return this;
|
|
68
73
|
}
|
|
69
74
|
|
|
@@ -75,6 +80,19 @@ export class AntAdapter {
|
|
|
75
80
|
return this;
|
|
76
81
|
}
|
|
77
82
|
|
|
83
|
+
/**
|
|
84
|
+
* Watch another root. Counters and hot rows restart; `null` stops watching.
|
|
85
|
+
* @param {ParentNode | null} root
|
|
86
|
+
*/
|
|
87
|
+
setRoot(root) {
|
|
88
|
+
this.root = root;
|
|
89
|
+
if (!this.#attached) return this;
|
|
90
|
+
this.#reset();
|
|
91
|
+
if (!root || !this.#active) return this;
|
|
92
|
+
this.#warnNotWatching(this.#observe());
|
|
93
|
+
return this;
|
|
94
|
+
}
|
|
95
|
+
|
|
78
96
|
/**
|
|
79
97
|
* Nodes whose current window is still at or above the threshold.
|
|
80
98
|
* Hidden boxes, the WordPress admin bar, and BeeLadybug chrome are skipped.
|
|
@@ -99,49 +117,83 @@ export class AntAdapter {
|
|
|
99
117
|
return { name: 'ant', items };
|
|
100
118
|
}
|
|
101
119
|
|
|
102
|
-
#
|
|
103
|
-
if (!this.#attached
|
|
104
|
-
|
|
120
|
+
#turnOn() {
|
|
121
|
+
if (!this.#attached) return;
|
|
122
|
+
this.#active = true;
|
|
123
|
+
this.#warnNotWatching(this.#observe());
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
#disable() {
|
|
127
|
+
this.#active = false;
|
|
128
|
+
this.#reset();
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
#reset() {
|
|
132
|
+
if (this.#observer) this.#observer.disconnect();
|
|
133
|
+
this.#observer = null;
|
|
134
|
+
this.#counts = new WeakMap();
|
|
135
|
+
this.#hot = [];
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/** @returns {'' | 'no-root' | 'no-observer' | 'failed'} why nothing is watched, empty when it is */
|
|
139
|
+
#observe() {
|
|
140
|
+
if (this.#observer) return '';
|
|
141
|
+
if (!this.root) return 'no-root';
|
|
142
|
+
if (typeof MutationObserver === 'undefined') return 'no-observer';
|
|
105
143
|
this.#counts = new WeakMap();
|
|
106
|
-
|
|
144
|
+
const observer = new MutationObserver(this.#onMutate);
|
|
107
145
|
try {
|
|
108
|
-
|
|
146
|
+
observer.observe(this.root, {
|
|
109
147
|
childList: true,
|
|
110
148
|
subtree: true,
|
|
111
149
|
attributes: true,
|
|
112
150
|
characterData: true
|
|
113
151
|
});
|
|
114
152
|
} catch {
|
|
115
|
-
|
|
116
|
-
|
|
153
|
+
observer.disconnect();
|
|
154
|
+
return 'failed';
|
|
117
155
|
}
|
|
156
|
+
this.#observer = observer;
|
|
157
|
+
return '';
|
|
118
158
|
}
|
|
119
159
|
|
|
120
|
-
#
|
|
121
|
-
if (
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
160
|
+
#warnNotWatching(reason) {
|
|
161
|
+
if (!reason) return;
|
|
162
|
+
const messages = {
|
|
163
|
+
'no-root': 'no root: mutation rate is not watched. Pass { root } or call setRoot().',
|
|
164
|
+
'no-observer': 'MutationObserver is missing: mutation rate is not watched.',
|
|
165
|
+
failed: 'observe() failed: mutation rate is not watched.'
|
|
166
|
+
};
|
|
167
|
+
this.core?.sendData('warn', { source: this.source, message: messages[reason] });
|
|
125
168
|
}
|
|
126
169
|
|
|
127
170
|
#ingest(records) {
|
|
128
171
|
if (!this.#attached || !this.core || !records?.length) return;
|
|
129
172
|
const now = typeof performance !== 'undefined' ? performance.now() : Date.now();
|
|
173
|
+
this.#prune(now);
|
|
174
|
+
// One layout read per element per batch, however many records it produced.
|
|
175
|
+
const skipped = new Map();
|
|
130
176
|
for (let i = 0; i < records.length; i++) {
|
|
131
|
-
const
|
|
132
|
-
if (!
|
|
177
|
+
const el = elementOf(records[i]?.target);
|
|
178
|
+
if (!el) continue;
|
|
179
|
+
let skip = skipped.get(el);
|
|
180
|
+
if (skip === undefined) {
|
|
181
|
+
skip = isBeeChrome(el) || isReportNoise(el);
|
|
182
|
+
skipped.set(el, skip);
|
|
183
|
+
}
|
|
184
|
+
if (skip) continue;
|
|
133
185
|
|
|
134
|
-
let slot = this.#counts.get(
|
|
186
|
+
let slot = this.#counts.get(el);
|
|
135
187
|
if (!slot || now - slot.windowStart >= this.windowMs) {
|
|
136
188
|
slot = { count: 0, windowStart: now, warned: false };
|
|
137
|
-
this.#counts.set(
|
|
189
|
+
this.#counts.set(el, slot);
|
|
138
190
|
}
|
|
139
191
|
slot.count += 1;
|
|
140
|
-
this.#track(
|
|
192
|
+
this.#track(el, slot);
|
|
141
193
|
if (slot.count < this.threshold || slot.warned) continue;
|
|
142
194
|
|
|
143
195
|
slot.warned = true;
|
|
144
|
-
const selector = nodeSelector(
|
|
196
|
+
const selector = nodeSelector(el);
|
|
145
197
|
this.core.sendData('warn', {
|
|
146
198
|
source: this.source,
|
|
147
199
|
message: `High Mutation Rate on ${selector}`,
|
|
@@ -150,21 +202,36 @@ export class AntAdapter {
|
|
|
150
202
|
}
|
|
151
203
|
}
|
|
152
204
|
|
|
153
|
-
#
|
|
154
|
-
|
|
205
|
+
#prune(now) {
|
|
206
|
+
this.#hot = this.#hot.filter((row) => now - row.windowStart < this.windowMs);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
#track(el, slot) {
|
|
210
|
+
const index = this.#hot.findIndex((row) => row.node === el);
|
|
155
211
|
if (slot.count < this.threshold) {
|
|
156
212
|
if (index >= 0) this.#hot.splice(index, 1);
|
|
157
213
|
return;
|
|
158
214
|
}
|
|
159
215
|
const row = {
|
|
160
|
-
node,
|
|
161
|
-
selector: nodeSelector(
|
|
216
|
+
node: el,
|
|
217
|
+
selector: nodeSelector(el),
|
|
162
218
|
count: slot.count,
|
|
163
219
|
windowStart: slot.windowStart
|
|
164
220
|
};
|
|
165
221
|
if (index >= 0) this.#hot[index] = row;
|
|
166
222
|
else this.#hot.push(row);
|
|
167
|
-
|
|
223
|
+
while (this.#hot.length > MAX_HOT) this.#hot.splice(this.#lowestRow(), 1);
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/** Fewest mutations first; on a tie the older window goes. */
|
|
227
|
+
#lowestRow() {
|
|
228
|
+
let worst = 0;
|
|
229
|
+
for (let i = 1; i < this.#hot.length; i++) {
|
|
230
|
+
const row = this.#hot[i];
|
|
231
|
+
const low = this.#hot[worst];
|
|
232
|
+
if (row.count < low.count || (row.count === low.count && row.windowStart < low.windowStart)) worst = i;
|
|
233
|
+
}
|
|
234
|
+
return worst;
|
|
168
235
|
}
|
|
169
236
|
}
|
|
170
237
|
|
|
@@ -173,15 +240,26 @@ function positive(value, fallback) {
|
|
|
173
240
|
return Number.isFinite(n) && n > 0 ? n : fallback;
|
|
174
241
|
}
|
|
175
242
|
|
|
243
|
+
/** A text node counts on its parent element, so one element keeps one window. */
|
|
244
|
+
function elementOf(node) {
|
|
245
|
+
if (!node) return null;
|
|
246
|
+
if (node.nodeType === 1) return node;
|
|
247
|
+
return node.parentElement || null;
|
|
248
|
+
}
|
|
249
|
+
|
|
176
250
|
function nodeSelector(node) {
|
|
177
|
-
const el = node
|
|
251
|
+
const el = elementOf(node);
|
|
178
252
|
if (!el || !el.tagName) return 'node';
|
|
179
|
-
if (el.id) return `#${el.id}`;
|
|
253
|
+
if (el.id) return `#${cssEscape(el.id)}`;
|
|
180
254
|
const cls = el.classList && el.classList[0];
|
|
181
|
-
if (cls) return `.${cls}`;
|
|
255
|
+
if (cls) return `.${cssEscape(cls)}`;
|
|
182
256
|
return el.tagName.toLowerCase();
|
|
183
257
|
}
|
|
184
258
|
|
|
259
|
+
function cssEscape(value) {
|
|
260
|
+
return String(value).replace(/[^\w-]/g, '\\$&');
|
|
261
|
+
}
|
|
262
|
+
|
|
185
263
|
function isBeeChrome(node) {
|
|
186
264
|
let el = node && node.nodeType === 1 ? node : node?.parentElement;
|
|
187
265
|
while (el) {
|
|
@@ -10,7 +10,7 @@ import { UIOverlay } from './UIOverlay.js';
|
|
|
10
10
|
import { buildAssistantSnapshot, normalizeAssistant } from './AiAssistant.js';
|
|
11
11
|
import { exportReport as writeReport } from './ReportExporter.js';
|
|
12
12
|
|
|
13
|
-
export const BEE_LADYBUG_VERSION = '0.6.
|
|
13
|
+
export const BEE_LADYBUG_VERSION = '0.6.5';
|
|
14
14
|
|
|
15
15
|
export const CORE_DEFAULTS = Object.freeze({
|
|
16
16
|
toggleKey: 'F2',
|
package/src/index.d.ts
CHANGED