Tags vs. context
Both ride along on every event captured after they're set, but serve different jobs:
- Tags (
setTag(key, value)) are flatkey: stringpairs, always coerced to a string. They're what the dashboard's search box matches against first for akey:valuefilter, e.g.checkout.step:paymenttyped straight into the search box on Exceptions/Traces/Logs (see Search). - Context (
setContext(key, value)) attaches a nested object under a namespace (setContext('order', { id, total })becomesorder.id/order.totalon the event). It's for descriptive data you want visible on the event detail page, and it still falls back into search if no tag with the same key exists.
Both are optional — nothing here is required to capture an error.
Setting tags and context
ts
Buglapse.setTag('checkout.step', 'payment')
Buglapse.setTag('plan', 'pro')
Buglapse.setContext('order', { id: order.id, total: order.total, currency: 'USD' })Once set, a tag/context value is attached to every event captured afterward — exceptions, logs, traces, spans — until you call setTag/setContext again with the same key. There's no unset; overwrite the key with a new value instead.
setContext flattens nested objects into dot-notated keys under the prefix you give it, so setContext('order', { customer: { id: 42 } }) produces order.customer.id — the same flattening device.*/browser.* contexts use (see Configuration). Arrays and primitives inside the object are kept as-is at their flattened key.
Identifying users
setUser() is a thin wrapper over setContext('user', ...) that also drives who an event gets attributed to on the dashboard's Users page:
ts
Buglapse.setUser({ id: user.id, email: user.email, username: user.username })Every field is optional, and calling it again merges into what's already known rather than replacing it — call it once with just an id at login, then again with email once you have it, and both are kept. Any extra field beyond id/email/username/name is preserved too, just not used for the id → email → IP matching order the dashboard uses to deduplicate a ProjectUser (falls back to the request IP if you never call setUser at all).
Per-call overrides with withScope()
setTag/setContext change what's attached globally, from that point on. withScope() instead overlays tags/context for a single callback, without touching anything set globally:
ts
Buglapse.withScope((scope) => {
scope.setTag('checkout.step', 'payment')
scope.setContext('order', { id: order.id, total: order.total })
try {
chargeCard(order)
} catch (error) {
Buglapse.captureException(error)
}
})Any event captured inside the callback carries the overlay merged on top of the global tags/context; anything captured after withScope() returns doesn't see it at all. scope.setUser() does the same for the user — the scoped counterpart of setUser().
The overlay only survives the synchronous portion of the callback. The browser client has no AsyncLocalStorage to carry it across an await, so:
ts
// ❌ loses the overlay — capture happens after an await
Buglapse.withScope(async (scope) => {
scope.setTag('checkout.step', 'payment')
await chargeCard(order) // scope is gone by the time execution resumes here
Buglapse.captureException(error) // captured with no "checkout.step" tag
})
// ✅ capture before the await, or re-apply with setTag()/setContext() after it
Buglapse.withScope((scope) => {
scope.setTag('checkout.step', 'payment')
try {
chargeCardSync(order)
} catch (error) {
Buglapse.captureException(error) // still inside the synchronous overlay
}
})If the capture has to happen after an await, set the tags/context globally with setTag()/setContext() instead (and clear/overwrite them once the flow is done) rather than relying on withScope().
environment and release
These two are set separately — via the environment/release options on init() or the data-environment/data-release script tag attributes, not setTag/setContext — because they're plain scalars, not objects to flatten, and because the dashboard treats them specially: environment powers the per-project environment filter and can silently drop events from listed environments before they're even stored; release is what lets a minified stack trace be decoded back to original source once you've uploaded matching sourcemaps. See Configuration and Source maps.
Auto-populated context
Some context is filled in for you by opt-in integrations rather than a setContext() call you write yourself:
- DeviceIntegration sends the browser's user agent once at setup. The server parses it into
browser.name/browser.version,os.name/os.versionanddevice.type/device.manufacturer/device.model(the last three only when the user agent identifies a specific device, e.g. an iPhone) on every event — nothing else about the visitor's browser (language, timezone, screen size, ...) is collected or stored. - HttpContextIntegration stamps
request.url/request.referrerfresh on every event, since in an SPA the URL can change between captures without a page reload.
Both are opt-in, toggled from the project's Settings → Loader Script tab — see Integrations.
Viewing context on the dashboard
An event's detail page groups its flattened context back by prefix (order.* under an "Order" heading, and so on), humanizing the dotted keys into labels — the parsed browser.*, os.* and device.* show up there as Browser / OS / Device groups. trace.*, replay.*, and user.* are excluded from that generic list because each already has its own dedicated UI (the trace/replay panel, the Users page). Clicking any row there searches for that exact key:value pair, the same syntax you'd type by hand:
order.currency:"USD"