Add anonymous usage telemetry - #125
Conversation
Studio sends no usage data today, so errors in the field stay invisible. The new telemetry module queues events and posts them in batches to beacon-datalake.org. It captures page views, query runs, console warnings and errors, and toasts. It sends no query content and no account. A System setting switches it off. The feedback form carries the install id and session id only while telemetry is on, so a report can join the event table.
There was a problem hiding this comment.
🟡 Changes recommended
Unresolved critical privacy and opt-out handling issues remain.
Get a fresh assessment by requesting another Copilot review.
Pull request overview
Adds client-side usage telemetry with batched delivery, opt-out controls, and feedback diagnostics.
Changes:
- Adds telemetry identity, sessions, queues, event types, and console capture.
- Instruments navigation, queries, visualisations, nodes, toasts, and feedback.
- Adds telemetry settings and updates UUID and service URL handling.
File summaries
| File | Changes | Review comments |
|---|---|---|
src/routes/visualisations/table-explorer/+page.svelte |
Tracks table visualisation events. | No additional review comments. |
src/routes/beacon-nodes/+page.svelte |
Updates the public nodes link. | No additional review comments. |
src/routes/+layout.svelte |
Starts telemetry and tracks page views. | No additional review comments. |
src/lib/utils.ts |
Adds a Web Crypto UUID fallback. | No additional review comments. |
src/lib/telemetry/types.ts |
Defines telemetry event shapes. | No additional review comments. |
src/lib/telemetry/session.ts |
Manages telemetry session tokens. | No additional review comments. |
src/lib/telemetry/queue.ts |
Buffers and sends event batches. | Moderate (3): Make token acquisition unload-safe. Moderate (1): Add fetch timeouts so stalled requests cannot block future flushes. |
src/lib/telemetry/index.ts |
Exposes telemetry controls and tracks node data. | Critical (3): Keep DEV_OVERRIDE disabled in committed code; it currently enables development telemetry and console patching, including at lines 66 and 81. Moderate (3): Normalize node URLs to remove paths. |
src/lib/telemetry/identity.ts |
Manages client identity values. | Moderate (1): Guard localStorage access. Moderate (1): Guard sessionStorage access. |
src/lib/telemetry/console.ts |
Captures console output. | Critical (2): Redact arbitrary object payloads or make capture opt-in to prevent query or filter data leakage. |
src/lib/stores/toasts.ts |
Emits toast events. | Critical (1): Send safe summaries instead of raw toast messages. |
src/lib/stores/stored-query.ts |
Uses the UUID helper for query IDs. | Nit (1): Remove whitespace before the semicolon to satisfy formatting checks. |
src/lib/stores/settings.ts |
Adds telemetry settings. | Moderate (2): Use privacy-accurate wording or stop persisting install_id. Critical (1): Opt-out must clear buffered events and call forgetIdentity. |
src/lib/stores/query-store.svelte.ts |
Tracks query execution and downloads. | Nit (3): Remove the unused uuidv4 import. Moderate (1): Redact error.message or send only an error class. Moderate (1): Use null for absent server query IDs rather than client-generated UUIDs. |
src/lib/services/open-nodes.ts |
Updates the public nodes API. | No additional review comments. |
src/lib/services/beacon-node.ts |
Tracks node selection. | No additional review comments. |
src/lib/data/home-examples.ts |
Updates the examples API. | No additional review comments. |
src/lib/components/visualisation/MapViewController.svelte.ts |
Tracks map visualisations. | No additional review comments. |
src/lib/components/sidebar/AppSidebar.svelte |
Updates the Studio link. | No additional review comments. |
src/lib/components/plots/ChartExplorerController.svelte.ts |
Tracks chart visualisations. | No additional review comments. |
src/lib/components/modals/FeedbackModal.svelte |
Adds conditional feedback diagnostics. | No additional review comments. |
Review details
Suppressed comments (8)
src/lib/stores/query-store.svelte.ts:249
- This uploads
error.messagewithout filtering. HTTP response text can echo query values. Redact the message or send only an error class.
message: error instanceof Error ? error.message : String(error)
src/lib/stores/query-store.svelte.ts:583
- This UUID is client-generated, but telemetry treats it as a server query id. CORS failures cannot join Beacon logs. Use null when absent.
const queryId = response.headers.get('x-beacon-query-id') ?? Utils.randomUUID(); // generate a UUID if the server didn't provide one, or is blocked by CORS
src/lib/stores/stored-query.ts:139
- This line has whitespace before the semicolon.
npm run lintruns Prettier onsrc/, so the format check rejects this file.
return `sq-${Utils.randomUUID()}` ;
src/lib/telemetry/identity.ts:47
- The
localStoragegetter can throw beforereadOrCreateenters itstryblock. In browsers that block storage, this skips the fallback UUID and makes telemetry diagnostics empty. Guard the storage access itself.
const stored = readOrCreate(globalThis.localStorage, INSTALL_KEY);
memoryInstallId = stored ?? Utils.randomUUID();
src/lib/telemetry/identity.ts:57
- The
sessionStoragegetter has the same uncaught access path. A storage-blocked browser cannot create the intended in-memory session ID. Guard this getter as well.
const stored = readOrCreate(globalThis.sessionStorage, SESSION_KEY);
memorySessionId = stored ?? Utils.randomUUID();
src/lib/telemetry/index.ts:66
- Turning
telemetryEnabledoff stops new events, but it does not clear events already inbuffer. The queue timer still posts those events, so the opt-out can send data after the user disables telemetry. Clear the queue when this setting changes, or recheck the setting before every POST.
if (!getSettings().telemetryEnabled) return;
src/lib/telemetry/index.ts:81
messageforwards raw toast, console, and query-error text. Existing callers include query names and server response bodies, so telemetry can send query content. Redact or omit this field before enqueue.
message: fields.message ?? null,
src/lib/telemetry/queue.ts:115
- Neither the session request nor this collect request has a timeout. A stalled fetch leaves
sendingtrue forever, so later events never flush. Add an abort timeout.
const response = await fetch(collectUrl(), {
- Files reviewed: 21/21 changed files
- Comments generated: 8
- Review effort level: Lite
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
| try { | ||
| return JSON.stringify(arg) ?? '[object]'; |
| const events = buffer.splice(0, MAX_EVENTS_PER_BATCH); | ||
|
|
||
| void post(events, true); |
An opt-out kept the install id and could still send a buffered event. The settings store sits below telemetry, so it cannot call telemetry itself. A watcher in the telemetry module reacts to the store instead, which also covers a reset of one key and a reset of every key. Also folds Utils.uuidv4 into Utils.randomUUID, and calls the events pseudonymous, because one install id groups the events of one browser.
There was a problem hiding this comment.
🟡 Changes recommended
Unresolved critical privacy, opt-out, data-sanitization, and lint issues block approval.
Get a fresh assessment by requesting another Copilot review.
Review details
Suppressed comments (8)
Previously missed (1) — in code that hasn't changed since the last review.
src/lib/stores/query-store.svelte.ts:317
- entry.queryId is a random client UUID when the response header is absent, as the fallback below shows. Sending it as query_id creates an ID that cannot join the Beacon run log. Keep the server ID separate and send null when absent.
src/lib/stores/toasts.ts:31
- This sends every toast message. Save toasts include user-controlled query names, so telemetry can receive query content despite the setting promise. Omit or redact
message.
track(`toast.${toastData.type}`, {
level: toastData.type,
message: toastData.message,
});
src/lib/telemetry/console.ts:42
- JSON.stringify serializes arbitrary warning and error arguments. Existing warnings can log cloned query objects, so this uploads query content despite the telemetry contract. Redact or allowlist non-string arguments.
return JSON.stringify(arg) ?? '[object]';
src/lib/telemetry/identity.ts:8
- install_id persists in localStorage and groups events across sessions, so this telemetry is pseudonymous, not anonymous. Update the PR title and description to state that retention.
* Neither id holds a name, an email or an account. Both are random. They are
* pseudonymous, not anonymous: one installId groups the events of one browser.
src/lib/telemetry/identity.ts:46
- If the browser throws while evaluating
globalThis.localStorage,readOrCreatenever runs. The promised memory fallback then fails, and telemetry silently drops every event.
const stored = readOrCreate(globalThis.localStorage, INSTALL_KEY);
src/lib/telemetry/index.ts:95
- The server limit does not protect the client. This enqueues unbounded error or toast text, which can inflate memory and request size. Truncate before queueing.
message: fields.message ?? null,
src/lib/telemetry/queue.ts:84
- On a first visit,
postmints token withoutkeepalive. Unload can cancel mint after batch leavesbuffer, so short sessions lose events. Mint earlier.
const events = buffer.splice(0, MAX_EVENTS_PER_BATCH);
void post(events, true);
src/lib/telemetry/queue.ts:84
keepalivelimits request bodies to about 64 KiB, but this takes 50 events. Large valid messages can fail here, andflushOnHidedrops the batch.
const events = buffer.splice(0, MAX_EVENTS_PER_BATCH);
void post(events, true);
- Files reviewed: 22/22 changed files
- Comments generated: 5
- Review effort level: Lite
| const fields: Record<string, string | null | undefined> = { | ||
| ...diagnostics(), | ||
| route: page.route.id, | ||
| node: node?.url, |
| track('query.error', { | ||
| nodeHost: node.url, | ||
| message: error instanceof Error ? error.message : String(error) |
| return crypto.randomUUID(); | ||
| } | ||
| return `sq-${Date.now()}-${Math.random().toString(16).slice(2)}`; | ||
| return `sq-${Utils.randomUUID()}` ; |
| async function post(events: TelemetryEvent[], keepalive = false): Promise<boolean> { | ||
| const token = await getToken(); | ||
|
|
||
| if (!token) return false; |
| /** Free text. The server cuts it to 500 characters. */ | ||
| message?: string; | ||
| durationMs?: number; | ||
| rowCount?: number; | ||
| /** Small, non-personal extras. The server drops the object above 2000 bytes. */ | ||
| props?: Record<string, unknown>; |
Studio sends no usage data today, so errors in the field stay invisible. The new telemetry module queues events and posts them in batches to beacon-datalake.org. It captures page views, query runs, console warnings and errors, and toasts. It sends no query content and no account.
A System setting switches it off. The feedback form carries the install id and session id only while telemetry is on, so a report can join the event table.