Skip to main content

Group browser and server errors into issues, triage them (resolve, ignore, reopen), see the flag variants each issue happens under, and read every event's stack trace, breadcrumbs and context

Last updated October 18, 2026

Error Tracking and Issues

Flaggr groups the errors your browsers, mobile apps and server-side services report into issues: every event with the same fingerprint is one issue. The console's Issues page (/console/<project>/issues) lists the project's issues from both sources together, and each issue has its own page. There you can see its events over time, the releases, environments, browsers, services, pages and routes it shows up in, the flag variants its events carried, and every event in full.

Where errors come from

SourceHow errors arriveStored in
BrowserThe analytics script with error tracking on (data-track-errors="true", or the detailed / full collection level) catches window.onerror and unhandled promise rejections. The Flaggr client SDK's own error listener sends its errors through POST /api/analytics/sdk-telemetry.analytics_events, under each of the project's Sites
Browser (SDK)flaggrErrors() in @flaggr/sdk: uncaught errors, unhandled rejections and captureException, with the release, environment, user, tags, breadcrumbs and evaluated flags.analytics_events
Mobile@flaggr/react-native in React Native and Expo apps: JavaScript crashes, errors and unhandled rejections, with the device, app version, release and flag variants, and a session per launch for crash-free rates.analytics_events (type mobile_error)
ServerflaggrErrors() on Node.js, the Go SDK's errorreport package, or any service that posts errors (and request spans) to POST /api/ingest/server-events with the project's API token.server_telemetry_events

Browser rows the ingest marked untrusted (sent from an origin that isn't the Site's domain) are left out, as they are on every analytics read. A member can count them with ?includeUntrusted=1.

The Flaggr CLI can also report its own crashes to the Flaggr team, if you opt in with flaggr crash-reports on. Those reports go to Flaggr's own project, never to yours. See flaggr crash-reports.

Versions

IntegrationAvailable
The analytics script's data-track-errorsNow
POST /api/ingest/server-eventsNow
The Go SDK's errorreport packageflaggr-go v0.2.0 and later (go get github.com/flaggr-dev/flaggr-go@latest)
flaggrErrors() in @flaggr/sdk@flaggr/sdk 0.6.0 and later. On an older SDK, use the script tag in browsers and the REST ingest on servers
@flaggr/react-native0.1.0 and later
flaggr sourcemapsFlaggr CLI v0.6.0 and later. The REST upload below works with any version

Set up

Pick the integration for each part of your stack. They all report to the same project, so an issue page can show a browser error next to the server error behind it.

PlatformIntegrationCredential
Browser, no build stepThe analytics script with data-track-errors="true"The Site's id (public)
Browser appflaggrErrors() in @flaggr/sdkThe project's SDK key (public)
Node.js, Bun, DenoflaggrErrors() in @flaggr/sdkA project API token (secret)
GoThe Go SDK's errorreport packageA project API token (secret)
React Native, Expo@flaggr/react-nativeThe app's Site id (public)
Anything elsePOST /api/ingest/server-eventsA project API token (secret)

Server-side routes refuse SDK keys and evaluation-only keys: those ship inside client bundles, so they can't vouch for a server. Create a project API token that reads the whole project (Project settings → API tokens). A token limited to a service, an environment or a flag is refused too. Keep the token in an environment variable such as FLAGGR_ERRORS_TOKEN, never in code.

Browser: the analytics script

If the page already runs browser analytics, add one attribute:

<script src="https://cdn.flaggr.dev/a.js" data-site-id="site_7c1e4a9b2f6d8e0a3b5c7d9f" data-track-errors="true" defer></script>

The script catches uncaught errors and unhandled promise rejections, with the flag values the page published. A page sends at most 50 errors, and at most 3 of the same error per page load. The _flaggr_analytics flag can also turn errors on for a share of traffic ("detailed"), without touching the tag.

Browser: @flaggr/sdk

flaggrErrors() adds breadcrumbs, releases, users, tags, captureException, crash-free sessions and trace propagation:

import { createFlaggr, flaggrErrors, captureException, setUser } from '@flaggr/sdk'
 
export const flaggr = createFlaggr({
  serviceId: 'web-app',
  apiKey: process.env.NEXT_PUBLIC_FLAGGR_API_KEY, // the project's SDK key
  environment: 'production',
  plugins: [
    flaggrErrors({
      release: process.env.NEXT_PUBLIC_RELEASE, // e.g. the commit SHA your build stamps
      ignoreErrors: ['ResizeObserver loop', /^Script error\.?$/],
      beforeSend: (event) => (event.url?.includes('/admin') ? null : event),
    }),
  ],
})
 
setUser({ id: session.userId })
 
try {
  await submitOrder()
} catch (error) {
  captureException(error, { tags: { step: 'pay' }, extra: { cartSize: cart.length } })
}

In React, report render errors from an error boundary with captureException(error, { extra: { componentStack } }). In Next.js, create the client in a Client Component that the root layout renders, so it runs on every page. The TypeScript SDK lists every option.

Node.js

The same plugin on a server sends to POST /api/ingest/server-events, so it needs a project API token:

import { createFlaggr, flaggrErrors } from '@flaggr/sdk'
 
export const flaggr = createFlaggr({
  serviceId: 'checkout-api',
  apiKey: process.env.FLAGGR_API_KEY,
  plugins: [
    flaggrErrors({
      release: process.env.GIT_SHA,
      serverToken: process.env.FLAGGR_ERRORS_TOKEN, // a project API token
      serverServiceId: process.env.FLAGGR_SERVICE_ID, // optional: the service's id, to file errors under it
    }),
  ],
})

After an uncaught exception, the plugin sends it, then exits with code 1 within shutdownTimeoutMs (2 seconds), as Node.js would have. Set exitOnUncaught: false if your own handler exits. Before a planned shutdown, await the plugin's flush().

Go

import "github.com/flaggr-dev/flaggr-go/errorreport"
 
func main() {
	reporter := errorreport.New(errorreport.Options{
		Token:            os.Getenv("FLAGGR_ERRORS_TOKEN"),
		Release:          "checkout-api@2.8.1",
		Environment:      "production",
		TracesSampleRate: 0.1, // record 10% of requests for the Performance page
	})
	defer reporter.Close()   // flush queued events
	defer reporter.Recover() // report a panic in main, then re-panic
 
	mux := http.NewServeMux()
	mux.HandleFunc("POST /orders", placeOrder)
	http.ListenAndServe(":8080", reporter.Middleware(mux)) // handler panics, request spans, traceparent
}

Capture errors you handle with the request's context, so they join its trace, and with the flag variants the request evaluated:

if err := charge(ctx, order); err != nil {
	reporter.CaptureError(err,
		errorreport.WithContext(ctx),
		errorreport.WithRequest(r),
		errorreport.WithFlags(map[string]string{"payments-v2": variant}),
		errorreport.WithTags(map[string]string{"provider": "stripe"}))
}

SetUser, SetTag, SetFlag and AddBreadcrumb add context to every later event. WithFingerprint groups an event yourself, and WithLevel(errorreport.LevelWarning) lowers its level. StartSpan records a child span (a database query, say) and Transport wraps an http.RoundTripper so outgoing calls are spans that carry the trace. See the Go SDK.

React Native and Expo

import AsyncStorage from '@react-native-async-storage/async-storage'
import { flaggrReactNative } from '@flaggr/react-native'
 
export const errors = flaggrReactNative({
  siteId: 'site_…', // the app's Site
  release: 'com.acme.shop@2.1.0+45',
  environment: __DEV__ ? 'development' : 'production',
  storage: AsyncStorage, // keeps a crash that kills the app until the next launch
})
errors.install() // before the root component renders

React Native and Expo covers what it catches, device context, screens and Hermes source maps.

Any language

Post batches of up to 100 events to POST /api/ingest/server-events with Authorization: Bearer <project API token>. The body is described under Flag variants below. From Python, for example:

import os, traceback, requests
 
def report(error: BaseException, flags: dict[str, str]) -> None:
    requests.post(
        "https://flaggr.dev/api/ingest/server-events",
        headers={"Authorization": f"Bearer {os.environ['FLAGGR_ERRORS_TOKEN']}"},
        json={"events": [{
            "kind": "error",
            "name": type(error).__name__,
            "message": str(error),
            "stack": "".join(traceback.format_exception(error)),
            "environment": "production",
            "release": os.environ.get("GIT_SHA"),
            "data": {"flags": flags, "handled": True},
        }]},
        timeout=5,
    )

The route answers 202 with accepted, rejected, sampledOut and throttled counts. It rejects an invalid event on its own, never the batch. A token can send 120 batches a minute (429 with Retry-After beyond that), and a body can be up to 256 KiB.

How events are grouped

  • Browser errors use the analytics script's fingerprint: the error's name, its message (without the engine's Uncaught prefix), the script URL (without query or fragment) and the line. Errors the client SDK reports get the same fingerprint, worked out from the stack's header and top frame. So an error a page reports through both is one issue.
  • Mobile errors are grouped by the error's type, its message (with numbers, ids and quoted values taken out) and the innermost function name that survives minification. A release bundle's positions change with every build, so they can't be used.
  • Server errors keep the fingerprint the sender gives. If it gives none, Flaggr groups the error by its stack: the error type plus the function and file of the five innermost frames of your own code. Line numbers, deploy paths and bundle content hashes are left out, so a deploy that moves code doesn't split an issue. Flaggr reads Go panics, Node.js and browser stacks (V8, Firefox, Safari) and Python tracebacks. An error without a stack is grouped by its type and message, with numbers, ids and quoted values taken out: user 42 not found and user 97 not found are one issue.

Frames from dependencies (node_modules, Go modules, site-packages), the language runtime and the standard library count as not in app. The issue's culprit is its innermost in-app frame.

Source maps

Minified stack traces from browser bundles and bundled servers become readable once you upload the build's source maps. The issue page then shows each frame as the original file, line and function, with the source lines around it, and names the issue's culprit the same way. Show minified switches back to the frames as they were reported.

Upload each release's maps with flaggr sourcemaps upload (Flaggr CLI v0.6.0 and later), under the release your app reports (the SDK's release option):

flaggr sourcemaps upload .next/static --url-prefix /_next/static --release "$GIT_SHA" --project <project-id>

Each map is stored under the path its minified file has in stack traces, such as /_next/static/chunks/app/checkout/page-8d7e6f1a.js, without origin or query. A frame uses a map when the frame's path ends with that path at a /. That's why dist/server.js covers /srv/app/dist/server.js.

Matching works like this:

  • The event's own release comes first.
  • An event without a release, or whose release has no map for the file, uses the newest map of that file from any release. That is safe for content-hashed bundle names, and the frame says which release's map it used.
  • The maps must include sourcesContent for the source lines to show. Most bundlers include it by default.

Over the API, PUT /api/projects/{id}/sourcemaps?release=&file= takes one map, plain or gzipped (Content-Encoding: gzip). It accepts up to 4 MiB as sent and 64 MiB decompressed, and needs write access. GET lists a project's releases, or one release's files with ?release=. DELETE ?release=[&file=] removes maps. A project keeps the maps of its last 20 releases, up to 2,000 files each. You don't have to serve your maps publicly.

Triage

Every issue starts unresolved. Members with write access can change its status, one issue at a time or up to 100 at once from the list:

StatusMeaning
UnresolvedNeeds attention. The list's default filter, which also shows regressed issues.
ResolvedFixed. You can record the release the fix ships in. Any event after the resolution reopens the issue as regressed.
RegressedResolved, then seen again. It stays regressed even after the event that reopened it leaves the time range.
IgnoredHidden from the unresolved list, either for a while (1 hour, 24 hours, 7 days) or until someone changes it. When an ignore runs out, the issue reads unresolved again.

Counts, first seen and last seen come from the events in the selected time range. Errors are kept for 30 days, so a range of 90 days reads 30.

Traces

An error sent with a traceId in its data (the Go SDK adds it to errors captured in a traced request) links to its trace on the Performance page. A browser error's fetch breadcrumbs link to the traces of the requests the page made.

Alerts

Alert rules can watch issues and notify your Slack, Discord, PagerDuty, email or webhook channels:

ConditionFires when
new_issueErrors no one has seen before arrive (threshold 0: any new issue)
issue_regressionA resolved issue comes back
issue_eventsOne issue has more events than the threshold in the window
crash_free_rateThe share of sessions without an unhandled error drops below the threshold, such as 99.5

Incidents, cooldowns, auto-resolve and notes work as for the other conditions. A new-issue or regression incident resolves itself after a full window with no more of them.

Release health

The Issues page's Releases view lists each release, from the release your errors carry. For each release it shows:

  • its crash-free session rate,
  • its error events and how many were unhandled,
  • the issues seen in it, and how many it introduced (issues whose earliest event in the range came from this release),
  • how many sessions and users the errors reached.

Pick a release to see its issues.

The crash-free rate needs sessions. flaggrErrors() in @flaggr/sdk reports each browser session once, with its release and environment (sessions: false turns that off). A session lasts across page loads in a tab until 30 minutes without activity, and counts as crashed when it had an unhandled error. Releases without sessions, such as server releases, show their errors only. Sessions are kept 90 days.

Over the API: GET /api/projects/{id}/issues/releases?range=24h|7d|14d|30d.

Flag variants

Each browser error carries the flags that were active on the page. The issue page shows which variants its events carried and what share of them had each one. If 80% of an issue's events came from new-cart = treatment, the new cart is the first place to look.

To get the same for server errors, send the evaluated flags with each event as data.flags, an object of flag key to variant:

