bee-ladybug 0.2.0 → 0.4.0

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.
@@ -1,5 +1,8 @@
1
+
1
2
  # BeeLadybug
2
3
 
4
+
5
+
3
6
  Universal telemetry and visual inspection overlay.
4
7
 
5
8
  BeeLadybug is **not** a Canvas widget. It is a small open-source debugger
@@ -34,6 +37,8 @@ bee-ladybug/
34
37
  │ └── adapters/
35
38
  │ ├── CanvasAdapter.js
36
39
  │ ├── WebDOMAdapter.js
40
+ │ ├── AntAdapter.js
41
+ │ ├── SpiderAdapter.js
37
42
  │ └── python/PythonBridge.js
38
43
  ├── examples/
39
44
  │ ├── core.html
@@ -93,6 +98,7 @@ Gli adapter parlano **solo** così. Uno scalare viene avvolto in `{ value }`.
93
98
  | `log` | `{ message, level?, source? }` | console |
94
99
  | `warn` | `{ message, source? }` | console ambra |
95
100
  | `error` | `{ message, source? }` | console rossa |
101
+ | `telemetry` | `{ kind?, duration?, source? }` | history + lampeggio pulsante adapter |
96
102
  | `clear` | `{ channel?: 'logs'\|'history'\|'hud' }` | reset buffer |
97
103
  | `sys` | emesso dal Core | freeze / visibilità / scala |
98
104
 
@@ -113,6 +119,8 @@ bee.subscribe((packet) => {
113
119
  });
114
120
  ```
115
121
 
122
+ ![Overlay Core: la ⓘ accanto a LIVE spiega che il freeze ferma solo il clock fittizio](docs/overlay-core.jpg)
123
+
116
124
  ## Tasti
117
125
 
118
126
  | Tasto | Azione |
@@ -167,6 +175,50 @@ adapter.attach();
167
175
 
168
176
  Traduce il DOM in pacchetti: nodo sotto il puntatore, box model (margin / padding / content), conteggio nodi, mutazioni (`+n -n attr`). `F4` congela l'hover. `F2` nasconde overlay e highlight. Il Core non sa che esiste HTML.
169
177
 
178
+ ## Registro adapter (overlay)
179
+
180
+ Il Core non conosce ANT/SPIDER. Gli adapter si iscrivono con hook propri; l'overlay disegna un toggle per ciascuno.
181
+
182
+ ```js
183
+ bee.registerAdapter('probe', {
184
+ label: 'PROBE',
185
+ enable: () => observer.observe(root, opts),
186
+ disable: () => observer.disconnect()
187
+ });
188
+ adapter.detach(); // chiama bee.unregisterAdapter('probe')
189
+ ```
190
+
191
+ Click sul pulsante → `enable()` / `disable()`. Un pacchetto `warn` o `telemetry` con `source` uguale al nome registrato fa lampeggiare quel pulsante, al massimo una volta ogni 1.5s.
192
+
193
+ ## AntAdapter
194
+
195
+ ```js
196
+ import { BeeLadybugCore, AntAdapter } from './src/index.js';
197
+
198
+ const bee = new BeeLadybugCore();
199
+ const ant = new AntAdapter(bee, {
200
+ root: document.body,
201
+ threshold: 30 // mutazioni / secondo per nodo
202
+ });
203
+ ant.attach();
204
+ ```
205
+
206
+ `MutationObserver` indipendente da `WebDOMAdapter` (possono condividere lo stesso `root`). Se un nodo supera la soglia, arriva un `warn` `source: 'ant'`.
207
+
208
+ ## SpiderAdapter
209
+
210
+ ```js
211
+ import { BeeLadybugCore, SpiderAdapter } from './src/index.js';
212
+
213
+ const bee = new BeeLadybugCore();
214
+ const spider = new SpiderAdapter(bee, {
215
+ resourceThreshold: 500 // ms
216
+ });
217
+ spider.attach();
218
+ ```
219
+
220
+ `PerformanceObserver`: `longtask` > 50ms → `telemetry`; risorse > soglia → `warn`. Se il browser non espone `longtask` (Firefox/Safari), un log una tantum e quella parte si spegne senza errori.
221
+
170
222
  ## PythonBridge
171
223
 
172
224
  Python non vede l'overlay. Manda JSON. Il browser lo srotola in `sendData()`.
@@ -197,6 +249,8 @@ python examples/python/sender.py
197
249
 
198
250
  Poi aprire `http://localhost:8080/examples/python.html` e Connect. I pulsanti mock funzionano anche a processo spento.
