Getting started
Widget
One script in your app opens a panel over it. Your users report a problem or suggest a feature without leaving the page, and CatchLoop attaches where they were. The panel is an iframe on our origin: your styles cannot reach it, and it cannot reach yours.
Install
Paste this before the closing </body> of your app, with the signed-in user filled in by your templates. Your snippet, with your workspace already in it, is in the dashboard under Settings, Integration.
<script
src="https://catchloop.co/widget.js"
data-project="acme"
data-user-id="4821"
data-user-email="ada@customer.com"
data-user-name="Ada Lovelace"
data-user-hash="9f86d0…"
defer
></script>
The script is under 4 kB compressed, loads with defer and never blocks your page. The panel itself loads the first time someone opens it. If CatchLoop is unreachable, the script does nothing and says nothing.
Identify your users
The widget acts as the user your app says is signed in, so they never sign in twice. Your server proves it: it signs the user's id and email with your widget secret, and the snippet carries the signature in data-user-hash. An id or an address typed into the page by anyone else matches no signature, so nobody can post or vote in someone else's name.
import { createHmac } from "node:crypto";
export const catchloopHash = (user) =>
createHmac("sha256", process.env.CATCHLOOP_WIDGET_SECRET)
.update(`${user.id}\n${user.email}`)
.digest("hex");
import hashlib, hmac, os
def catchloop_hash(user_id, email):
message = f"{user_id}\n{email}".encode()
return hmac.new(os.environ["CATCHLOOP_WIDGET_SECRET"].encode(), message, hashlib.sha256).hexdigest()
function catchloop_hash($userId, $email) {
return hash_hmac('sha256', $userId . "\n" . $email, getenv('CATCHLOOP_WIDGET_SECRET'));
}
Sign the id and the address exactly as the snippet carries them, joined by a newline. The user's account is their email address, the same account as on your portal; the id is how your team recognises them in the dashboard. The name is optional and becomes their display name if they have none yet.
The widget secret never leaves your server. Never put it in a browser bundle or a mobile app. If it leaks, regenerate it under Settings, Integration: every signature made with the old one stops working at once.
Without a signed user, the panel still opens: people can search your requests, and it tells them to sign in to your app to vote, post or report a problem. This identity works in the widget only, never on your portal, where customers sign in with their email.
Your button or ours
By default a round button sits in the bottom corner. To open the panel from your own Feedback button, help menu or settings page instead, switch ours off and mark yours:
<button data-catchloop-open>Give feedback</button>
<script
src="https://catchloop.co/widget.js"
data-project="acme"
data-launcher="custom"
defer
></script>
Any element carrying data-catchloop-open opens the panel, including one your app renders later. It keeps your text, your style and its place.
Options
| Attribute | Values | Note | What it does |
|---|---|---|---|
data-project | your workspace slug | required | Which board the feedback goes to. |
data-launcher | floating, custom | Our button, or only yours. Defaults to floating. | |
data-position | bottom-right, bottom-left | Where our button and the panel sit. | |
data-color | a hex colour | Our button's colour. The panel uses your portal's colour. | |
data-label | text | A label next to the icon on our button. | |
data-types | bug, feature | Offer only one of the two, for example data-types="feature". Both by default. | |
data-keep-params | comma-separated names | Query parameters kept in the page address, for example tab,view. All others are removed. | |
data-keep-hash | present or absent | Keep the part after #, for apps that route on it. |
JavaScript API
Once the script has run, three calls drive the panel:
window.Catchloop.open();
window.Catchloop.close();
// When your user signs in after the page loaded, with the hash from your server.
// null when they sign out.
window.Catchloop.identify({ id: "4821", email: "ada@customer.com", name: "Ada Lovelace", hash: "9f86d0…" });
A call made before the script has loaded would find nothing. Either wait for the catchloop:ready event on window, or define this stub first: the script replays what it queued.
window.Catchloop = window.Catchloop || {
q: [],
open: function () { this.q.push("open"); },
close: function () { this.q.push("close"); },
identify: function (user) { this.q.push(["identify", user]); },
};
React, Next.js, Vue
Load the script once, in the page shell (index.html, or the root layout with Next.js <Script strategy="afterInteractive">), not inside a component that mounts and unmounts. Client-side navigation needs nothing else: the page address and title are read each time the panel opens. If your app learns who the user is after it loaded, call Catchloop.identify() with the hash your API returns, and Catchloop.identify(null) on sign-out.
What is sent, and what never is
- Always, with each report or request: the page address without its query string and fragment (except what
data-keep-paramsanddata-keep-hashkeep), the page title, the window size and the browser language. We clean the address again on our side, and drop any parameter whose name looks like a token or a password even if you listed it. - Only if the user asks: a screenshot of an area they select. They see it before it is sent and can take it again, or send without it.
- Never: form fields, keystrokes, cookies, or the content of your page.
Feature requests land on your public board like any other request, with the page they came from visible only to your team. Problem reports are private: they appear on the Bugs page of your dashboard and nowhere else.
Screenshots
"Capture an area" hides the panel and lets the user draw a rectangle on the page, or click once for the whole visible page. Pressing Capture makes the browser ask to share the tab; the area is cut from it and shown in the panel. Nothing is attached until they press "Use this image", and they can retake it or cancel. It is sent with the report and nowhere else, and only your team can open it from the dashboard.
Chrome and Edge share the tab itself. Browsers that offer only a window or a screen send that whole window, which the preview shows before anything leaves. Phones cannot capture: the button is not offered there, and reports go without an image. Content from other sites inside your page, such as embedded iframes or protected video, may come out blank.