G
YouTube TikTok Games

GenClass Telemetry and Privacy

What anonymous diagnostics the GenClass runtime sends by default, what it never sends, where they go, and every way to turn them off.

GenClass Runtime (@genclass/runtime), an open-source browser library that reports race conditions in web apps, sends anonymous diagnostics about its own decisions to its maintainers, by default on public websites; this page lists every event and field it sends, where they are stored and for how long, and every way to turn it off.

Version. Written for @genclass/runtime 0.3.0-beta.5 (2026-11-02), the version on GitHub main, in package.json and in the runtime field of every batch. The copy in an npm package describes that package's version. As of 2026-11-02, npm i @genclass/runtime@beta installs 0.3.0-beta.5 (the newest line; 0.3.0-beta.1 to beta.5 were released 2026-10-22 to 2026-11-02) and npm i @genclass/runtime installs the stable line, 0.2.3 (latest, 2026-10-28). Items marked 0.3.0 are not in 0.2.x; interaction analytics are marked 0.3.0-beta.1, visits and navigation paths 0.3.0-beta.2.

Since 0.1.0-beta.3, GenClass.init() in a browser sends anonymous diagnostics about GenClass's own decisions to the GenClass maintainers (Daybot Solutions Inc.). We use them to measure and improve the GenClass model (accuracy, false interventions, latency, which situations it sees in real apps).

Turn it off: GenClass.init({ telemetry: false }) (or npx @genclass/runtime@beta init --no-telemetry while 0.3.0 is in beta; on 0.2.x, the same without @beta), ?genclass=no-telemetry in the URL, or localStorage.setItem("genclass.telemetry", "off"). Browsers that send Global Privacy Control are never collected. Details under Opting out.

When it is on

how GenClass starts default
GenClass.init(), import "@genclass/runtime/auto" (and /auto/*), the CDN script tag, npx @genclass/runtime@beta init setups on in a browser on a public host
the same, on a local or private host (localhost, *.localhost, *.local, *.test, *.internal, 127.0.0.1, ::1, 10/8, 172.16/12, 192.168/16, link-local, file:) off unless telemetry: true / { ... } (0.1.0-beta.3 to 0.2.0: on, with no local-host rule yet; 0.2.1: on, by mistake; fixed in 0.2.2, see the CHANGELOG)
GenClass.init() outside a browser (Node, SSR, workers) off unless telemetry: true / { ... }
createRuntime() (headless, tests, the simulator) off unless telemetry: true / { ... }

In 0.2.1 the local-host row did not apply to GenClass.init() (and so to /auto and the script tag): it passed an explicit telemetry: true internally, so local and private hosts sent too. Fixed in 0.2.2 (CHANGELOG).

When it is on, the console shows one notice per page:

[GenClass] Sends anonymous diagnostics (decisions and counts, no page text) to improve the model. Opt out: GenClass.init({ telemetry: false }) or ?genclass=no-telemetry.

runtime.telemetry reports { enabled, reason?, endpoint?, sessionId?, token?, flush() }; reason says why it is off (option, headless, local, url, localStorage, gpc, sampled-out, no-crypto, no-transport, kill-switch, disabled).

What is sent

(The coding-agent link, connectAgent from @genclass/runtime/agent, is not telemetry: it runs in development builds on development hosts and sends its recording only to a bridge on the same machine, http://127.0.0.1:47357. It changes nothing below. docs/runtime/AGENT.md section 6.)

Only data the runtime already computes for its own decisions. By default, no text from the page is sent: decision events carry the trigger kind, the diagnosis and its confidence, the chosen action, the gate, the outcome, latency and counts, plus the app's hostname and the id-normalised route (no query or fragment; from 0.3.0 a path segment holding an @, such as an e-mail address, is :id too).

  • The situation text the model read is sent only with include: { situation: true } (default off since 0.2.1). It is the text exactly as built for the model, after the redactor (InitOptions.redact, default: secret-named fields such as passwords, tokens, card numbers, API keys are replaced by [redacted]; see the README's Privacy section for the rules and their known gaps). It describes operations (e.g. GET /api/items/:id), store fields and short value summaries, timing and the facts GenClass computed. Before enabling it, know what it can contain: request names with their query strings (parameter values are redacted only when the parameter name looks secret), route changes with query string and fragment, the page title, the first 100 characters of your app's error messages, the first 40 characters typed into ordinary inputs (a name or an email typed into a field called "Email"), fragments of WebSocket / SSE messages (up to 60 characters) and store leaf values up to 48 characters, including state the runtime discovered on its own (autoState: React, Redux or Zustand state the app never registered; those decisions carry autoState: true). Add a redact function for fields with ordinary names and personal values.
  • Interaction analytics (new in 0.3.0-beta.1; design): off unless the app turns them on with analytics: true, and only with the app's token (or your own telemetry.endpoint). Then the page also sends interactions events: per page view, the route, engaged time, scroll depth milestones and clicks, rage clicks and dead clicks per element, the element named from markup (a data-genclass-name, a data-testid, an id, a link's target, stable class names or a short structural path), never its text; and (0.3.0-beta.2) per visit a random visit id kept 30 minutes, where the visit came from (the referring host, utm values), the device class, browser and OS families and whether the browser visited before (a flag in localStorage). See Interaction analytics.
  • Measured visual checks (VISUAL.md section 10; counts sent from 0.3.0-beta.5, approved by the maintainers on 2026-10-27): counts only. The session event says whether the checks run on the page (visual: off, local or on) and whether the app turned their runtime fixes on (fix). Once the checks have run on the page, every summary event carries how many findings each check made (counts.checks), the same per page path (counts.checksByRoute, the path normalised as on session, at most 20 paths), how many runtime fixes were applied and undone (counts.fixes), and how many passes ran, their check time in ms, how many ran out of their budget and the tier (runtime.visual). The findings themselves stay in the page (the console, rt.checks(), your own sinks): never a selector, element text, colour, URL, size or one event per finding. The checks are off on public hosts unless the app turns them on (visual: true), so most pages send visual: "local" and no counts. When they start with telemetry on, the console says so once:

[GenClass] The diagnostics also carry the measured visual checks' counts: findings per check and page, runtime fixes applied and undone, never selectors, page text, colours or URLs. - Never, in either setting: typed values of password, payment (cc-*), one-time-code or secret-named inputs (the runtime never records them in the first place), cookies, request or response headers, request or response bodies, storage contents, the sentences GenClass prints about what it changed, the measured visual checks' selectors, element text, colours, URLs, sizes and messages (only their counts, above), IP addresses or user agents (the collector does not store them).

Batch envelope (schema genclass-telemetry/1)

POST <endpoint> with content-type: text/plain;charset=UTF-8 (JSON body), no credentials, no referrer:

{
  "schema": "genclass-telemetry/1",
  "token": "gc_…",           // only when the app configured a valid token (InitOptions.token); see "App tokens"
  "sid": "3f9c…",            // 24 hex chars from crypto.getRandomValues, new on every page load, never stored
  "sent": 12034,             // ms since this page's GenClass started (no wall-clock time is sent)
  "runtime": "0.3.0-beta.5", // @genclass/runtime version
  "model": "2.0.0-rc4t",     // model card version, or null without a model
  "events": [ /* ≤ 100 events, ≤ 60 KB per request */ ]
}

