Skip to content

Add anonymous usage telemetry - #125

Merged
sharppaul merged 2 commits into
mainfrom
feature/telemetry
Sep 16, 2026
Merged

sharppaul merged 2 commits into
mainfrom
feature/telemetry

Conversation

@sharppaul

Copy link
Copy Markdown
Contributor

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.

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.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 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.message without 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 lint runs Prettier on src/, so the format check rejects this file.
	return `sq-${Utils.randomUUID()}` ;

src/lib/telemetry/identity.ts:47

  • The localStorage getter can throw before readOrCreate enters its try block. 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 sessionStorage getter 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 telemetryEnabled off stops new events, but it does not clear events already in buffer. 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

  • message forwards 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 sending true 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.

Comment thread src/lib/stores/settings.ts
Comment thread src/lib/stores/toasts.ts
Comment on lines +41 to +42
try {
return JSON.stringify(arg) ?? '[object]';
Comment thread src/lib/telemetry/index.ts Outdated
Comment thread src/lib/stores/settings.ts Outdated
Comment thread src/lib/telemetry/index.ts Outdated
Comment on lines +77 to +79
const events = buffer.splice(0, MAX_EVENTS_PER_BATCH);

void post(events, true);
Comment thread src/lib/stores/query-store.svelte.ts
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.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 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, readOrCreate never 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, post mints token without keepalive. Unload can cancel mint after batch leaves buffer, 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

  • keepalive limits request bodies to about 64 KiB, but this takes 50 events. Large valid messages can fail here, and flushOnHide drops 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,
Comment on lines +246 to +248
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()}` ;
Comment on lines +113 to +116
async function post(events: TelemetryEvent[], keepalive = false): Promise<boolean> {
const token = await getToken();

if (!token) return false;
Comment on lines +44 to +49
/** 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>;
@sharppaul
sharppaul merged commit 8fbde09 into main Sep 16, 2026
1 check passed
@sharppaul
sharppaul deleted the feature/telemetry branch September 16, 2026 14:02
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants