Skip to content

Overview ​

An integration is a small plugin that hooks into the SDK client once, at registration time. Every built-in (globalErrorIntegration, breadcrumbsIntegration, httpErrorsIntegration, ...) is built on exactly the same interface you get — there's no private API reserved for them, so anything a built-in does, your own integration can do too.

ts
interface Integration {
    name: string
    setup(client: BuglapseClient): void
    teardown?(): void
}
  • name — a unique id. It's the key disable(name) looks up, and the key that decides whether a second registration is ignored or replaces an existing one (see Registering).
  • setup(client) — called once, when the integration is registered on a live client. This is where you subscribe to events, patch globals, stamp context, or add a preprocessor.
  • teardown() — optional. Called by disable(name); undo whatever setup() did (remove listeners, restore patched functions).

A minimal integration ​

Stamp the current page's color scheme onto every event:

ts
import type { Integration } from '@buglapse/browser'

export function colorSchemeIntegration(): Integration {
    return {
        name: 'color-scheme',
        setup(client) {
            if (typeof window === 'undefined') return

            const query = window.matchMedia('(prefers-color-scheme: dark)')
            const apply = () => client.setTag('color_scheme', query.matches ? 'dark' : 'light')

            apply()
            query.addEventListener('change', apply)

            this.teardown = () => query.removeEventListener('change', apply)
        },
    }
}

Two conventions the built-ins follow, worth copying:

  • Export a factory, not a plain object. colorSchemeIntegration() returns a fresh object each call, so per-instance state (listeners, counters, the last-seen value) never leaks between clients, and every integration — built-in or yours — is called the same way.
  • Guard browser globals. Check typeof window/document/navigator before touching them, so the integration is a no-op instead of a crash when your bundle is evaluated during SSR or in a test runner.

Registering an integration ​

In init() ​

ts
import { init } from '@buglapse/browser'
import { colorSchemeIntegration } from './buglapse/color-scheme'

init({
    dsn: '{dsn}',
    integrations: [colorSchemeIntegration()],
})

The list is merged with the SDK's defaults (currently only globalErrorIntegration()). An entry whose name matches a default replaces that default — so an integration named 'global-error' swaps out the built-in error capture without a separate disable() call.

With use() ​

ts
import { use } from '@buglapse/browser'

use(colorSchemeIntegration())

use() works both before and after init(): called earlier, the registration is queued and applied right after the defaults; called later, setup() runs immediately. On the script-tag install it's Buglapse.use(...) on the global.

use() also accepts a factory plus its config, the same way the built-ins are registered:

ts
use(slowRequestIntegration, { thresholdMs: 2000 })
// same as: use(slowRequestIntegration({ thresholdMs: 2000 }))

Unlike init({ integrations }), use() never replaces — if an integration with the same name is already registered, the new one is silently ignored and its setup() never runs. To swap one out after init(), disable(name) first, then use() the replacement.

Disabling ​

ts
import { disable } from '@buglapse/browser'

disable('color-scheme') // calls teardown(), then unregisters it

Called before init(), disable(name) only affects the built-in defaults (it stops them from being registered at all).

What setup() can do ​

setup() receives the same BuglapseClient the public API talks to. The useful surface:

MethodUse it to
client.setTag(key, value)Add a searchable tag to every future event
client.setContext(key, object)Attach structured context to every future event
client.setUser(user)Identify the current user
client.addBreadcrumb({ type, level, message, data })Record something that leads up to the next exception
client.captureException(error)Report an error
client.log.info(message, attributes) (and log/warn/error/debug)Send a log event
client.span() / client.asyncSpan() / client.startInactiveSpan()Instrument an operation as a span
client.addPreprocessor(fn)Rewrite or drop events before they're sent
client.flush()Push out any batched events right away
client.transport.endpointThe envelope URL — to avoid instrumenting the SDK's own requests
client.debugWhether debug: true was passed to init()

Context, tags and breadcrumbs are covered in Context and Breadcrumbs; spans in Tracing.

Filtering and rewriting events ​

A preprocessor sees every event before it's sent. Return the (possibly modified) event to keep it, or null to drop it. For exception events the second argument is the original Error object, so you can branch on instanceof:

ts
import type { Integration } from '@buglapse/browser'

class AbortedByUserError extends Error {}

export function ignoreAbortsIntegration(): Integration {
    let enabled = true

    return {
        name: 'ignore-aborts',
        setup(client) {
            client.addPreprocessor((event, rawError) => {
                if (!enabled || event.type !== 'exception') return event
                if (rawError instanceof AbortedByUserError) return null
                if (rawError?.name === 'AbortError') return null

                return event
            })
        },
        teardown() {
            enabled = false
        },
    }
}

