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/runtime0.3.0-beta.5 (2026-11-02), the version on GitHubmain, inpackage.jsonand in theruntimefield of every batch. The copy in an npm package describes that package's version. As of 2026-11-02,npm i @genclass/runtime@betainstalls 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) andnpm i @genclass/runtimeinstalls 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 since0.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 carryautoState: true). Add aredactfunction 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 withanalytics: true, and only with the app's token (or your owntelemetry.endpoint). Then the page also sendsinteractionsevents: per page view, the route, engaged time, scroll depth milestones and clicks, rage clicks and dead clicks per element, the element named from markup (adata-genclass-name, adata-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 inlocalStorage). 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. Thesessionevent says whether the checks run on the page (visual:off,localoron) and whether the app turned their runtime fixes on (fix). Once the checks have run on the page, everysummaryevent carries how many findings each check made (counts.checks), the same per page path (counts.checksByRoute, the path normalised as onsession, 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 sendvisual: "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:
runtimehost(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)modeeffectiveModeaggressivenesssampledmodel(local/custom/off)modelStatemodelVersiontriageshadowholdWritesdevice(webgpuavailable,cores,memoryGB(the browser's coarse deviceMemory),crossOriginIsolated,effectiveType,saveData)samplesituation(whether situation text is included)visual(0.3.0-beta.5: the measured visual checksoff;local, the default on a public host, where they load only when the devtools overlay mounts orrt.checkNow()runs; oron, where they start afterload: a development host,visual: trueor a?genclass-visual=onlink) andfix(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:
stateversionmodelvariantdevice(webgpu/wasm)threadsworkerworkerError(why the model worker was not used, URLs replaced by<url>)gpu(WebGPU probe summary)ortloadMswarmupMsfromCachebytesphasereasonerror(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:
idtriggerroute(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)modellatencyMsheld(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)budgetcompactquestions(question ids)answers(per question: calibratedprobabilitiesper label,choice,confidence; orp/score)diagnosisdiagnosisConfidenceaction(chosen)confidencetiercandidateranexecutedactedreason(why the passive action ran)massgateKindthreshold/gain/marginthresholdSourcegates(report/guard/heal thresholds, aggressiveness, level and sources)effectiveModeshadowfn(only on batches with a token: the name the app gave theprotect()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:
decisiontriggerdiagnosisptier(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 whosepis belowgates.reportand at least the eager profile's report value (gates().possible, 0.70 with model 0.2.0), which the record does not carry, sotiersays 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 itsp, 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:
reasoncounts(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 onsession),counts.fixes(applied,undone) andruntime.visual(passes,msof check time,partialpasses,tierfullorlight) telemetry(queued, dropped, send failures, events sent; and, 0.3.0,decisionRecords(decisions whose records were sent),decisionsSampledOutanddecisionsCapped(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 onsession)start(the view's first event: a page view)engagedMsscroll(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) andfromMs - once per view,
first(the element key of the first click, or of the first focus on a form field after a key press) andfirstMs - on the page load's last event (pagehide),
exit: trueandpath(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
detectevent is sent with thedecisionrecord it points to, and so is every decision whose top diagnosis is notexpected(a near-miss below the report gate included), every decision that ran an action or would have in shadow mode, and every veto. decisionSample(default0.25): of the routine decisions (top diagnosisexpected, 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(default200): 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 onpagehide/ when the tab becomes hidden vianavigator.sendBeacon(falling back tofetchwithkeepalive). - The
fetchandsendBeaconcaptured when@genclass/runtimeloaded, 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 aRetry-Afterthe 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 theRetry-After(in seconds) when it is longer, never more than 5 minutes (page exit still sends, bysendBeacon). After 8 back-offs in a row (about 27 minutes) a refusal counts as a failure. ARetry-Afteron 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 annth-of-typepath 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>(orgenclass.analytics.endpointwith 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 aftert, or a page load that comes from another site or from a link with utm values, starts a new visit with a new id.ronly 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) andruntime.analytics.stop()removes it. With storage blocked, a visit lasts the page load andreturningis 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 theutm_source,utm_mediumandutm_campaignvalues of the page's URL, cleaned (the rules are under theinteractionsevent 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
interactionsevent 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
tokenand 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 (thedetectevents withtier: "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 runtime0.3.0-beta.5it 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
fnondecisionevents: the name the app chose for theprotect()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 tokenfnis 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. initcreates 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/projectswith{ "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 Googlesuband 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.
loginsendsPOST https://genclass.dev/api/device/codewith{ "client_name": "genclass-runtime <version>" }(the approval page shows that name and the account page lists the CLI session under it), then pollsPOST /api/device/tokenwith the code until the sign-in is approved, denied or expires.whoamiandconnectread the account's apps (GET /api/me);connectcreates an app withPOST /api/account/projectsand its name (after the developer approved the changes), or adds an existing dashboard withPOST /api/account/projects/claimand its link from.genclass.local;logoutsendsDELETE /api/account/sessions/current. Signed in,initasks whether a new app goes into the account and then creates it withPOST /api/account/projectsinstead of/api/projects. These account requests carryuser-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'shttps://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, allowhttps://genclass.devinconnect-srcor 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
interactionsevents: 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, prefixevents/) 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 underexport/<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 intelemetry-worker/deploys to any Cloudflare account withwrangler.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: falseuntil 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 callruntime.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 customredactlowers 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: falsesends 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, thelocalStorageswitch) and Global Privacy Control do not turn them off;?genclass=offturns 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.