{
  "events": [
    {
      "kind": "error",
      "serviceId": "svc-checkout-api",
      "environment": "production",
      "release": "checkout-api@2.8.1",
      "name": "panic",
      "message": "runtime error: invalid memory address or nil pointer dereference",
      "stack": "goroutine 12 [running]:\ngithub.com/acme/checkout/graph.(*Resolver).ApplyCoupon(...)\n\t/app/graph/coupon.go:88 +0x1d",
      "route": "/graphql",
      "method": "POST",
      "statusCode": 500,
      "timestamp": "2026-10-17T05:15:06Z",
      "data": {
        "userId": "user_3",
        "flags": { "coupon-engine": "v2" },
        "tags": { "region": "syd" },
        "breadcrumbs": [
          { "category": "http", "message": "POST /graphql ApplyCoupon", "timestamp": 1792214104000 },
          { "category": "db", "message": "SELECT coupons WHERE code = $1", "timestamp": 1792214104500 }
        ],
        "handled": false
      }
    }
  ]
}

The issue page reads these fields from an event's data:

  • userId (or user.id): counted as the issue's users.
  • flags: the variant breakdown.
  • tags: shown as the event's tags.
  • breadcrumbs: the events that led up to the error. Each one has category, message and timestamp.
  • handled: false counts the event as unhandled.

Every other key in data is shown as the event's context. Message, stack, route and every string in data are scrubbed before they're stored (tokens, email addresses and URL credentials are removed).

API

EndpointMethodsNotes
/api/projects/{id}/issuesGET, PATCHList issues. Query: range (1h, 24h, 7d, 14d, 30d), source (browser, server, mobile, all), status (unresolved, resolved, ignored, regressed, all), sort (last_seen, first_seen, events, users), query, limit, offset. PATCH sets the status of up to 100 issues.
/api/projects/{id}/issues/{fingerprint}?source=GET, PATCHOne issue: its trend, breakdowns, flag variants, a page of events (cursor, eventsLimit), and one event in full (eventId) with its stack frames, breadcrumbs, tags and context. PATCH sets this issue's status.

A status change takes status (unresolved, resolved or ignored), plus resolvedInRelease when resolving, or ignoreUntil (an ISO time) or ignoreForHours when ignoring. Reading needs read access to the project, and changing a status needs write access.

Callers who reach a project only through a public project (the shared demo, the showcase) see each issue's counts and breakdowns under a label made of its source and fingerprint. They never see an event's message, stack, URL, user or context, and they can't change a status.

From an AI assistant

With Flaggr's MCP server connected, an assistant can triage for you. The flaggr-errors agent skill teaches it the setup and the workflow.

ToolDoes
list_issuesThe issue list, with the same filters as the page
get_issueOne issue: culprit, trend, breakdowns, flag variants, and an event's stack and breadcrumbs
update_issue_statusResolve (optionally in a release), ignore or reopen up to 100 issues
get_performance, get_server_traceTransactions and one trace's waterfall
list_server_events, get_server_event_summaryRaw server events and their totals
create_alert_ruleAn alert on new_issue, issue_regression, issue_events or crash_free_rate

Try: "What are the top unresolved issues in production since yesterday's deploy, and does any of them follow a flag variant?"

Troubleshooting

SymptomCause and fix
No browser errors arriveCheck the network panel for POST https://ingest.flaggr.dev/e (script) or POST /api/analytics/sdk-telemetry (SDK). A 401 or 403 means a wrong Site id or SDK key. The script needs data-track-errors="true", and a window.onerror assigned after it replaces its handler.
Server events get 401 or 403The route takes a project API token that reads the whole project. SDK keys, evaluation-only keys, personal access tokens and tokens scoped to a service, environment or flag are refused.
Events arrive, but no issue showsBrowser rows from an origin that isn't the Site's domain are left out. Add ?includeUntrusted=1, or create a Site for that domain.
One bug is split into many issuesServer events with a varying fingerprint you set yourself, or minified browser errors whose message includes ids. Drop the custom fingerprint, or pass a stable one with captureException(error, { fingerprint }).
Unrelated errors share an issueA stack-less error falls back to its type and message. Send stacks, or a fingerprint.
Frames stay minifiedThe event's release has no map for that file and no other release has one either, the uploaded file path doesn't end the frame's path at a /, or the map lacks sourcesContent (no source lines). GET /api/projects/{id}/sourcemaps?release= lists what's stored.
No crash-free rate for a releaseOnly flaggrErrors() in browsers and @flaggr/react-native report sessions; server releases show errors only.
A resolved issue keeps coming backAny event after the resolution regresses it, including from clients still running the old release. Resolve "in a release" so it's clear which release the fix shipped in, and check the regression's event release.