Browser
@zharp/browser sends a web page's logs and errors, traces its page loads, clicks and requests into your back end, and can record sessions to replay.
This SDK is new, and its API may still change before 1.0. Requires a current browser; it does nothing during server-side rendering.
Install
npm install @zharp/browser
Script tag
Without a bundler, add it in the <head> of every page, before your own scripts. It gives the page a global Zharp with the same API. zharp.min.js is about 9 KB gzipped; the replay recorder is a separate file, loaded from next to it only when a session records.
<script src="https://cdn.jsdelivr.net/npm/@zharp/browser/dist/zharp.min.js"></script>
<script>
Zharp.init({
key: "zpk_…",
service: "checkout-web",
environment: "production",
});
</script>Initialize
Call Zharp.init as early as possible, before your app renders, so the first errors and the page load are caught. Copy the key from the app's setup page in App Telemetry.
| Framework | Where |
|---|---|
| React | src/main.tsx |
| Next.js | instrumentation-client.ts at the project root. For the server half, see Node.js. |
| Vue | src/main.ts |
| Angular | src/main.ts |
import { Zharp } from "@zharp/browser";
Zharp.init({
key: "zpk_…",
service: "checkout-web",
environment: "production",
release: "1.0.0", // optional: see which release broke
tracingOrigins: ["api.example.com"], // your API, so traces continue into it
replay: {
sampleRate: 0.1, // record 10% of sessions from start to end
onErrorSampleRate: 1.0, // and every session that hits an error
},
});init runs once; a second call is ignored with a console warning. Outside a browser it does nothing, so the same import is safe in code that also renders on the server.
Logs
Zharp.log.info("Checkout started", { cartSize: 3 }); // trace, debug, info, warn, error, fatal
Zharp.log.error("Payment failed", { error }); // an Error among the attributes is sent as the exceptionEach log carries the session, the user and the trace it was written in. The console isn't copied by default — on most pages it is mostly other people's libraries:
Zharp.init({ key: "zpk_…", service: "checkout-web", captureConsole: ["warn", "error"] }); // or true for every levelLogs only
If you only want logs, turn tracing off. Logs, errors, and the user and session they belong to are still sent; no spans are recorded and no trace headers are added to outgoing requests.
import { Zharp } from "@zharp/browser";
Zharp.init({
key: "zpk_…",
service: "checkout-web",
tracing: false,
});The logs show on the Logs page under the service name, and on the app's Logs tab with the user and session of each line. The key comes from an app — see Get a key. To add tracing later, remove the option; nothing else changes.
Errors
Uncaught errors and unhandled promise rejections are sent as unhandled. Report one you caught with captureError; it is sent as handled. Both are grouped into issues.
try {
await pay(order);
} catch (error) {
Zharp.captureError(error, { orderId: order.id }); // reported as handled
}Readable stack traces
Minified code reports stacks like main.4f2c.js:1:20871. Add Zharp's plugin to your build and errors are shown and grouped by your original functions, files and lines, with the code around each frame. On a production build the plugin turns on hidden source maps, gives each JS file a debug ID, uploads the files and maps, and deletes the maps from the output so your source isn't deployed.
// npm install --save-dev @zharp/vite-plugin
// vite.config.js
import { defineConfig } from "vite";
import { zharp } from "@zharp/vite-plugin";
export default defineConfig({
plugins: [zharp({ app: 42 })],
});42 is the app's ID, the number in its dashboard URL (or ZHARP_APP). Set ZHARP_AUTH_TOKEN in CI to a workspace key with the artifacts:write scope — see The upload key. Without it, local builds still work: the plugin warns, uploads nothing and still removes the maps.
| Plugin option | Default | Notes |
|---|---|---|
app | ZHARP_APP | The application ID. |
authToken | ZHARP_AUTH_TOKEN | Keep it in CI secrets. |
release | ZHARP_RELEASE, else the commit SHA from your CI or host | Optional: matching is by debug ID. |
deleteSourcemapsAfterUpload | true | false keeps the .map files. |
include / exclude | every JS file | Globs or regular expressions. |
failOnError | false | Fail the build when the upload fails, instead of warning. |
dryRun, force, disabled, silent | false | Report only; run on non-production builds; do nothing; no output. |
The SDK sends the debug IDs of the files in each error's stack, so maps are found whatever URL or CDN the code was served from. More: Readable stack traces, Zharp CLI.
Tracing
| What | How it is recorded |
|---|---|
| Page loads | A pageload /path span from navigation start until the page and its requests have loaded, with time to first byte, DOMContentLoaded and load. |
| Navigations | A navigation /path span per route change in a single-page app — pushState, back and forward, and #/ routes. |
| Clicks | A click “Pay now” span for a click that leads to requests. Clicks that do nothing aren't recorded. |
| Requests | A span per fetch and XMLHttpRequest with method, URL and status. 4xx, 5xx and network failures are errors. |
A click is labelled from the element's data-zharp-name, then its aria-label, text or title. Name the ones that matter:
<button data-zharp-name="Pay now">…</button>
Page timing is limited to those load milestones; Core Web Vitals (LCP, INP, CLS) aren't collected.
Traces into your back end
Requests to the page's own origin and to anything in tracingOrigins carry a traceparent header, so your API continues the page's trace, and a baggage header with the session and user. Nothing else gets them; a third-party request still gets a span.
- A host, any scheme and port:
"api.example.com"; one port:"localhost:3001". - A URL prefix:
"https://example.com/api". - A
RegExp, tested against the full URL.
A cross-origin API must list traceparent and baggage in Access-Control-Allow-Headers, or the browser blocks the request. The server SDKs don't add that for you.
Spans of your own
await Zharp.startSpan("render invoice", async () => {
await renderInvoice(order); // requests and logs in here belong to the span
});
Zharp.setTag("plan", "pro"); // on every span and log from now on; null removes itUser context
After sign-in, and on every page load while someone is signed in:
Zharp.setUser({ id: user.id, email: user.email }); // name is optional too
Zharp.clearUser(); // on sign-out: also starts a new sessionA session is one person's use of one tab. It is kept in sessionStorage, so it survives reloads and each tab has its own, and ends after 30 minutes without clicks, keys or scrolling, or on clearUser(). Where storage is unavailable (some private modes, sandboxed iframes) it lasts as long as the page. Zharp.sessionId is the current one. Every span and log carries the session, the user, the browser and the OS.
Session replay
Off until you set replay. replay: { onErrorSampleRate: 1.0 } uploads a recording only when the session hits an error; sampleRate records a share of all sessions. Sampling, masking and privacy options are on Session Replay.
Options
| Option | Default | Notes |
|---|---|---|
key | — | Required. The app key, zpk_…. |
service | web | Sent as service.name. Use the app's service name. |
environment | none | deployment.environment.name |
release | none | service.version — a version or commit SHA. |
endpoint | https://api.zharp.io/api/v1/otlp | Where to send. |
tracing | true | false sends logs and errors only — see Logs only. |
tracingOrigins | [] | Hosts that get trace headers. See Traces into your back end. |
sampleRate | 1 | Share of traces kept, 0 to 1. Errors, logs and replays aren't sampled by it. |
replay | off | See Session Replay. |
captureConsole | false | true or a list of levels. |
interactions | true | Spans for clicks that lead to requests. |
ignoreUrls | [] | URLs never traced: hosts, prefixes or RegExps. |
sessionTimeoutMinutes | 30 | Idle time before a new session starts. |
Delivery
Batches of up to 100 records go every 5 seconds, gzipped where the browser supports it. Rate limits, server errors and network failures are retried with backoff; at most 1,000 records of each kind wait, and the oldest go first past that. When the page is hidden or closed, what is left is sent with keepalive so it outlives the page. Zharp.flush() sends everything waiting now; Zharp.close() flushes, stops recording and undoes every patch. The SDK never throws into your code.
Troubleshooting
Nothing arrives
On the app's setup page (App Telemetry → the app → settings → Setup instructions), the verify step shows the first event as soon as it lands, and Send a test event checks the key on its own. If the test event arrives but your app's don't, the SDK isn't starting or isn't reaching Zharp:
- Is
initcalled, and before the code you expect to see? Without a key, the SDK starts nothing and says so once. - Does the service name match? With an app key everything goes to that app whatever it says; with a workspace key,
servicedecides the app — see Keys. - Data is sent in batches every few seconds. A process that exits straight away should call
flushfirst. - Logs and errors arrive but no traces? Check that tracing isn't turned off — see Logs only.
- Is the request in the network tab? Look for
/v1/tracesand/v1/logstoapi.zharp.io.
401 or 403
Ingest answers 401 when the key is missing, mistyped, revoked or rolled, and 403 when the request comes from an origin or IP address the key doesn't allow. Neither is retried: the SDK warns once and drops what was refused, and it never throws into your code. Copy the key again from the app's settings.
Allowed origins
A browser sends an Origin header, and when the app has Allowed origins, only those may send with its key. An origin is the scheme, host and port, exactly — https://shop.example.com, not shop.example.com or a URL with a path. Add http://localhost:3000 for local development, or leave the list empty.
Ad blockers
Some browser extensions block requests to monitoring services. If it works in a private window with extensions off, that's the cause — your users with the same extension won't be reported either.
The server half: continue these traces in your API.
play_circleSampling and privacy options for recordings.
rocket_launchKeys, environments, releases and linking a front end to its API.
descriptionSearch, filter and live-tail these logs with the rest of your logs.