Every event has t (type), seq (0, 1, 2, … per page) and at (ms since start). Event types:

t when fields
session once, at init see session event
model model state changes, and (0.3.0) once right after session when the model was skipped or failed before telemetry started see model event
status mode, aggressiveness or breaker changes mode, effectiveMode, aggressiveness, breaker
decision a model decision: always when its top diagnosis is not expected, it acted, it would have acted in shadow mode or it was vetoed; a sample of the routine ones (0.3.0; Volume) see decision event
detect a decision reported as a detection (from 0.3.0 also a "possible" one): every one, never sampled or capped see detect event
action an action ran, failed or was undone id, decision, action, tier, trigger, outcome (applied / failed / undone), late (a late revert), reversible, droppedFields (count)
veto onBeforeAction vetoed an action (every one) decision, action, enforced
breaker the breaker tripped or reset tripped, reason, undos, errors, decisions (ids)
model-error model errors (first 20 per page; all are counted) code, message (≤ 200 chars, URLs replaced by <url>: a model host's message can quote the browser's worker error, which names the script and the page origin)
summary about once a minute (≥ 60 s apart) while decisions continue, never capped; when the tab is hidden, on pagehide and on destroy see summary event
interactions only with analytics: true and a token (or a custom endpoint; 0.3.0-beta.1): when a page view ends, when the tab is hidden and before a summary, if anything changed see interactions event

Not sent: expectation reports (a learned step that did not happen, EXPECTATIONS.md) and anything else learned about the page (flows, profiles, relations); those stay in the browser (localStorage, rt.learned()).

Numbers are rounded to 4 decimals. The source of truth is packages/runtime/src/telemetry/client.ts.

session event

When: once, at init.

