@atolljs/devtools 0.1.7 → 0.1.8

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.
Files changed (48) hide show
  1. package/README.md +408 -71
  2. package/app/api.js +68 -0
  3. package/app/index.html +161 -2
  4. package/app/main.js +877 -127
  5. package/app/panels/actions.js +324 -0
  6. package/app/panels/audit-rules.js +728 -0
  7. package/app/panels/audits.js +438 -0
  8. package/app/panels/inspect-island.js +351 -0
  9. package/app/panels/memory-values.js +287 -0
  10. package/app/panels/performance.js +304 -0
  11. package/app/panels/reactivity.js +271 -0
  12. package/app/panels/recorder.js +344 -0
  13. package/app/panels/recording-format.js +166 -0
  14. package/app/panels/route.d.ts +44 -0
  15. package/app/panels/route.js +264 -0
  16. package/app/panels/shell.js +789 -0
  17. package/app/panels/toast.js +38 -0
  18. package/dist/bin.js +704 -5
  19. package/dist/chunk-57NGHQUL.js +344 -0
  20. package/dist/{chunk-4N3NBFL3.js → chunk-AGT6VNZI.js} +179 -217
  21. package/dist/chunk-WIL2P7HG.js +661 -0
  22. package/dist/client.d.ts +7 -0
  23. package/dist/control.d.ts +4 -0
  24. package/dist/index.js +2 -1
  25. package/dist/node.js +2 -1
  26. package/dist/nodeWs.d.ts +1 -0
  27. package/dist/otel.d.ts +35 -0
  28. package/dist/otel.js +87 -0
  29. package/dist/otelMap.d.ts +232 -0
  30. package/dist/otelTransport.d.ts +69 -0
  31. package/dist/overlay.d.ts +16 -2
  32. package/dist/probes.d.ts +7 -0
  33. package/dist/protocol.d.ts +46 -4
  34. package/dist/server.d.ts +19 -0
  35. package/dist/server.js +61 -3
  36. package/package.json +6 -2
  37. package/src/bin.ts +27 -2
  38. package/src/broadcast.ts +6 -0
  39. package/src/client.ts +26 -2
  40. package/src/control.ts +51 -0
  41. package/src/nodeWs.ts +7 -1
  42. package/src/otel.ts +140 -0
  43. package/src/otelMap.ts +661 -0
  44. package/src/otelTransport.ts +286 -0
  45. package/src/overlay.ts +153 -27
  46. package/src/probes.ts +155 -0
  47. package/src/protocol.ts +52 -4
  48. package/src/server.ts +83 -3
package/dist/server.js CHANGED
@@ -1,3 +1,10 @@
1
+ import {
2
+ OtelMapper,
3
+ createOtelPipeline,
4
+ createOtlpSender,
5
+ sessionResource
6
+ } from "./chunk-WIL2P7HG.js";
7
+
1
8
  // src/server.ts
2
9
  import { createServer } from "node:http";
3
10
  import { existsSync, readFileSync } from "node:fs";
@@ -167,6 +174,22 @@ function createDevtoolsServer(opts = {}) {
167
174
  const closedCap = 20;
168
175
  const closedTtlMs = opts.closedTtlMs ?? 30 * 60 * 1e3;
169
176
  const tail = [];
177
+ const pendingControl = /* @__PURE__ */ new Map();
178
+ let otlp = null;
179
+ const otlpSessions = /* @__PURE__ */ new Map();
180
+ if (opts.otlp) {
181
+ const o = opts.otlp;
182
+ otlp = createOtelPipeline({
183
+ sender: createOtlpSender(o),
184
+ signals: o.signals,
185
+ flushIntervalMs: o.flushIntervalMs,
186
+ maxBatch: o.maxBatch,
187
+ newMapper: (id) => new OtelMapper({
188
+ resource: sessionResource(otlpSessions.get(id) ?? { id, runtime: "browser" }, o.serviceName, o.resource),
189
+ signals: o.signals
190
+ })
191
+ });
192
+ }
170
193
  const sessionList = () => [...sessions.values(), ...closedSessions];
171
194
  const evictClosed = (sessionId) => {
172
195
  const i = closedSessions.findIndex((s) => s.id === sessionId);
@@ -204,7 +227,10 @@ function createDevtoolsServer(opts = {}) {
204
227
  if (head?.length) socket.unshift(head);
205
228
  if (url.pathname === "/view") {
206
229
  viewers.add(conn);
207
- conn.onClose = () => viewers.delete(conn);
230
+ conn.onClose = () => {
231
+ viewers.delete(conn);
232
+ for (const [id, v] of pendingControl) if (v === conn) pendingControl.delete(id);
233
+ };
208
234
  conn.onMessage = (text) => {
209
235
  let msg;
210
236
  try {
@@ -221,6 +247,20 @@ function createDevtoolsServer(opts = {}) {
221
247
  if (!s || s.pinned === msg.pinned) return;
222
248
  s.pinned = msg.pinned;
223
249
  broadcast({ type: "sessions", sessions: sessionList() });
250
+ } else if (msg.type === "control") {
251
+ const app = [...sessions].find(([, s]) => s.id === msg.sessionId)?.[0];
252
+ if (!app) {
253
+ conn.send(JSON.stringify({
254
+ type: "control-result",
255
+ sessionId: msg.sessionId,
256
+ id: msg.id,
257
+ ok: false,
258
+ error: "session is not live"
259
+ }));
260
+ return;
261
+ }
262
+ pendingControl.set(msg.id, conn);
263
+ app.send(JSON.stringify(msg));
224
264
  }
225
265
  };
226
266
  conn.send(JSON.stringify({ type: "sessions", sessions: sessionList() }));
@@ -242,11 +282,26 @@ function createDevtoolsServer(opts = {}) {
242
282
  return;
243
283
  }
244
284
  if (msg.type === "batch" && greeted) {
245
- broadcast({ type: "batch", session: sessions.get(conn), events: msg.events });
285
+ const session = sessions.get(conn);
286
+ broadcast({ type: "batch", session, events: msg.events });
287
+ if (otlp && Array.isArray(msg.events)) {
288
+ otlpSessions.set(session.id, session);
289
+ const recv = Date.now();
290
+ for (const ev of msg.events) otlp.ingest(ev, recv, session.id);
291
+ }
292
+ }
293
+ if (msg.type === "control-result") {
294
+ const viewer = pendingControl.get(msg.id);
295
+ pendingControl.delete(msg.id);
296
+ if (viewer && viewers.has(viewer)) viewer.send(JSON.stringify(msg));
246
297
  }
247
298
  };
248
299
  conn.onClose = () => {
249
300
  const info = sessions.get(conn);
301
+ if (info && otlp) {
302
+ otlp.release(info.id);
303
+ otlpSessions.delete(info.id);
304
+ }
250
305
  if (sessions.delete(conn) && info) {
251
306
  info.closed = true;
252
307
  info.closedAt = Date.now();
@@ -285,7 +340,10 @@ function createDevtoolsServer(opts = {}) {
285
340
  clearInterval(sweep);
286
341
  for (const v of viewers) v.close();
287
342
  for (const c of sessions.keys()) c.close();
288
- server.close(() => done());
343
+ server.close(() => {
344
+ if (otlp) void otlp.close().then(() => done());
345
+ else done();
346
+ });
289
347
  })
290
348
  });
291
349
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@atolljs/devtools",
3
- "version": "0.1.7",
3
+ "version": "0.1.8",
4
4
  "description": "Devtools transport + local dashboard server for AtollJS instrumentation events",
5
5
  "homepage": "https://jwhenry3.github.io/atolljs/consumer/quickstart/",
6
6
  "type": "module",
@@ -13,6 +13,10 @@
13
13
  "types": "./dist/node.d.ts",
14
14
  "default": "./dist/node.js"
15
15
  },
16
+ "./otel": {
17
+ "types": "./dist/otel.d.ts",
18
+ "default": "./dist/otel.js"
19
+ },
16
20
  "./server": {
17
21
  "types": "./dist/server.d.ts",
18
22
  "default": "./dist/server.js"
@@ -35,7 +39,7 @@
35
39
  "registry": "https://registry.npmjs.org"
36
40
  },
37
41
  "peerDependencies": {
38
- "@atolljs/core": "0.1.7"
42
+ "@atolljs/core": "0.1.8"
39
43
  },
40
44
  "repository": {
41
45
  "type": "git",
package/src/bin.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- /** `atoll-devtools [--port N]` — boot the local devtools server + dashboard. */
2
+ /** `atoll-devtools [--port N] [--otlp URL]` — boot the local devtools server + dashboard. */
3
3
  import { createDevtoolsServer } from './server';
4
4
 
5
5
  const arg = (name: string): string | undefined => {
@@ -7,17 +7,42 @@ const arg = (name: string): string | undefined => {
7
7
  return i >= 0 ? process.argv[i + 1] : undefined;
8
8
  };
9
9
 
10
+ /** `k=v,k2=v2` (the OTEL_EXPORTER_OTLP_HEADERS format) → a header record. */
11
+ const parseHeaders = (list = ''): Record<string, string> =>
12
+ Object.fromEntries(
13
+ list
14
+ .split(',')
15
+ .filter((h) => h.includes('='))
16
+ .map((h) => [h.slice(0, h.indexOf('=')).trim(), h.slice(h.indexOf('=') + 1).trim()])
17
+ .filter(([k]) => k !== ''),
18
+ );
19
+
10
20
  if (process.argv.includes('--help') || process.argv.includes('-h')) {
11
- console.log('usage: atoll-devtools [--port N] — serve the devtools dashboard + event ingest (default port 4780)');
21
+ console.log(
22
+ 'usage: atoll-devtools [--port N] [--otlp URL [--otlp-headers k=v,k2=v2] [--otlp-service NAME]]\n' +
23
+ ' serve the devtools dashboard + event ingest (default port 4780);\n' +
24
+ ' --otlp also forwards every session to an OTLP/HTTP endpoint (e.g. http://localhost:4318)',
25
+ );
12
26
  process.exit(0);
13
27
  }
14
28
 
15
29
  const port = Number(arg('port') ?? 4780);
30
+ const otlpEndpoint = arg('otlp');
31
+ const headers = parseHeaders(arg('otlp-headers'));
16
32
  const server = await createDevtoolsServer({
17
33
  port: Number.isFinite(port) ? port : 4780,
34
+ otlp: otlpEndpoint
35
+ ? {
36
+ endpoint: otlpEndpoint,
37
+ headers,
38
+ serviceName: arg('otlp-service'),
39
+ onError: (err) => console.warn(`otlp: ${err.message}`),
40
+ }
41
+ : undefined,
18
42
  });
19
43
 
20
44
  console.log(`atoll devtools → ${server.url}`);
21
45
  console.log(' dashboard: ' + server.url);
22
46
  console.log(' ingest: ' + server.url.replace('http://', 'ws://') + '/events');
47
+ if (otlpEndpoint) console.log(' otlp: ' + otlpEndpoint);
23
48
  console.log('\nIn the app: import { connectDevtools } from \'@atolljs/devtools\'; connectDevtools()');
package/src/broadcast.ts CHANGED
@@ -15,6 +15,8 @@ import {
15
15
  setDevtoolsSink,
16
16
  type EmittedDevtoolsEvent,
17
17
  } from '@atolljs/core';
18
+ import { answerControl } from './control';
19
+ import { installJankProbe } from './probes';
18
20
  import { DEVTOOLS_CHANNEL, type BroadcastMessage, type SessionInfo } from './protocol';
19
21
  import type { ConnectDevtoolsOptions, DevtoolsConnection } from './client';
20
22
 
@@ -54,6 +56,8 @@ export function connectBroadcast(
54
56
  if (m.data?.type === 'view') {
55
57
  hello();
56
58
  for (const events of tail) post({ type: 'batch', session, events });
59
+ } else if (m.data?.type === 'control' && m.data.sessionId === session.id) {
60
+ void answerControl(m.data, (res) => post(res));
57
61
  }
58
62
  };
59
63
 
@@ -69,6 +73,7 @@ export function connectBroadcast(
69
73
 
70
74
  if (opts.network !== false) installFetchProbe(globalThis);
71
75
  if (opts.memory !== false) installMemoryProbe(globalThis);
76
+ const releaseJank = opts.jank !== false ? installJankProbe() : () => {};
72
77
 
73
78
  setDevtoolsSink((event) => {
74
79
  if (closed) return;
@@ -90,6 +95,7 @@ export function connectBroadcast(
90
95
  bye();
91
96
  setDevtoolsSink(null);
92
97
  clearInterval(timer);
98
+ releaseJank();
93
99
  if (typeof removeEventListener === 'function') removeEventListener('pagehide', onHide);
94
100
  bc.close();
95
101
  },
package/src/client.ts CHANGED
@@ -24,7 +24,9 @@ import {
24
24
  type EmittedDevtoolsEvent,
25
25
  } from '@atolljs/core';
26
26
  import { connectBroadcast } from './broadcast';
27
- import type { ClientMessage, SessionInfo } from './protocol';
27
+ import { answerControl, captureEnv } from './control';
28
+ import { installJankProbe } from './probes';
29
+ import type { AppMessage, ClientMessage, SessionInfo } from './protocol';
28
30
 
29
31
  export interface ConnectDevtoolsOptions {
30
32
  /**
@@ -37,7 +39,7 @@ export interface ConnectDevtoolsOptions {
37
39
  /** Server ingest endpoint — implies websocket transport. */
38
40
  url?: string;
39
41
  /** Session identity shown in the dashboard. */
40
- session?: { name?: string; hint?: string };
42
+ session?: { name?: string; hint?: string; framework?: string };
41
43
  /** Batch flush interval ms (default 100). */
42
44
  flushMs?: number;
43
45
  /** Events buffered while disconnected (default 50_000 — oldest dropped). */
@@ -48,6 +50,12 @@ export interface ConnectDevtoolsOptions {
48
50
  network?: boolean;
49
51
  /** Sample JS heap usage periodically — Chrome/Blink only (default true). */
50
52
  memory?: boolean;
53
+ /**
54
+ * Main-thread jank probe (default true): long frames (`runtime:longframe`,
55
+ * Long Animation Frames or `longtask`) and a rAF fps sampler
56
+ * (`runtime:frames`). Browser main thread only.
57
+ */
58
+ jank?: boolean;
51
59
  }
52
60
 
53
61
  export interface DevtoolsConnection {
@@ -70,8 +78,10 @@ const makeSession = (opts: ConnectDevtoolsOptions): SessionInfo => {
70
78
  return {
71
79
  id: `s-${Date.now().toString(36)}-${++sessionSeq}`,
72
80
  name: opts.session?.name,
81
+ framework: opts.session?.framework,
73
82
  runtime,
74
83
  hint,
84
+ env: captureEnv(),
75
85
  };
76
86
  };
77
87
 
@@ -118,6 +128,16 @@ function connectWebSocket(opts: ConnectDevtoolsOptions, session: SessionInfo): D
118
128
  flush();
119
129
  };
120
130
 
131
+ const onMessage = (text: string) => {
132
+ let msg: AppMessage;
133
+ try {
134
+ msg = JSON.parse(text);
135
+ } catch {
136
+ return;
137
+ }
138
+ if (msg.type === 'control') void answerControl(msg, (res) => send(res));
139
+ };
140
+
121
141
  const isNode = typeof window === 'undefined' || typeof window.document === 'undefined';
122
142
  if (typeof WebSocket !== 'undefined') {
123
143
  const ws = new WebSocket(url);
@@ -129,6 +149,7 @@ function connectWebSocket(opts: ConnectDevtoolsOptions, session: SessionInfo): D
129
149
  close: () => ws.close(),
130
150
  };
131
151
  ws.onopen = onOpen;
152
+ ws.onmessage = (m: MessageEvent) => onMessage(String(m.data));
132
153
  ws.onclose = () => clearInterval(timer);
133
154
  ws.onerror = () => ws.close();
134
155
  } else if (isNode) {
@@ -142,6 +163,7 @@ function connectWebSocket(opts: ConnectDevtoolsOptions, session: SessionInfo): D
142
163
  const ws = m.connectNodeWebSocket(url);
143
164
  sock = ws;
144
165
  ws.onOpen = onOpen;
166
+ ws.onMessage = onMessage;
145
167
  ws.onClose = () => clearInterval(timer);
146
168
  }).catch(() => {
147
169
  /* no net available — sink stays installed but dead */
@@ -154,6 +176,7 @@ function connectWebSocket(opts: ConnectDevtoolsOptions, session: SessionInfo): D
154
176
 
155
177
  if (opts.network !== false) installFetchProbe(globalThis);
156
178
  if (opts.memory !== false) installMemoryProbe(globalThis);
179
+ const releaseJank = opts.jank !== false ? installJankProbe() : () => {};
157
180
 
158
181
  setDevtoolsSink((event) => {
159
182
  if (closed) return;
@@ -173,6 +196,7 @@ function connectWebSocket(opts: ConnectDevtoolsOptions, session: SessionInfo): D
173
196
  flush();
174
197
  sock?.close();
175
198
  clearInterval(timer);
199
+ releaseJank();
176
200
  },
177
201
  };
178
202
  }
package/src/control.ts ADDED
@@ -0,0 +1,51 @@
1
+ /**
2
+ * App side of the control channel: answer a dashboard `control` frame by
3
+ * running the named command (`registerDevtoolsCommand` in @atolljs/core)
4
+ * and replying with a `control-result`. Both transports share this.
5
+ */
6
+ import { runDevtoolsCommand } from '@atolljs/core';
7
+ import type { ControlRequest, ControlResult, SessionEnv } from './protocol';
8
+
9
+ /** Make a command result transport-safe: JSON round-trip, functions dropped. */
10
+ const jsonSafe = (v: unknown): unknown => {
11
+ if (v === undefined) return undefined;
12
+ try {
13
+ return JSON.parse(JSON.stringify(v));
14
+ } catch {
15
+ return String(v);
16
+ }
17
+ };
18
+
19
+ export const answerControl = async (
20
+ req: ControlRequest,
21
+ reply: (res: ControlResult) => void,
22
+ ): Promise<void> => {
23
+ try {
24
+ const result = await runDevtoolsCommand(req.cmd, req.args ?? {});
25
+ reply({ type: 'control-result', sessionId: req.sessionId, id: req.id, ok: true, result: jsonSafe(result) });
26
+ } catch (err) {
27
+ reply({
28
+ type: 'control-result',
29
+ sessionId: req.sessionId,
30
+ id: req.id,
31
+ ok: false,
32
+ error: err instanceof Error ? err.message : String(err),
33
+ });
34
+ }
35
+ };
36
+
37
+ /** Runtime facts for the session's `env` — what the audits panel reasons over. */
38
+ export const captureEnv = (): SessionEnv => {
39
+ const g = globalThis as {
40
+ crossOriginIsolated?: boolean;
41
+ navigator?: { hardwareConcurrency?: number; userAgent?: string };
42
+ process?: { version?: string };
43
+ };
44
+ return {
45
+ crossOriginIsolated: g.crossOriginIsolated,
46
+ sharedArrayBuffer: typeof SharedArrayBuffer !== 'undefined',
47
+ hardwareConcurrency: g.navigator?.hardwareConcurrency,
48
+ userAgent: g.navigator?.userAgent,
49
+ node: g.navigator ? undefined : g.process?.version,
50
+ };
51
+ };
package/src/nodeWs.ts CHANGED
@@ -21,6 +21,7 @@ export interface NodeWebSocket {
21
21
  close(): void;
22
22
  onOpen: (() => void) | null;
23
23
  onClose: (() => void) | null;
24
+ onMessage: ((text: string) => void) | null;
24
25
  }
25
26
 
26
27
  /** Connect a WebSocket to `url` — handshake, then framed text IO. */
@@ -62,6 +63,7 @@ export function connectNodeWebSocket(url: string): NodeWebSocket {
62
63
  },
63
64
  onOpen: null,
64
65
  onClose: null,
66
+ onMessage: null,
65
67
  };
66
68
 
67
69
  const die = () => {
@@ -108,7 +110,11 @@ export function connectNodeWebSocket(url: string): NodeWebSocket {
108
110
  if (opcode === OPCODES.pong) continue;
109
111
  if (opcode === OPCODES.text || opcode === OPCODES.continuation) {
110
112
  fragments.push(payload);
111
- if (fin) fragments = []; // client.ts never reads frames — drain only
113
+ if (fin) {
114
+ const text = Buffer.concat(fragments).toString('utf8');
115
+ fragments = [];
116
+ conn.onMessage?.(text);
117
+ }
112
118
  }
113
119
  }
114
120
  });
package/src/otel.ts ADDED
@@ -0,0 +1,140 @@
1
+ /**
2
+ * `@atolljs/devtools/otel` — ship atoll instrumentation to any OpenTelemetry
3
+ * backend (Collector, Jaeger, Tempo, Honeycomb, Grafana) over OTLP/HTTP
4
+ * with JSON encoding. Zero dependencies: `fetch` + the mapping in otelMap.ts.
5
+ *
6
+ * import { exportOtel } from '@atolljs/devtools/otel';
7
+ * const otel = exportOtel({ serviceName: 'my-app' }); // → http://localhost:4318
8
+ *
9
+ * Installs as an additive devtools listener (`addDevtoolsSink`), so it runs
10
+ * alongside `connectDevtools()` or alone. Call it before pools spawn: like
11
+ * the dashboard transports, worker-side events forward only when a
12
+ * listener existed at the worker's INIT handshake.
13
+ */
14
+ import { addDevtoolsSink, installFetchProbe, installMemoryProbe } from '@atolljs/core';
15
+ import { OtelMapper, type OtelMapperOptions, type OtelSignals } from './otelMap';
16
+ import {
17
+ createOtelPipeline,
18
+ createOtlpSender,
19
+ localResource,
20
+ type OtlpSenderOptions,
21
+ type ResourceAttributes,
22
+ } from './otelTransport';
23
+ import { installJankProbe } from './probes';
24
+
25
+ export interface ExportOtelOptions extends OtlpSenderOptions {
26
+ /** Resource `service.name` (default 'unknown_service'). */
27
+ serviceName?: string;
28
+ /** Extra resource attributes, merged over the detected ones. */
29
+ resource?: ResourceAttributes;
30
+ /** Signals to export: all on by default. */
31
+ signals?: OtelSignals;
32
+ /** Periodic flush interval (default 5000ms). */
33
+ flushIntervalMs?: number;
34
+ /** Spans / log records per request (default 512). */
35
+ maxBatch?: number;
36
+ /** Queued spans and log records each before the oldest drop (default 2048). */
37
+ maxQueue?: number;
38
+ /** Series per metric before new attribute sets drop (default 2000). */
39
+ maxSeries?: number;
40
+ /** Patch global fetch for `net:fetch` spans (default true). */
41
+ network?: boolean;
42
+ /** Sample JS heap for `atoll.runtime.heap` (default true). */
43
+ memory?: boolean;
44
+ /** Browser main thread: long-frame / fps probe (default true). */
45
+ jank?: boolean;
46
+ }
47
+
48
+ export interface OtelExporter {
49
+ /** Ship everything queued now (plus a metrics snapshot). Never rejects. */
50
+ flush(): Promise<void>;
51
+ /** Uninstall the listener and lifecycle hooks, then flush. Never rejects. */
52
+ close(): Promise<void>;
53
+ }
54
+
55
+ export function exportOtel(opts: ExportOtelOptions = {}): OtelExporter {
56
+ // The sender captures the global fetch now, before the probe below wraps
57
+ // it, so the exporter's own posts never become net:fetch spans.
58
+ const sender = createOtlpSender(opts);
59
+ const endpoint = sender.endpoint;
60
+ const perf = globalThis.performance;
61
+ const originMs = perf.timeOrigin ?? Date.now() - perf.now();
62
+ const mapperOpts: OtelMapperOptions = {
63
+ resource: localResource(opts.serviceName, opts.resource),
64
+ originMs,
65
+ signals: opts.signals,
66
+ maxQueue: opts.maxQueue,
67
+ maxSeries: opts.maxSeries,
68
+ // An earlier probe (connectDevtools) may already wrap the captured fetch.
69
+ ignoreUrl: (url) => url.startsWith(endpoint),
70
+ };
71
+ const pipeline = createOtelPipeline({
72
+ sender,
73
+ newMapper: () => new OtelMapper(mapperOpts),
74
+ flushIntervalMs: opts.flushIntervalMs,
75
+ maxBatch: opts.maxBatch,
76
+ signals: opts.signals,
77
+ });
78
+
79
+ const remove = addDevtoolsSink((ev) => pipeline.ingest(ev, originMs + perf.now()));
80
+
81
+ if (opts.network !== false) installFetchProbe(globalThis);
82
+ if (opts.memory !== false) installMemoryProbe(globalThis);
83
+ const releaseJank = opts.jank !== false ? installJankProbe() : () => {};
84
+
85
+ // Last-chance flushes: keepalive fetch survives page unload (sendBeacon
86
+ // can't carry auth headers or a JSON content-type cross-origin).
87
+ type Listenable = {
88
+ addEventListener?: (t: string, l: () => void) => void;
89
+ removeEventListener?: (t: string, l: () => void) => void;
90
+ };
91
+ const g = globalThis as Listenable & {
92
+ document?: Listenable & { visibilityState?: string };
93
+ process?: {
94
+ once?: (e: string, l: () => void) => void;
95
+ off?: (e: string, l: () => void) => void;
96
+ versions?: { node?: string };
97
+ };
98
+ };
99
+ const onHide = () => {
100
+ if (g.document?.visibilityState === 'hidden') void pipeline.flush({ keepalive: true });
101
+ };
102
+ const onPageHide = () => void pipeline.flush({ keepalive: true });
103
+ const inBrowser = typeof g.document !== 'undefined' && typeof g.addEventListener === 'function';
104
+ if (inBrowser) {
105
+ g.addEventListener!('pagehide', onPageHide);
106
+ g.document!.addEventListener?.('visibilitychange', onHide);
107
+ }
108
+ const onBeforeExit = () => void pipeline.flush();
109
+ const inNode = !inBrowser && typeof g.process?.once === 'function' && g.process.versions?.node !== undefined;
110
+ if (inNode) g.process!.once!('beforeExit', onBeforeExit);
111
+
112
+ let closed = false;
113
+ return {
114
+ flush: () => pipeline.flush(),
115
+ async close() {
116
+ if (closed) return;
117
+ closed = true;
118
+ remove();
119
+ releaseJank();
120
+ if (inBrowser) {
121
+ g.removeEventListener?.('pagehide', onPageHide);
122
+ g.document?.removeEventListener?.('visibilitychange', onHide);
123
+ }
124
+ if (inNode) g.process!.off?.('beforeExit', onBeforeExit);
125
+ await pipeline.close();
126
+ },
127
+ };
128
+ }
129
+
130
+ export { OtelMapper, SEVERITY, newSpanId, newTraceId, toUnixNano } from './otelMap';
131
+ export type { KeyValue, OtelMapperOptions, OtelSignals, OtlpLogRecord, OtlpMetric, OtlpSpan } from './otelMap';
132
+ export { createOtelPipeline, createOtlpSender, localResource, sessionResource } from './otelTransport';
133
+ export type {
134
+ OtelPipeline,
135
+ OtelPipelineOptions,
136
+ OtlpSender,
137
+ OtlpSenderOptions,
138
+ OtlpSignal,
139
+ ResourceAttributes,
140
+ } from './otelTransport';