Preskočiť na obsah

Dokumentácia pre vývojárov je v angličtine, rovnako ako API.

Widget

The widget lets people report a bug or suggest an idea without leaving your app. Bug reports arrive with a screenshot and technical details; private data is masked in the browser before anything is sent.

Add it to your app

Two lines anywhere in your page. project is your board's address (https://ozvi.io/acme), token-url an endpoint of your app that says who is signed in.

HTML
<script src="https://ozvi.io/widget.js" defer></script>
<ozvi-widget project="acme" token-url="/api/ozvi-token" app-version="2.15.0"></ozvi-widget>

Allow your app's address in the board's settings (admin → Project → Your app's addresses); elsewhere the widget won't work. React and Vue components, the JavaScript API (Ozvi.open("bug"), events) and the inline roadmap and changelog are described in the widget README.

Sign people in

Your backend tells Ozvi who is signed in: an endpoint of your app (the widget's token-url) returns { "token": "…" }, a short-lived token signed with the board's SSO secret (admin → Sign-in → Turn on SSO). No second password and no Ozvi account for your users. Keep the secret on the server, never in the browser.

In Node.js, @ozvi/sdk (MIT, no dependencies) signs it. The token is a standard JWT signed with HMAC-SHA256, so any other language makes it with its standard library. Fields: id (required, stable id in your app), email, name, role (member, team or admin), locale and traits (company, plan, mrr in cents… shown to your team only).

Node.js: npm i @ozvi/sdk
import { signOzviToken } from "@ozvi/sdk";

// Express; in Next.js the same inside app/api/ozvi-token/route.ts
app.get("/api/ozvi-token", requireLogin, (req, res) => {
  res.json({ token: signOzviToken(
    { id: req.user.id, email: req.user.email, name: req.user.name,
      traits: { company: req.org.name, plan: req.org.plan } },
    process.env.OZVI_SSO_SECRET) });
});
Python
import base64, hashlib, hmac, json, os, time

def ozvi_token(user: dict, secret: str = os.environ["OZVI_SSO_SECRET"]) -> str:
    b64 = lambda o: base64.urlsafe_b64encode(json.dumps(o, separators=(",", ":")).encode()).rstrip(b"=").decode()
    now = int(time.time())
    body = f'{b64({"alg": "HS256", "typ": "JWT"})}.{b64({**user, "iat": now, "exp": now + 600})}'
    sig = hmac.new(secret.encode(), body.encode(), hashlib.sha256).digest()
    return f"{body}.{base64.urlsafe_b64encode(sig).rstrip(b'=').decode()}"
PHP
function ozvi_token(array $user, ?string $secret = null): string {
    $secret ??= getenv('OZVI_SSO_SECRET');
    $b64 = fn($s) => rtrim(strtr(base64_encode($s), '+/', '-_'), '=');
    $now = time();
    $body = $b64(json_encode(['alg' => 'HS256', 'typ' => 'JWT'])) . '.'
          . $b64(json_encode($user + ['iat' => $now, 'exp' => $now + 600], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES));
    return $body . '.' . $b64(hash_hmac('sha256', $body, $secret, true));
}

Keep it short-lived

Ozvi accepts a token only with the right secret and before exp. Ten minutes is plenty: the widget asks your endpoint for a fresh one whenever it needs it. Tokens carry your users' e-mails, so don't paste them into online decoders.

What a bug report captures

Everything is kept in memory only, masked in the browser before it is kept, and leaves the page only when the person sends a report. The network log, steps and session links are visible to your team only; the Ozvi server masks and bounds them again.

WhatDetailNever
PageAddress (masked), app version, browser, window size, language
ScreenshotThe visible part of the page without the widget, with private data covered (below). The reporter sees it first, can mark it up or remove it.Form values, e-mails, phone numbers, IBANs, card numbers, tokens, data-ozvi-mask
Console errorsLast 20, 300 characters each
Network logLast 30 requests: method, URL, status, duration, sizeHeaders (no Authorization, no Cookie); bodies unless you turn them on
StepsLast 30 page changes and clicks on buttons and links, by nameWhat anyone typed

In URLs and logged text, values of parameters named like token, password, key, session or card, and any JWT, Bearer credential, e-mail address, IBAN or card number become ***.

Screenshot privacy

The screenshot is drawn from a copy of the page that the widget edits first. The page the person sees is never touched, and what is covered never becomes pixels: covered text is drawn transparent on a solid box, covered images are not drawn at all. Without any setup, Ozvi covers:

  • the values of every input, textarea, select and editable area (checkboxes and radio buttons keep their state);
  • e-mail addresses, phone numbers, IBANs, card numbers and tokens anywhere in the text; the rest of the sentence stays readable;
  • everything inside elements marked data-ozvi-mask.

If the copy can't be masked, the report is sent without a screenshot, never with an unmasked one.

Mark what can't be recognised: data-ozvi-mask

Names, notes, diagnoses or amounts don't have a recognisable shape. Mark them and the whole element is covered, its images too:

HTML
<td data-ozvi-mask>Jana Kováčová</td>
<p data-ozvi-mask>{{ client.note }}</p>

Cover everything

For apps where almost everything on screen is private (health care, finance, HR), cover all text and images and reveal only what helps your team understand the page, such as navigation and headings:

JavaScript and HTML
<script>window.OzviConfig = { mask: "all" };</script>
<nav data-ozvi-unmask>…</nav>
<h1 data-ozvi-unmask>Invoice settings</h1>

Your privacy policy

The board's settings (admin → Project data → Text for your privacy policy) generate paragraphs describing exactly what your widget sends, for your own privacy policy.

Configuration

Set window.OzviConfig before widget.js loads, or call Ozvi.configure() later.

window.OzviConfig
window.OzviConfig = {
  mask: "auto",          // "all" = cover all text and images except [data-ozvi-unmask]
  network: true,         // false = no network log
  networkBodies: false,  // true = masked bodies of failed requests (max 2 000 characters)
  actions: true,         // false = no steps
  maskRequest(entry) {   // last word on every logged request; return null to drop it
    return { ...entry, url: entry.url.replace(/\/customers\/\d+/, "/customers/:id") };
  },
};

More

The full reference (JavaScript API, events, React and Vue, inline roadmap and changelog, strict CSP with a nonce, the screenshot editor) is in the widget README. Try the widget in a sample app on the demo.