Preprocessors can't be unregistered — addPreprocessor() has no counterpart. If your integration should be disable-able, gate the preprocessor behind a flag that teardown() flips, as above. Preprocessors run in registration order, and once one returns null the rest are skipped.

If you only need a one-off filter rather than a reusable, named plugin, call addPreprocessor(fn) directly on the SDK instead of wrapping it in an integration.

Patching globals ​

Integrations that observe the page — network requests, history navigation, DOM events — usually wrap a browser API. Keep a reference to the original, and restore it in teardown(). This one reports any fetch slower than a threshold as a warning log:

ts
import type { BuglapseClient, Integration } from '@buglapse/browser'

export interface SlowRequestConfig {
    thresholdMs?: number
}

// The SDK sends its own batches with fetch — never report (or re-instrument) those, or a slow
// /api/envelope would feed itself an endless stream of "slow request" logs.
function isOwnRequest(client: BuglapseClient, url: string): boolean {
    const endpoint = client.transport.endpoint
    if (!endpoint) return false

    const target = new URL(url, window.location.href)
    const own = new URL(endpoint)

    return target.origin === own.origin && target.pathname === own.pathname
}

export function slowRequestIntegration(config: SlowRequestConfig = {}): Integration {
    const thresholdMs = config.thresholdMs ?? 3000

    return {
        name: 'slow-request',
        setup(client) {
            if (typeof window === 'undefined' || typeof window.fetch !== 'function') return

            const originalFetch = window.fetch.bind(window)

            window.fetch = async (...args: Parameters<typeof fetch>) => {
                const [input, init] = args
                const url = typeof input === 'string' ? input : input instanceof URL ? input.toString() : input.url

                if (isOwnRequest(client, url)) return originalFetch(...args)

                const startedAt = performance.now()
                const response = await originalFetch(...args)
                const duration = Math.round(performance.now() - startedAt)

                if (duration >= thresholdMs) {
                    client.log.warn(`Slow request: ${init?.method ?? 'GET'} ${url} took ${duration}ms`, {
                        url,
                        method: init?.method ?? 'GET',
                        status: response.status,
                        duration,
                    })
                }

                return response
            }

            this.teardown = () => {
                window.fetch = originalFetch
            }
        },
    }
}

Things to keep in mind when patching:

  • Never swallow the original behavior. Always call through to the original and return (or re-throw) its result unchanged — your integration must be invisible to the app.
  • Skip the SDK's own traffic. Compare against client.transport.endpoint, as above. Without it, instrumenting fetch/XMLHttpRequest can report the SDK's own requests — and a failing envelope endpoint turns into a feedback loop.
  • Setting this.teardown inside setup() is the simplest way to capture the values (originalFetch, listener references) that only exist once setup has run. It works because the client calls setup() on the integration object itself.
  • Multiple integrations can wrap the same API. Built-ins like breadcrumbsIntegration and httpErrorsIntegration each wrap fetch independently; yours just becomes another layer. Disable integrations in the reverse order you registered them if they all patch the same function, or a later teardown() can restore a stale wrapper.

Sending other event types ​

client.captureException(), client.log.* and the span helpers cover almost everything. For a built-in event type with no dedicated method, there's a lower-level escape hatch:

ts
client.capture('hit', { sessionId, name: 'checkout-opened' })

capture(type, payload) stamps the current tags/contexts/trace onto the payload and sends it through the same preprocessor pipeline and transport as everything else. type must be one of the types /api/envelope accepts (exception, log, trace, span, replay, hit) and payload must match that type's shape — prefer the dedicated methods whenever one exists.

Debugging ​

Pass debug: true to init() and the client logs every integration registration (Integration "color-scheme" registered), every disable(), every captured event and every event a preprocessor drops. Inside your own integration, check client.debug before adding extra logging of your own.

Script-tag install ​

On the CDN script tag, the Settings → Loader Script toggles only cover the built-ins. Custom integrations are registered from your own code through the global:

html
<script src="https://app.buglapse.com/cdn/{project-token}/buglapse.min.js"></script>
<script>
    Buglapse.use({
        name: 'color-scheme',
        setup(client) {
            client.setTag('color_scheme', matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light')
        },
    })
</script>

The script tag here is loaded without async, so window.Buglapse is guaranteed to exist when the second script runs. If you keep async, register from a load listener on the SDK's <script> element instead.