Fields:

  • runtime
  • host (the page's hostname, which identifies the app using GenClass)
  • route (the page path with id-like segments replaced by :id, and from 0.3.0 any segment holding an @ or %40, such as an e-mail address; no query or fragment)
  • mode
  • effectiveMode
  • aggressiveness
  • sampled
  • model (local / custom / off)
  • modelState
  • modelVersion
  • triage
  • shadow
  • holdWrites
  • device (webgpu available, cores, memoryGB (the browser's coarse deviceMemory), crossOriginIsolated, effectiveType, saveData)
  • sample
  • situation (whether situation text is included)
  • visual (0.3.0-beta.5: the measured visual checks off; local, the default on a public host, where they load only when the devtools overlay mounts or rt.checkNow() runs; or on, where they start after load: a development host, visual: true or a ?genclass-visual=on link) and fix (the app turned their runtime fixes on, visual: { on: true, fix }; they still run only in guard or heal)

Not the page's decisionSample or maxDecisions setting: summary.telemetry says how many records were sent and withheld (Volume).

model event

When: model state changes, and (0.3.0) once right after session when the model was skipped or failed before telemetry started (the device floor, 0.3.0: state skipped, reason device-memory / save-data / loadIf; with model.inlineFallback: false and no worker, also 0.3.0, reason worker-unavailable). On 0.2.x the model is skipped only when the app sets model.loadIf to skip, and a skip before telemetry started shows only as modelState on the session event, with no model event.

Fields:

  • state
  • version
  • model
  • variant
  • device (webgpu/wasm)
  • threads
  • worker
  • workerError (why the model worker was not used, URLs replaced by <url>)
  • gpu (WebGPU probe summary)
  • ort
  • loadMs
  • warmupMs
  • fromCache
  • bytes
  • phase
  • reason
  • error (GenClass model-host error text, ≤ 200 chars, URLs replaced by <url>)
  • attempts (variant, device and error per load attempt, URLs replaced by <url>)

decision event

When: a model decision: every one whose top diagnosis is not expected (detections and near-misses), that acted, that would have acted in shadow mode, or that was vetoed, always; a decisionSample share (default 1 in 4) of the routine ones (top diagnosis expected, nothing acted), at most maxDecisions (default 200) routine ones per page load (0.3.0; 0.2.x sends every decision record; Volume); all are counted in summary.

Fields:

  • id
  • trigger
  • route (the page path, normalised; for a decision deferred until the model loaded or the tab was visible again, 0.3.0, the path at its trigger)
  • model
  • latencyMs
  • held (the subject waited for the answer; false = decided in the background)
  • situation (text, see above)
  • autoState (true when discovered state was recorded on the page)
  • budget
  • compact
  • questions (question ids)
  • answers (per question: calibrated probabilities per label, choice, confidence; or p / score)
  • diagnosis
  • diagnosisConfidence
  • action (chosen)
  • confidence
  • tier
  • candidate
  • ran
  • executed
  • acted
  • reason (why the passive action ran)
  • mass
  • gateKind
  • threshold / gain / margin
  • thresholdSource
  • gates (report/guard/heal thresholds, aggressiveness, level and sources)
  • effectiveMode
  • shadow
  • fn (only on batches with a token: the name the app gave the protect()ed function the decision's subject ran inside, the outermost one; omitted otherwise)

detect event

When: a decision reported as a detection: every one, never sampled or capped, with its decision record; since 0.3.0 also observe's "possible" ones (InitOptions.detect: below the report gate, at or above the eager profile's report value).

Fields:

  • decision
  • trigger
  • diagnosis
  • p
  • tier (0.3.0: "possible" on a possible detection; absent on a likely one, as from earlier runtimes. It cannot be derived from the decision record: a possible detection is one whose p is below gates.report and at least the eager profile's report value (gates().possible, 0.70 with model 0.2.0), which the record does not carry, so tier says which side of that threshold a near-miss fell on. A decision deferred until the model loaded or the tab was visible again is "possible" whatever its p, until the false-report rate of those late decisions is measured. Nothing about the page)

summary event

When: about once a minute (≥ 60 s apart) while decisions continue, never capped; when the tab is hidden, on pagehide and on destroy.

Fields:

  • reason
  • counts (decisions, acted, detections (likely ones), possibleDetections (0.3.0: the possible ones, counted apart), actions applied/failed, late reverts, undos, vetoes, breaker trips, per trigger / diagnosis / action ran, detections per diagnosis, action limits hit, model errors per code, fail-opens per reason and trigger)
  • runtime (model state, p50/p95 latency, decisions, dropped, hidden-tab skips; held time total and max; denied and shadow counts; error count)
  • from 0.3.0-beta.5, once the measured visual checks have run on the page (their own file adds them, so first load carries no code for them): counts.checks (findings per check kind), counts.checksByRoute ({ [path]: { [kind]: n } }, at most 20 paths, normalised as on session), counts.fixes (applied, undone) and runtime.visual (passes, ms of check time, partial passes, tier full or light)
  • telemetry (queued, dropped, send failures, events sent; and, 0.3.0, decisionRecords (decisions whose records were sent), decisionsSampledOut and decisionsCapped (routine ones sampled out and capped))

interactions event

When: only with analytics: true and a token (or a custom endpoint; 0.3.0-beta.1): when a page view ends (a route change, page exit), when the tab is hidden, and before a summary, if anything changed (Interaction analytics).

Fields:

  • view (page view number in this page load)
  • route (normalised as on session)
  • start (the view's first event: a page view)
  • engagedMs
  • scroll (milestones 25 / 50 / 75 / 100 first reached since the previous event)
  • clicks, rage, dead (counts per element key, at most 50 / 20 / 20 keys, the rest under (other))
  • end (the view ended)

Counts are deltas since the view's previous event.

Visits (0.3.0-beta.2; Visits), on a view's first event only:

  • visit (16 hex characters, random)
  • and when that view opened the visit: entry: true; ref (the referring site's host, www. dropped, when it is another site; (other) when the host looks like data); utm (source, medium, campaign: lower case, letters, digits, . _ + - and spaces, words that look like ids dropped, at most 40 characters each); device (desktop / mobile / tablet); browser (Chrome, Safari, Firefox, Edge, Opera, Samsung Internet, Other); os (Windows, macOS, iOS, Android, Linux, ChromeOS, Other); returning (whether this browser had a visit before; absent when storage is blocked)

Navigation paths (0.3.0-beta.2; design):

  • on the view's first event, nav (how it was reached: load, reload, back_forward, push, back, forward, pop) and, from a page load's second view on, from (the previous view's route) and fromMs
  • once per view, first (the element key of the first click, or of the first focus on a form field after a key press) and firstMs
  • on the page load's last event (pagehide), exit: true and path (its routes in order, at most 10, then "(more)")

Volume

The built-in model takes up to 30 decisions a minute (model.maxDecisionsPerMinute), and in 0.2.2 and earlier every one of them was sent as a decision record with its answers, gates and shadow fields, so a busy tab left open kept uploading for as long as it stayed open. Two defaults bound the routine part of that. Neither changes a count, and neither ever drops a detection:

  • Detections are never sampled or capped. Every detect event is sent with the decision record it points to, and so is every decision whose top diagnosis is not expected (a near-miss below the report gate included), every decision that ran an action or would have in shadow mode, and every veto.
  • decisionSample (default 0.25): of the routine decisions (top diagnosis expected, passive action, no veto), one in four is sent. The draw is a hash of the session id and the decision id (mixed, so every page sends close to that share), not a random number at decision time.
  • maxDecisions (default 200): after 200 routine decisions' records in a page load, no more routine records are sent. Detections and the other events (summary, about once a minute while decisions continue, so the exact counts keep arriving; action, veto, model, status, breaker, model-error) continue. Summaries are not capped: their cost grows with how long a busy tab stays open, not with how many decisions it takes (see the table).

summary.counts count every decision, detection, action and veto whatever was sampled, and summary.telemetry says how many decisions' records were sent (decisionRecords), and how many routine ones were sampled out (decisionsSampledOut) and capped (decisionsCapped), so a collector can scale what it received. telemetry: { decisionSample: 1, maxDecisions: Infinity } sends every record (your own collector, debugging).

How much that is, measured in CI (RESULTS.md §10) by test/telemetry.test.ts "an hour at the built-in model's maximum rate": one page, 30 decisions a minute for a simulated hour (1,800 decisions from a scripted decider, not the model; situation text off), all bytes of all request bodies counted:

decisions defaults (0.3.0-beta.0: CI run 38471973596 with the detection tiers, and the release candidate's runs 38485063587, 38485289326 and 38488753971) decisionSample: 1, maxDecisions: Infinity (every record, as in 0.2.2 and earlier)
all routine 250.9 to 252.2 KB in 157 to 167 requests (which routine decisions are sampled, and so how many requests, follows the random session id) not measured
every one a detection 2,068.5 KB in 361 requests: every record (detections are never sampled or capped) 2,068.5 KB in 361 requests
of which summary events 61 summaries, 40.7 KB (routine) and 42.2 KB (detections) 61 summaries, 42.2 KB

The summaries are 1.4 KB an hour larger than in CI run 38465242176 (39.3 / 40.8 KB): each carries the new possibleDetections count. The scripted detections in this test are likely ones, which carry no tier.

A page where every decision is a detection sends every record, as 0.2.2 did: the detections are what the diagnostics are for. A page whose decisions are routine costs about 40 KB an hour in summaries after the cap, plus the batch envelopes, for as long as it stays open and keeps deciding; the test fails if that case reaches 300 KB. A real page takes far fewer decisions than this (only salient situations go to the model), so these are ceilings, not typical volumes.

Transport

  • Batches every 10 s (flushMs) of activity, and on pagehide / when the tab becomes hidden via navigator.sendBeacon (falling back to fetch with keepalive).
  • The fetch and sendBeacon captured when @genclass/runtime loaded, so GenClass never observes, holds or decides about its own requests.
  • At most 1,000 queued events; beyond that the oldest routine decision record (top diagnosis expected, nothing acted) makes room for a new event (since 0.3.0), else the new event is dropped; both are counted (summary.telemetry.dropped). A detection and its decision record are never dropped while a routine record is queued. A failed request (a network error, a 5xx) is dropped, never retried; after 3 failures in a row the page stops sending (MAX_FAIL_STREAK). Since 0.3.0 an HTTP 429, or a 503 with a Retry-After the page can read, is a back-off, not a failure: the refused batch goes back to the front of the queue and nothing is sent for 30 s, doubling per back-off in a row, or the Retry-After (in seconds) when it is longer, never more than 5 minutes (page exit still sends, by sendBeacon). After 8 back-offs in a row (about 27 minutes) a refusal counts as a failure. A Retry-After on any other status is ignored. Before 0.3.0 a busy network address (an office or campus behind one IP, against the collector's per-address limit) could stop its pages for good after three 429s. Telemetry never throws into your app, never changes what the model sees, and never delays a decision.

Interaction analytics

New in 0.3.0-beta.1. Off by default. An app turns them on for its own dashboard:

GenClass.init({ token: "gc_…", analytics: true });

(also data-analytics="on" on the script tag, <meta name="genclass" content="analytics=on">, window.GENCLASS_CONFIG.analytics). They need telemetry on and the app's token, or a custom telemetry.endpoint; without either, analytics: true warns once in the console and records nothing. Every telemetry opt-out below, Global Privacy Control and the local-host default turn them off too. When they start, the console says so once:

[GenClass] Interaction analytics are on (this app turned them on): page views and the path between them, visits, time on page, scroll depth, clicks per element, the referring site, campaign and device type, never page text.

What is recorded, per page view (a page load, and each route change to a new path):

  • page views and the route (the path as on session: id-like segments and segments with an @ as :id, no query, no fragment);
  • engaged time: while the tab is visible, at most 30 s after the visitor's last input (pointer, key, wheel, scroll, touch);
  • scroll depth: which of 25 / 50 / 75 / 100 % of the page height the bottom of the viewport reached (a page that fits the screen reaches 100 % at once);
  • clicks per element; rage clicks (3 or more on one element within 1 s, once per burst, not while text is being selected); dead clicks (a click on a link or button after which nothing in the page changed within 1 s: no DOM change, no URL change; not counted for modified clicks, links that open another tab or download);
  • the element key: [data-genclass-name="…"] (a name you put on the element; the way to get readable keys), [data-testid="…"], tag#id (ids that look generated or like data are skipped), a link's target (a[href="/route"] on your site, the other site's origin only), the tag with up to two stable class names, or an nth-of-type path of up to 4 levels under a named ancestor or a landmark. At most 120 characters. Any attribute value that looks like data (an e-mail address, a URL, 4 or more digits, 12 or more hex digits) is not used;
  • navigation within the page load (0.3.0-beta.2, design): how each view was reached (a page load, a reload, the browser's back or forward button, an in-app route change, an in-app back or forward), the previous view's route and the time from its start to this view's, the element key of the view's first interaction (a click, or a focus on a form field right after a key press) and the time to it, and when the page load ends (pagehide), the routes of its views in order (at most 10) and that it ended there. These describe one page load: a full page navigation to another page of the same site starts a new path (the visit, below, follows pages across page loads), so on a site without client-side routing each path is one page long.

Visits

New in 0.3.0-beta.2 (2026-10-25). A visit is a run of page views with no gap over 30 minutes:

  • The visit flag, in localStorage. The runtime keeps one entry, genclass.analytics.<the app's token> (or genclass.analytics.endpoint with your own endpoint and no token), holding { "v": <visit id>, "t": <time of the last page view or event, ms>, "r": 1 }. The visit id is 16 random hex characters (crypto.getRandomValues). A page view more than 30 minutes after t, or a page load that comes from another site or from a link with utm values, starts a new visit with a new id. r only says that this browser had a visit before, which is all "new or returning" means: there is no visitor id. It is written only while analytics run (so never with any opt-out below, GPC or on a local host by default) and runtime.analytics.stop() removes it. With storage blocked, a visit lasts the page load and returning is not sent.
  • Where a visit came from, on the view that opened it: the referring site's host only (google.com, never its path or query; your own site is not a source), and the utm_source, utm_medium and utm_campaign values of the page's URL, cleaned (the rules are under the interactions event above). No other query parameter is read.
  • The device class, browser family and OS family, from the user agent and its hints, as one of the buckets listed under the interactions event above. The user agent string itself is never sent.
  • The country is the collector's own coarse request.cf.country (two letters), as for every batch, counted once per visit.

The collector keeps per visit only a hash of the app's token and the visit id (never the id itself), when it started and was last seen, its entry and exit page, its number of page views and the routes of its first 50 page views in order, and deletes it 90 days after the visit started. That is what the dashboard's entry and exit pages, pages per visit and funnels come from: an app defines up to 5 funnels of 2 to 6 routes on its dashboard (stored with the app), and each step counts the visits that reached the steps before it in that order.

Never recorded: element text, labels, alt, title, placeholders, input values or what was typed, mouse coordinates, the order or timing of clicks, the query string or fragment (beyond the three utm values), the referrer's path or query, the page title, the screen size, the user agent string, a visitor id. No cookies are set, and nothing links two visits of one browser except the yes-or-no returning flag, so the dashboard does not count unique visitors. The order of page views and the time between them are sent only within one page load (the visit's pages in order are stored by the collector, keyed by a hash of the visit id). Synthetic events (isTrusted false) and events inside [data-genclass-ignore] are not counted.

They never reach the model or a decision: they are recorded by their own listeners (a separate file the runtime loads only when analytics are on), outside GenClass's event log, and change nothing on the page in any mode. The maintainers use interactions events only for the app's dashboard and its export, never for model training or evaluation.

After consent: runtime.analytics.start() turns them on later (for example once a visitor accepts analytics in your consent banner), runtime.analytics.stop() turns them off, drops the counts not sent yet and removes the visit flag from localStorage. runtime.analytics reports { enabled, reason? } (option, telemetry-off, no-token, stopped, error).

App tokens and dashboards

New in 0.3.0 (0.2.x ignores a token and sends neither token nor fn). An app can set a token (InitOptions.token, data-token, <meta name="genclass" content="token=..."> or window.GENCLASS_CONFIG.token): gc_ followed by 22 letters or digits, created by init (npx @genclass/runtime@beta init while 0.3.0 is in beta) or at https://genclass.dev/start.

  • The token is public (it is in the page). It is sent as the batch envelope's top-level token and nowhere else; it lets the collector group the batches of one app. A malformed token logs one console warning and is not sent.
  • Whoever holds the app's private dashboard link (https://genclass.dev/dashboard/<secret>, given once when the token is created) can see aggregated diagnostics for that token (decisions, detections by diagnosis, possible detections (the detect events with tier: "possible", shown apart), actions, the app's hostnames, top routes and protected functions, model load, backend and latency, versions; with interaction analytics on, page views, visits, their sources, devices, countries, entry and exit pages and the app's funnels) and its recent detections. Dashboard data is also deleted after 90 days (visits after 90 days on every plan). From runtime 0.3.0-beta.5 it also shows the measured visual checks' counts per check and page and their runtime fixes, as their own kind ("measured check"), never as model detections.
  • A token changes nothing about when data is collected: with telemetry off (any opt-out, or Global Privacy Control, or a local host) nothing is sent and the dashboard gets nothing from that page. On a local host the console says so once when a token is set; otherwise only with debug: true.
  • Only batches that carry a token have fn on decision events: the name the app chose for the protect()ed function the decision was about (at most 80 characters). A name that looks like it carries data rather than naming a function (an e-mail address, a URL, 4 or more digits in a row, 12 or more hex digits) is sent as [redacted], so use fixed names. Without a token fn is never sent.
  • With a token, the console notice ends with one more sentence: With this app's token they also feed its private GenClass dashboard.
  • init creates a token (a "project") only on a new setup and only after the developer approved its changes (or passed --yes): it sends one request, POST https://genclass.dev/api/projects with { "name": <package.json name, at most 80 characters> }. The collector keeps one record per project: the token, that name, the creation time, the times of the first and the last batch, a SHA-256 hash of the dashboard secret (never the secret itself) and, when the app belongs to a genclass.dev account, that account. Project records have no automatic expiry; the aggregated dashboard data does (90 days). To delete a project and its dashboard data, write to the contact in PRIVACY.md with the token.
  • An app can also belong to an optional genclass.dev account (a passkey, Google, an email and password, or GitHub sign-in), which opens its dashboard at https://genclass.dev/account/apps/<id> for the signed-in owner; an existing dashboard link can be added to an account and keeps working until the owner turns that link off on the account page. This changes nothing the runtime sends: pages send the same batches with the same token, and nothing in a batch identifies a person or an account. What an account itself stores is listed in full in PRIVACY.md; among other things, an email as typed or as Google returned it and whether it is verified; a salted one-way hash of the password if one is set, with when it was set and last used and the count of wrong passwords in a row; a one-way hash of the device cookie of each browser where the password was right; passkey public keys; the Google sub and the GitHub id and login if used; session records; and the apps it owns. Deleting the account on its account page deletes the apps created in it with their dashboard data.
  • The CLI's account commands send nothing until they are run. login sends POST https://genclass.dev/api/device/code with { "client_name": "genclass-runtime <version>" } (the approval page shows that name and the account page lists the CLI session under it), then polls POST /api/device/token with the code until the sign-in is approved, denied or expires. whoami and connect read the account's apps (GET /api/me); connect creates an app with POST /api/account/projects and its name (after the developer approved the changes), or adds an existing dashboard with POST /api/account/projects/claim and its link from .genclass.local; logout sends DELETE /api/account/sessions/current. Signed in, init asks whether a new app goes into the account and then creates it with POST /api/account/projects instead of /api/projects. These account requests carry user-agent: genclass-runtime-cli/<version>.

What a coding agent reports (opt-in, from the developer's machine)

The runtime sends nothing of this. When a developer sets GENCLASS_REPORT for @genclass/mcp (off by default), that server on their machine mirrors its log of the loop to the app's dashboard, with the dashboard's private link: the id, kind and page path of each finding it made in the app's development build, when the agent read its evidence, each replay armed and its verdict (reproduced or not), when a regression test was written, each explore report (how many page loads it compared and how many races it found) and each clear; never page text, evidence, snapshots or values. The dashboard keeps them 90 days (at most 500 log rows per app) and shows them in its Closed-loop development view. Details: docs/runtime/AGENT.md section 4.5.

Where it goes

  • Endpoint: https://genclass.dev/api/v1/events (DEFAULT_TELEMETRY_ENDPOINT; before 0.2.2 the same worker's https://genclass-telemetry.mehar-144.workers.dev/v1/events, which still works), a Cloudflare Worker operated by the GenClass maintainers (source: telemetry-worker/ in the repository). With a Content-Security-Policy, allow https://genclass.dev in connect-src or turn telemetry off; after 3 failed requests in a row a page stops sending (a 429 is a back-off, not a failure, since 0.3.0).
  • Storage: a private Cloudflare R2 bucket (genclass-telemetry), gzip JSON Lines, one object per batch, partitioned by UTC date, runtime version and model version. The collector adds only the time it received the batch and the visitor's coarse country (two letters, from Cloudflare). It does not store IP addresses, user agents, cookies or any request header, and it drops unknown fields.
  • Use: model evaluation and training by the GenClass maintainers (never interactions events: those are only for the app's dashboard and export), and, for batches that carry a token, that app's dashboard. For an app in a genclass.dev account (any plan, Free included), also what that account's owner turns on: an export of the app's stored batches, and forwarding of each likely detection (problem type, confidence, trigger, page route, versions, time; never page text) to the services the owner chose. Not sold, not used for advertising.
  • Retention: 90 days. An R2 lifecycle rule (expire-90d, prefix events/) deletes every stored batch 90 days after it was written. For an app on a plan with a year of history (Enterprise, given by GenClass's own grant), a copy of each batch is kept for its export under export/<token>/ for up to 1 year (another lifecycle rule, 366 days), and its dashboard data for 1 year; both are deleted when the grant ends (30 days after the paid period ends, for a paid plan), and at once when the app or its account is deleted. The maintainers may change this later; changes are recorded in the CHANGELOG.
  • Your own endpoint (private mode): telemetry: { endpoint: "https://…" } sends the same batches to a collector you run instead, so nothing reaches GenClass: the worker in telemetry-worker/ deploys to any Cloudflare account with wrangler.selfhost.toml, dashboards and accounts included (telemetry-worker/README.md, "Self-host the collector (private mode)").

Options

GenClass.init({
  telemetry: {
    endpoint: "https://…",          // default DEFAULT_TELEMETRY_ENDPOINT
    sample: 0.25,                    // fraction of page loads that send (default 1)
    decisionSample: 1,               // 0.3.0: share of routine decisions whose record is sent (default 0.25; counts stay exact)
    maxDecisions: 1_000,             // 0.3.0: most routine decisions' records per page load (default 200; Infinity: no cap; detections never capped; 0.2.x sends every record)
    flushMs: 10_000,                 // batch interval
    maxBatch: 100,                   // events per request
    include: { situation: true },    // also send the redacted situation text (default: off)
  },
  analytics: true,                   // 0.3.0-beta.1: interaction analytics for the app's dashboard (default: off; needs the token; visits and navigation paths from 0.3.0-beta.2)
});

telemetry: true is the defaults; telemetry: false turns it off.

Opting out

Any one of these turns telemetry off for the page:

how who
GenClass.init({ telemetry: false }) (or window.GENCLASS_CONFIG = { telemetry: false }, <meta name="genclass" content="telemetry=off">, data-telemetry="off" on the script tag) the app developer
npx @genclass/runtime@beta init --no-telemetry (0.2.x: npx @genclass/runtime init --no-telemetry). It writes the opt-out into what init adds: telemetry: false in its option file, telemetry=off in its <meta name="genclass"> line (Astro), or data-telemetry="off" on a plain HTML script tag; from 0.3.0 it implies --no-token. One exception: in an Astro layout whose </head> shares its line with other markup, init puts GenClass inline in <head>, cannot write the meta line there and prints a note instead; add <meta name="genclass" content="telemetry=off"> yourself the app developer
?genclass=no-telemetry in the page URL (?genclass=off turns GenClass off entirely) anyone, for one page load
localStorage.setItem("genclass.telemetry", "off") (localStorage.setItem("genclass", "off") turns GenClass off entirely, unless the URL has its own ?genclass= value, which takes precedence) a user or tester, persistent per origin
Global Privacy Control: navigator.globalPrivacyControl === true the user's browser setting

Global Privacy Control (GPC). California law (CCPA as amended by the CPRA, and its regulations) requires businesses to treat a GPC signal as a valid request to opt out of the sale or sharing of personal information, and other US state laws (e.g. Colorado, Connecticut) recognise universal opt-out signals. GenClass honours GPC for every visitor, wherever they are: when the browser sends it, nothing is collected.

If you ship GenClass in your app

The data is collected from your users' browsers. Even though it is designed to be anonymous (random per-page session id, no IP or user agent stored, redacted text), situation text can contain fragments of your app's data and the session event names your hostname. Depending on your users and jurisdiction you may need to:

  • mention GenClass diagnostics in your privacy policy / cookie or tracking disclosures (GDPR transparency, Articles 13 and 14; CCPA notice at collection);
  • decide whether it needs consent where you require consent for analytics (GDPR / ePrivacy), and if so start GenClass with telemetry: false until consent is given (GenClass.init() reads the option once; to switch it on later, create the runtime after consent);
  • if you turn on interaction analytics (analytics: true): in the EU, the EEA and the UK, Article 5(3) of the ePrivacy Directive (regulation 6 of PECR in the UK) requires consent to read information from a visitor's device unless it is strictly necessary for the service they asked for, and regulators read it as covering scripts that send interaction data, not only cookies (EDPB Guidelines 2/2023). Analytics are not strictly necessary, so start with them off there and call runtime.analytics.start() after consent; some regulators exempt first-party audience measurement under strict conditions (the CNIL in France), which is your call. Name GenClass as a recipient in your privacy policy either way;
  • turn it off (telemetry: false) in apps that handle regulated data (health, finance, children's services): a custom redact lowers what reaches situation text but does not guarantee that nothing sensitive does;
  • if you add a webhook sink (@genclass/runtime/sinks/webhook, init --sink webhook:<url>, 0.3.0) that posts to an endpoint run by someone else, disclose that recipient too: the webhook sends the redacted evidence by default (the console line, subject and timeline, which can hold short fragments of what users typed) to the URL you configure (evidence: false sends the structured fields only). The free runtime forwards nothing to Sentry, PostHog or Datadog. Sinks are your configuration, not telemetry: the telemetry opt-outs (telemetry: false, ?genclass=no-telemetry, the localStorage switch) and Global Privacy Control do not turn them off; ?genclass=off turns GenClass off altogether.

Privacy policy for this collection: PRIVACY.md. Data processing terms are not published yet (planned); if you cannot disclose a third-party diagnostics recipient, set telemetry: false.

Rendered from packages/runtime/TELEMETRY.md in the GenClass repository.