199
251
 
252
+ ![Overlay PythonBridge: ASK AI e quadratini mock/echo per scegliere il provider](docs/overlay-python.jpg)
253
+
200
254
  ## npm / TypeScript
201
255
 
202
256
  ```bash
@@ -216,14 +270,19 @@ npm run typecheck
216
270
  npm run lint
217
271
  ```
218
272
 
219
- Versione runtime e pacchetto: **0.2.0** (`BEE_LADYBUG_VERSION` e `package.json` devono restare uguali). Su npm ora risulta `0.1.0`: il prossimo `npm publish` sarà `0.2.0`.
273
+ ```bash
274
+ npm login
275
+ npm publish --access public
276
+ ```
277
+
278
+ Versione runtime e pacchetto: **0.4.0** (`BEE_LADYBUG_VERSION` e `package.json` devono restare uguali).
220
279
 
221
280
  ## Assistente AI (gancio, nessun vendor)
222
281
 
223
- BeeLadybug **non** chiama OpenAI, Claude o Ollama. Lo sviluppatore innesta il proprio modello:
282
+ BeeLadybug **non** chiama OpenAI, Claude o Ollama. Lo sviluppatore innesta i propri modelli. `registerAssistant` **aggiunge** (non sostituisce gli altri). `setActiveAssistant(name)` sceglie chi risponde a **ASK AI**.
224
283
 
225
284
  ```js
226
- bee.setAssistant({
285
+ bee.registerAssistant({
227
286
  name: 'ollama',
228
287
  async complete({ question, snapshot }) {
229
288
  const res = await fetch('http://127.0.0.1:11434/api/generate', {
@@ -240,17 +299,18 @@ bee.setAssistant({
240
299
  }
241
300
  });
242
301
 
302
+ bee.registerAssistant({
303
+ name: 'mock',
304
+ complete({ snapshot }) {
305
+ return `logs=${snapshot.logs.length}`;
306
+ }
307
+ });
308
+
309
+ bee.setActiveAssistant('ollama');
243
310
  await bee.ask('perché il loss sale?');
244
311
  ```
245
312
 
246
- Il Core manda un **snapshot** (log recenti, HUD, metriche) a `complete()`. Chiavi tipo `token`/`password` vengono redacted. In overlay: pulsante **ASK AI**. Senza `setAssistant`, il bottone spiega come collegarlo.
247
-
248
- ## API Core
249
-
250
- ```bash
251
- npm login
252
- npm publish --access public
253
- ```
313
+ Il Core manda un **snapshot** (log recenti, HUD, metriche) a `complete()`. Chiavi tipo `token`/`password` vengono redacted. In overlay: pulsante **ASK AI** e un quadratino per ogni provider; quello attivo è evidenziato. Senza provider, il bottone spiega come collegarlo. `setAssistant` resta come scorciatoia: registra e attiva, senza cancellare gli altri.
254
314
 
255
315
  ## API Core
256
316
 
@@ -277,15 +337,18 @@ bee.getHud();
277
337
  bee.show(); bee.hide(); bee.toggle();
278
338
  bee.freeze(); bee.unfreeze(); bee.toggleFreeze();
279
339
  bee.applySlowMo(); bee.restoreRealtime();
