devbench (browser)
The Dev Bench browser sensor. Deterministic, always-on, and strictly budgeted: errors, failed and slow requests, slow pages and interactions, counted per problem — traffic grows with distinct problems, not with page views.
Install
Rails apps: nothing to do here — gem 'devbench' renders this tag for
you (bin/rails generate devbench adds <%= devbench_script_tag %> to your
layout). The rest of this section is for any other web app.
One script tag. Put it before your app's scripts:
<script src="https://unpkg.com/devbench@0.6.0/dist/devbench.min.js"
data-dsn="https://<public key>@adt-ingest.onrender.com"
data-release="<build sha>"></script>That is the whole install: the bundle reads data-dsn from its own tag and
calls Devbench.init itself. data-release is optional. Without data-dsn
the tag does nothing but define Devbench. (jsDelivr works the same:
https://cdn.jsdelivr.net/npm/devbench@0.6.0/dist/devbench.min.js. Serving
the file yourself works too — copy dist/devbench.min.js.)
Or npm, and one call:
npm install devbenchimport * as Devbench from 'devbench';
Devbench.init({
dsn: 'https://<public key>@adt-ingest.onrender.com',
release: BUILD_SHA, // optional
user: { email: me.email, account: me.accountId }, // optional, if known now
});The DSN here is the public form of your environment's DSN: Dev Bench
gives you https://<public>:<secret>@host; a page uses only
https://<public>@host (drop :<secret>). The public key is write-only and
safe to ship. The sensor refuses a DSN that still carries the secret. An invalid
DSN never throws — it logs one console.warn and the sensor stays inert.
enabled: false turns everything off silently (e.g. in development).
Who is using the page. On login, and whenever the user changes:
Devbench.setUser({ email: currentUser.email, account: currentAccount.id });
Devbench.setUser(null); // on logoutinit returns the sensor (an inert one when disabled), and every method is
safe to call either way; Devbench.getSensor() returns it later. Calling
init again keeps the first sensor and installs nothing twice;
Devbench.close() removes every hook. Every other init option is a sensor
option (slowRequestMs, flushIntervalMs, autoFlush, redaction, …;
see SensorOptions in src/sensor.ts).
AngularJS needs nothing extra. When window.angular exists at init, or
exists by DOMContentLoaded (so the script tag may come before or after
angular.js), Dev Bench hooks $http (trace headers, readback, intents) and
$exceptionHandler through AngularJS's core module — no 'adt' dependency to
add. Only if AngularJS loads later than that (lazy-loaded, or a manual
angular.bootstrap after DOMContentLoaded from a script that arrived
late), call this before bootstrapping:
Devbench.registerAngular(angular);It is idempotent, and apps that already list the 'adt' module keep working
and still get one interceptor, not two.
Flushing is automatic: every 15s when there is something to report, nothing when there is not, and once more when the page is hidden or closed.
Size. 11.5KB gzipped (dist/devbench.min.js), against an 18KB budget the
build enforces. No runtime dependencies.
Migrating from ADT
Nothing is required. The bundle still defines window.ADT — the very same
object as Devbench — so new ADT.Sensor({ endpoint, tenant, ingestKey }),
ADT.installErrorCapture(sensor, window) and ADT.registerAngularModule(...)
keep working, and dist/adt.iife.js is still built. To move to the one-line
install, replace that block with the script tag above (a DSN is
https://<ingestKey>@<host of your endpoint>); remove the old init code in
the same change, or init keeps the first sensor and warns.
Content-Security-Policy
If your app sets a CSP, it needs two entries, or the sensor is blocked
(silently, under report-only, until the day enforcement is switched on):
| Directive | Add | Why |
|---|---|---|
script-src |
https://unpkg.com |
the script tag's source (skip if you serve devbench.min.js from your own assets) |
connect-src |
your DSN's host, e.g. https://adt-ingest.onrender.com |
where reports are sent |
Evidence uploads go to presigned object-storage URLs on a separate host; add
that host to connect-src as well when your Dev Bench account names one
(today: https://*.storage.supabase.co).
What is captured automatically
Errors. Uncaught errors, unhandled rejections, console.error,
and every fetch and XMLHttpRequest that fails: a response of 400 or more,
or no response at all (an abort the app asked for is not a failure). A
success carrying x-adt-handled from a server SDK is a silent failure. With
AngularJS, $http adds intents, trace headers and write-then-readback, and
one failed request (or one AngularJS exception) is still one count. The sensor
never records its own ingest
or evidence requests.
What is detected as slow. Slowness a user feels, as degradation problems
that are fingerprinted and counted exactly like errors:
| Type | When | Message (one fingerprint per) | Option, default |
|---|---|---|---|
slow_request |
a fetch/XHR takes this long (call to response) | GET /api/deals/<id> was slow |
slowRequestMs, 3000 |
slow_page_load |
LCP at or above this, judged once per load | page /#!/deals/<id> loaded slowly |
slowPageLoadMs, 4000 |
slow_interaction |
an interaction's input-to-paint time (Event Timing, as INP) | interaction on /#!/deals/<id> was slow |
slowInteractionMs, 500 |
Page and interaction thresholds are web.dev's "poor" bands; 0 disables
each. Messages carry the route template and never a duration, so every
occurrence of the same slow thing is one fingerprint. A request that fails
is recorded as that failure only, not also as slow, and a handled-header
silent failure is not also slow: one bad request is one problem. Browsers
without PerformanceObserver or the entry types simply report no page or
interaction slowness.
Why there is no tracing. Sentry's BrowserTracing sends a transaction with spans for every page load and navigation, so its cost grows with traffic; ADT's design is that traffic grows only with distinct problems. The question tracing is bought to answer — what is still too slow, for whom, how often — is answered here by slow requests, slow page loads and slow interactions counted per route, with the affected users on each count and the evidence bundle (intents, requests) fetched once per new fingerprint. Like Session Replay (DECISIONS #157), span storage is a Sentry feature deliberately not matched: no waterfalls, no percentiles, no per-view timings.
Identity. setUser takes email (trimmed, lowercased) and account (your
own customer id); each optional, each truncated to 255 characters. It travels
on counts in its own users field — never in a fingerprint, a message or
evidence — and emails inside messages are still redacted.
The two rules this package exists under
It must never harm the host app. Every buffer is fixed-size and preallocated, every entry point is wrapped so an internal failure disables the sensor rather than throwing into the application, and the bundle budget fails the build rather than appearing in a report. Currently 11.5KB gzipped against an 18KB budget.
It must never leak user data. Redaction happens here, before anything leaves the browser, because it cannot be retrofitted. Field values are captured only when explicitly allow-listed; otherwise they are reduced to a salted, per-session comparison token — enough to tell whether two values differ, never enough to learn what either is.
Generated from sdk/client/README.md, the README the install test follows.