Widget
The widget adds a report button to your app. When a user reports a problem it attaches console errors, failed requests, browser and page, and a screenshot only after the user confirms it. The loader is under 15 KB gzip and has no dependencies.
Install with a script tag
<script src="https://cdn.loopback.so/w.js" data-key="pk_live_..." data-release="2.4.0" data-api="https://api.loopback.so" async></script>| Attribute | Description |
|---|---|
data-key | Public key of the workspace: pk_live_… or pk_test_…. Required. |
data-release | Version of your app, for source maps and regression detection. |
data-locale | it or en; the workspace language by default. |
data-theme | auto, light or dark; then the page’s data-theme, then the system preference. |
data-api | API origin, for self-hosted installations. |
data-consent | granted, pending or denied: the consent of your app for passive collection. pending by default. |
data-position | bottom-right or bottom-left. |
Calls made before the script has loaded can be queued:
<script> window.Loopback = window.Loopback || []; Loopback.push(['identify', { id: 'u_123', email: 'mario@example.com' }]); Loopback.push(['setConsent', true]);</script>Install from npm
npm install @loopback/widgetimport { Loopback } from '@loopback/widget'; Loopback.init({ key: 'pk_live_...', release: '2.4.0', apiUrl: 'https://api.loopback.so', user: { id: 'u_123', email: 'mario@example.com' },});API
| Method | What it does |
|---|---|
init(options) | Starts the widget once and loads the configuration of the workspace. An unknown key or an origin that is not allowed turns the widget off. |
identify({ id, email?, name? }) | Sets the user. The id leaves the browser only as a salted hash; with your consent the email is linked to it, to notify affected users after a fix. |
setConsent(granted) | Your app’s consent: true allows passive error collection, if the workspace enabled it, and identify; false stops it and drops the buffers. |
open() / close() | Opens or closes the report form. |
captureException(error) | Records an exception your code handled. |
destroy() | Removes the interface and every listener, and restores console, fetch and XMLHttpRequest. |
React Native
import { captureRef } from 'react-native-view-shot'; // optional, for screenshotsimport { Loopback, LoopbackProvider } from '@loopback/widget-react-native'; Loopback.init({ key: 'pk_live_...', release: '2.4.0', apiUrl: 'https://api.loopback.so', captureScreenshot: (root) => captureRef(root, { format: 'jpg', quality: 0.7, width: 1080, result: 'data-uri' }),}); export default function App() { return ( <LoopbackProvider> <Navigation /> </LoopbackProvider> );}LoopbackProvider shows the report button and the sheet outside the captured view, so they never appear in screenshots. The screenshot function is optional.
iOS
import Loopback Loopback.initialize( key: "pk_live_...", release: "2.4.0", user: LoopbackUser(id: "u_123", email: "mario@example.com"), apiURL: URL(string: "https://api.loopback.so"))Swift Package for iOS 15 and later, with no third-party dependencies. The SwiftUI button is LoopbackReportButton; masked areas use .loopbackMask().
Android
import dev.loopback.Loopbackimport dev.loopback.LoopbackOptionsimport dev.loopback.LoopbackUser class App : Application() { override fun onCreate() { super.onCreate() Loopback.init(this, LoopbackOptions( key = "pk_live_...", release = BuildConfig.VERSION_NAME, user = LoopbackUser(id = "u_123", email = "mario@example.com"), apiUrl = "https://api.loopback.so", )) }}Kotlin library for Android 7.0 and later. Sensitive fields are masked automatically, in classic views and in Jetpack Compose; mask other content with view.loopbackMasked or Loopback.setMaskedArea.
Privacy, consent and masking
Everything is kept in memory until a report is sent, and only what the user and your app allowed leaves the browser.
| Data | Sent when |
|---|---|
| Report text and email | The user submits the form. |
| Page URL, release, and console, network and browser details | The user ticks the box that attaches technical details. |
| Screenshot | The user asked for it, saw the masked preview and confirmed it. |
| User id | Only as a salted SHA-256 hash, never in clear. |
| Passive error events | The workspace enabled passive collection and your app granted consent; sampled and rate-limited. |
| Email linked to the user hash | Your app granted consent. |
Masking
- Never read, and masked as solid blocks in screenshots: password, email and phone fields, card fields, one-time codes, fields named like card, iban, password, token or secret, and any element with
data-loopback-mask. On white-label installs, the neutraldata-sw-maskdoes the same. - Emails, card numbers, tokens and keys are scrubbed from console messages and URLs; sensitive query parameters become
[filtered]. - The server scrubs again before anything reaches a model.
<div data-loopback-mask> IBAN IT60 X054 2811 1010 0000 0123 456</div> <!-- White-label installs: the neutral attribute --><div data-sw-mask> IBAN IT60 X054 2811 1010 0000 0123 456</div>Content Security Policy
script-src https://cdn.loopback.soconnect-src https://api.loopback.soimg-src data:script-src: the loader and the screenshot module, loaded only when the user asks for a screenshot.connect-src: configuration, reports, events and identify.img-src data:: the screenshot and its preview.style-src https://fonts.googleapis.comandfont-src https://fonts.gstatic.com: only when the brand profile sets a font. Without them the widget uses the system font.
Upload your source maps with the GitHub Action, so that stack traces from minified code point to real files and lines.