280
- bee.setAssistant({ name: 'mock', complete: async ({ snapshot }) => 'ok' });
340
+ bee.registerAssistant({ name: 'mock', complete: async ({ snapshot }) => 'ok' });
341
+ bee.setActiveAssistant('mock');
281
342
  await bee.ask('why fps drop?');
343
+ bee.registerAdapter('probe', { label: 'PROBE', enable() {}, disable() {} });
344
+ bee.toggleAdapter('probe');
282
345
  bee.destroy();
283
346
  ```
284
347
 
285
348
  ## Roadmap
286
349
 
287
- 1. **Ora** — Core, adapter, PythonBridge, tipi, ESLint, gancio AI. Fatto.
288
- 2. **npm 0.2.0** — `npm publish` (registry è ancora 0.1.0).
350
+ 1. **Ora** — Core, adapter, registro toggle overlay, Ant/Spider, PythonBridge, tipi, ESLint, gancio AI multi-provider. Fatto.
351
+ 2. **npm 0.4.0** — `npm publish`.
289
352
 
290
353
  ## Come contribuire
291
354
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bee-ladybug",
3
- "version": "0.2.0",
3
+ "version": "0.4.0",
4
4
  "description": "Universal telemetry and visual inspection overlay for Canvas, DOM, Python and AI flows.",
5
5
  "type": "module",
6
6
  "main": "src/index.js",
@@ -19,7 +19,7 @@
19
19
  "scripts": {
20
20
  "typecheck": "tsc --noEmit",
21
21
  "lint": "eslint src examples",
22
- "test": "node --test tests/**/*.test.js"
22
+ "test": "node --test tests/*.test.js"
23
23
  },
24
24
  "keywords": [
25
25
  "debug",
@@ -45,7 +45,7 @@
45
45
  },
46
46
  "homepage": "https://github.com/antonioprosperi2-svg/BeeLadybug-universal#readme",
47
47
  "engines": {
48
- "node": ">=18"
48
+ "node": ">=20"
49
49
  },
50
50
  "devDependencies": {
51
51
  "@eslint/js": "^9.36.0",
@@ -0,0 +1,144 @@
1
+ /**
2
+ * AntAdapter — DOM mutation rate tracker.
3
+ *
4
+ * Independent MutationObserver. Can sit on the same root as WebDOMAdapter.
5
+ * The core must never import this file.
6
+ *
7
+ * @example
8
+ * const ant = new AntAdapter(core, { root: document.body, threshold: 30 });
9
+ * ant.attach();
10
+ */
11
+
12
+ export const ANT_ADAPTER_DEFAULTS = Object.freeze({
13
+ threshold: 30,
14
+ windowMs: 1000,
15
+ source: 'ant'
16
+ });
17
+
18
+ export class AntAdapter {
19
+ /**
20
+ * @param {import('../core/BeeLadybugCore.js').BeeLadybugCore} core
21
+ * @param {Partial<typeof ANT_ADAPTER_DEFAULTS> & {
22
+ * root?: ParentNode | null
23
+ * }} [options]
24
+ */
25
+ constructor(core, options = {}) {
26
+ const cfg = { ...ANT_ADAPTER_DEFAULTS, ...options };
27
+ this.core = core;
28
+ this.root = options.root !== undefined
29
+ ? options.root
30
+ : (typeof document !== 'undefined' ? document.body : null);
31
+ this.threshold = positive(cfg.threshold, ANT_ADAPTER_DEFAULTS.threshold);
32
+ this.windowMs = positive(cfg.windowMs, ANT_ADAPTER_DEFAULTS.windowMs);
33
+ this.source = cfg.source;
34
+
35
+ this.#attached = false;
36
+ this.#observer = null;
37
+ this.#counts = new WeakMap();
38
+ this.#onMutate = (records) => this.#ingest(records);
39
+ }
40
+
41
+ #attached;
42
+ #observer;
43
+ #counts;
44
+ #onMutate;
45
+
46
+ attach() {
47
+ if (this.#attached) return this;
48
+ this.#attached = true;
49
+ this.#enable();
50
+ this.core?.registerAdapter('ant', {
51
+ enable: () => this.#enable(),
52
+ disable: () => this.#disable(),
53
+ label: 'ANT'
54
+ });
55
+ this.core?.sendData('log', {
56
+ source: this.source,
57
+ message: 'AntAdapter attached. Watching DOM mutation rate.'
58
+ });
59
+ return this;
60
+ }
61
+
62
+ detach() {
63
+ if (!this.#attached) return this;
64
+ this.#attached = false;
65
+ this.#disable();
66
+ this.core?.unregisterAdapter('ant');
67
+ return this;
68
+ }
69
+
70
+ #enable() {
71
+ if (!this.#attached || this.#observer) return;
72
+ if (typeof MutationObserver === 'undefined' || !this.root) return;
73
+ this.#counts = new WeakMap();
74
+ this.#observer = new MutationObserver(this.#onMutate);
75
+ try {
76
+ this.#observer.observe(this.root, {
77
+ childList: true,
78
+ subtree: true,
79
+ attributes: true,
80
+ characterData: true
81
+ });
82
+ } catch {
83
+ this.#observer.disconnect();
84
+ this.#observer = null;
85
+ }
86
+ }
87
+
88
+ #disable() {
89
+ if (this.#observer) this.#observer.disconnect();
90
+ this.#observer = null;
91
+ this.#counts = new WeakMap();
92
+ }
93
+
94
+ #ingest(records) {
95
+ if (!this.#attached || !this.core || !records?.length) return;
96
+ const now = typeof performance !== 'undefined' ? performance.now() : Date.now();
97
+ for (let i = 0; i < records.length; i++) {
98
+ const node = records[i]?.target;
99
+ if (!node || isBeeChrome(node)) continue;
100
+
101
+ let slot = this.#counts.get(node);
102
+ if (!slot || now - slot.windowStart >= this.windowMs) {
103
+ slot = { count: 0, windowStart: now, warned: false };
104
+ this.#counts.set(node, slot);
105
+ }
106
+ slot.count += 1;
107
+ if (slot.count < this.threshold || slot.warned) continue;
108
+
109
+ slot.warned = true;
110
+ const selector = nodeSelector(node);
111
+ this.core.sendData('warn', {
112
+ source: this.source,
113
+ message: `High Mutation Rate on ${selector}`,
114
+ node: selector
115
+ });
116
+ }
117
+ }
118
+ }
119
+
120
+ function positive(value, fallback) {
121
+ const n = Number(value);
122
+ return Number.isFinite(n) && n > 0 ? n : fallback;
123
+ }
124
+
125
+ function nodeSelector(node) {
126
+ const el = node && node.nodeType === 1 ? node : node?.parentElement;
127
+ if (!el || !el.tagName) return 'node';
128
+ if (el.id) return `#${el.id}`;
129
+ const cls = el.classList && el.classList[0];
130
+ if (cls) return `.${cls}`;
131
+ return el.tagName.toLowerCase();
132
+ }
133
+
134
+ function isBeeChrome(node) {
135
+ let el = node && node.nodeType === 1 ? node : node?.parentElement;
136
+ while (el) {
137
+ const tag = el.tagName;
138
+ if (tag === 'BEE-LADYBUG-OVERLAY') return true;
139
+ if (el.hasAttribute?.('data-bee-ladybug-dom')) return true;
140
+ if (el.hasAttribute?.('data-bee-ladybug-canvas')) return true;
141
+ el = el.parentElement;
142
+ }
143
+ return false;
144
+ }
@@ -15,11 +15,17 @@
15
15
  * adapter.attach();
16
16
  * // after updating entity positions each host frame:
17
17
  * adapter.pump();
18
+ *
19
+ * @example
20
+ * // third-party / bookmarklet: no access to the host loop
21
+ * const remote = new CanvasAdapter(core, { canvas, entities, autoPump: true });
22
+ * remote.attach();
18
23
  */
19
24
 
20
25
  export const CANVAS_ADAPTER_DEFAULTS = Object.freeze({
21
26
  overlay: true,
22
27
  computeCollisions: true,
28
+ autoPump: false,
23
29
  colorActive: '#3dff6a',
24
30
  colorColliding: '#ff3b3b',
25
31
  colorInactive: '#8a8a8a',
@@ -41,6 +47,7 @@ export class CanvasAdapter {
41
47
  this.entities = options.entities ?? [];
42
48
  this.overlayEnabled = cfg.overlay;
43
49
  this.computeCollisions = cfg.computeCollisions;
50
+ this.autoPump = Boolean(cfg.autoPump);
44
51
  this.colorActive = cfg.colorActive;
45
52
  this.colorColliding = cfg.colorColliding;
46
53
  this.colorInactive = cfg.colorInactive;
@@ -54,19 +61,35 @@ export class CanvasAdapter {
54
61
  this.#attached = false;
55
62
  this.#overlay = null;
56
63
  this.#unsubscribe = null;
64
+ this.#rafId = 0;
65
+ this.#pumpFromAuto = false;
66
+ this.#manualPumpWarned = false;
57
67
  this.#lastPump = 0;
58
68
  this.#fps = 0;
59
69
  this.#wasColliding = new Set();
60
70
  this.#onResize = () => this.#syncOverlay();
71
+ this.#onTick = () => {
72
+ this.#rafId = requestAnimationFrame(this.#onTick);
73
+ this.#pumpFromAuto = true;
74
+ try {
75
+ this.pump();
76
+ } finally {
77
+ this.#pumpFromAuto = false;
78
+ }
79
+ };
61
80
  }
62
81
 
63
82
  #attached;
64
83
  #overlay;
65
84
  #unsubscribe;
85
+ #rafId;
86
+ #pumpFromAuto;
87
+ #manualPumpWarned;
66
88
  #lastPump;
67
89
  #fps;
68
90
  #wasColliding;
69
91
  #onResize;
92
+ #onTick;
70
93
 
71
94
  attach() {
72
95
  if (this.#attached) return this;
@@ -83,6 +106,7 @@ export class CanvasAdapter {
83
106
  window.addEventListener('resize', this.#onResize);
84
107
  window.addEventListener('scroll', this.#onResize, true);
85
108
  }
109
+ if (this.autoPump) this.startAutoPump();
86
110
 
87
111
  this.core?.sendData('log', {
88
112
  source: this.source,
@@ -99,6 +123,7 @@ export class CanvasAdapter {
99
123
  detach() {
100
124
  if (!this.#attached) return this;
101
125
  this.#attached = false;
126
+ this.#stopAutoPumpLoop();
102
127
  if (this.#unsubscribe) this.#unsubscribe();
103
128
  this.#unsubscribe = null;
104
129
  this.#unmountOverlay();
@@ -109,6 +134,20 @@ export class CanvasAdapter {
109
134
  return this;
110
135
  }
111
136
 
137
+ /**
138
+ * Drive `pump()` from an internal `requestAnimationFrame` loop.
139
+ * Use this when the host game loop cannot call `pump()` (bookmarklet / console).
140
+ */
141
+ startAutoPump() {
142
+ this.autoPump = true;
143
+ if (!this.#attached) return this;
144
+ if (this.#rafId) return this;
145
+ if (typeof requestAnimationFrame !== 'function') return this;
146
+ this.#manualPumpWarned = false;
147
+ this.#rafId = requestAnimationFrame(this.#onTick);
148
+ return this;
149
+ }
150
+
112
151
  /** @param {HTMLCanvasElement | null} canvas */
113
152
  setCanvas(canvas) {
114
153
  this.canvas = canvas;
@@ -126,10 +165,15 @@ export class CanvasAdapter {
126
165
  * Call once per host frame, after entity positions are updated.
127
166
  * Pass a 2D context only if overlay is disabled and you want hitboxes
128
167
  * drawn onto the game canvas itself.
168
+ * Ignored (with a warning) while `autoPump` owns the frame loop.
129
169
  * @param {CanvasRenderingContext2D} [ctx]
130
170
  */
131
171
  pump(ctx) {
132
172
  if (!this.#attached || !this.core) return this;
173
+ if (this.#rafId && !this.#pumpFromAuto) {
174
+ this.#warnManualPump();
175
+ return this;
176
+ }
133
177
 
134
178
  const now = typeof performance !== 'undefined' ? performance.now() : Date.now();
135
179
  if (this.#lastPump) {
@@ -147,6 +191,25 @@ export class CanvasAdapter {
147
191
  return this;
148
192
  }
149
193
 
194
+ #stopAutoPumpLoop() {
195
+ if (this.#rafId && typeof cancelAnimationFrame === 'function') {
196
+ cancelAnimationFrame(this.#rafId);
197
+ }
198
+ this.#rafId = 0;
199
+ this.#pumpFromAuto = false;
200
+ this.#manualPumpWarned = false;
201
+ }
202
+
203
+ #warnManualPump() {
204
+ if (this.#manualPumpWarned) return;
205
+ this.#manualPumpWarned = true;
206
+ const message = 'pump() skipped: autoPump already drives the frame loop.';
207
+ if (typeof console !== 'undefined' && typeof console.warn === 'function') {
208
+ console.warn(`[BeeLadybug] CanvasAdapter ${message}`);
209
+ }
210
+ this.core.sendData('warn', { source: this.source, message });
211
+ }
212
+
150
213
  #onCore(packet) {
151
214
  if (packet.type !== 'sys') return;
152
215
  const op = packet.payload.op;
@@ -0,0 +1,160 @@
1
+ /**
2
+ * SpiderAdapter — long-task and slow-resource tracker.
3
+ *
4
+ * Native PerformanceObserver only. The core must never import this file.
5
+ *
6
+ * @example
7
+ * const spider = new SpiderAdapter(core, { resourceThreshold: 500 });
8
+ * spider.attach();
9
+ */
10
+
11
+ export const SPIDER_ADAPTER_DEFAULTS = Object.freeze({
12
+ longTaskThreshold: 50,
13
+ resourceThreshold: 500,
14
+ source: 'spider'
15
+ });
16
+
17
+ export class SpiderAdapter {
18
+ /**
19
+ * @param {import('../core/BeeLadybugCore.js').BeeLadybugCore} core
20
+ * @param {Partial<typeof SPIDER_ADAPTER_DEFAULTS>} [options]
21
+ */
22
+ constructor(core, options = {}) {
23
+ const cfg = { ...SPIDER_ADAPTER_DEFAULTS, ...options };
24
+ this.core = core;
25
+ this.longTaskThreshold = positive(cfg.longTaskThreshold, SPIDER_ADAPTER_DEFAULTS.longTaskThreshold);
26
+ this.resourceThreshold = positive(cfg.resourceThreshold, SPIDER_ADAPTER_DEFAULTS.resourceThreshold);
27
+ this.source = cfg.source;
28
+
29
+ this.#attached = false;
30
+ this.#longObserver = null;
31
+ this.#resourceObserver = null;
32
+ this.#longTaskMissingLogged = false;
33
+ this.#onLongTask = (list) => this.#ingestLongTasks(list);
34
+ this.#onResource = (list) => this.#ingestResources(list);
35
+ }
36
+
37
+ #attached;
38
+ #longObserver;
39
+ #resourceObserver;
40
+ #longTaskMissingLogged;
41
+ #onLongTask;
42
+ #onResource;
43
+
44
+ attach() {
45
+ if (this.#attached) return this;
46
+ this.#attached = true;
47
+ this.#enable();
48
+ this.core?.registerAdapter('spider', {
49
+ enable: () => this.#enable(),
50
+ disable: () => this.#disable(),
51
+ label: 'SPIDER'
52
+ });
53
+ this.core?.sendData('log', {
54
+ source: this.source,
55
+ message: 'SpiderAdapter attached. Watching long tasks and slow resources.'
56
+ });
57
+ return this;
58
+ }
59
+
60
+ detach() {
61
+ if (!this.#attached) return this;
62
+ this.#attached = false;
63
+ this.#disable();
64
+ this.core?.unregisterAdapter('spider');
65
+ return this;
66
+ }
67
+
68
+ #enable() {
69
+ if (!this.#attached) return;
70
+ if (typeof PerformanceObserver !== 'function') {
71
+ this.#logLongTaskUnavailable();
72
+ return;
73
+ }
74
+ const types = supportedEntryTypes();
75
+ if (types.includes('longtask')) this.#watchLongTasks();
76
+ else this.#logLongTaskUnavailable();
77
+ if (types.includes('resource')) this.#watchResources();
78
+ }
79
+
80
+ #disable() {
81
+ if (this.#longObserver) this.#longObserver.disconnect();
82
+ if (this.#resourceObserver) this.#resourceObserver.disconnect();
83
+ this.#longObserver = null;
84
+ this.#resourceObserver = null;
85
+ }
86
+
87
+ #watchLongTasks() {
88
+ if (this.#longObserver) return;
89
+ try {
90
+ this.#longObserver = new PerformanceObserver(this.#onLongTask);
91
+ this.#longObserver.observe({ entryTypes: ['longtask'] });
92
+ } catch {
93
+ this.#longObserver = null;
94
+ this.#logLongTaskUnavailable();
95
+ }
96
+ }
97
+
98
+ #watchResources() {
99
+ if (this.#resourceObserver) return;
100
+ try {
101
+ this.#resourceObserver = new PerformanceObserver(this.#onResource);
102
+ this.#resourceObserver.observe({ entryTypes: ['resource'] });
103
+ } catch {
104
+ this.#resourceObserver = null;
105
+ }
106
+ }
107
+
108
+ #ingestLongTasks(list) {
109
+ if (!this.#attached || !this.core) return;
110
+ const entries = list?.getEntries?.() ?? [];
111
+ for (let i = 0; i < entries.length; i++) {
112
+ const entry = entries[i];
113
+ if (!entry || entry.duration <= this.longTaskThreshold) continue;
114
+ this.core.sendData('telemetry', {
115
+ source: this.source,
116
+ kind: 'longtask',
117
+ duration: entry.duration,
118
+ attribution: entry.attribution?.[0]?.containerType ?? 'unknown'
119
+ });
120
+ }
121
+ }
122
+
123
+ #ingestResources(list) {
124
+ if (!this.#attached || !this.core) return;
125
+ const entries = list?.getEntries?.() ?? [];
126
+ for (let i = 0; i < entries.length; i++) {
127
+ const entry = entries[i];
128
+ if (!entry || entry.duration <= this.resourceThreshold) continue;
129
+ this.core.sendData('warn', {
130
+ source: this.source,
131
+ message: `Risorsa lenta: ${entry.name} (${Math.round(entry.duration)}ms)`
132
+ });
133
+ }
134
+ }
135
+
136
+ #logLongTaskUnavailable() {
137
+ if (this.#longTaskMissingLogged) return;
138
+ this.#longTaskMissingLogged = true;
139
+ this.core?.sendData('log', {
140
+ source: this.source,
141
+ message: 'Long task detection non disponibile in questo browser'
142
+ });
143
+ }
144
+ }
145
+
146
+ function positive(value, fallback) {
147
+ const n = Number(value);
148
+ return Number.isFinite(n) && n > 0 ? n : fallback;
149
+ }
150
+
151
+ function supportedEntryTypes() {
152
+ if (typeof PerformanceObserver !== 'function') return [];
153
+ const list = PerformanceObserver.supportedEntryTypes;
154
+ if (!list) return [];
155
+ try {
156
+ return Array.from(list);
157
+ } catch {
158
+ return [];
159
+ }
160
+ }