Skip to content
TraceItX Docs
Documentation

Install

One call, from any framework or none at all. Two minutes.

Updated

@traceitx/web is the framework-free capture core. There is no provider and no wrapper component — you call init() once and get a handle back.

Which install do I want?

Your appUse
Has a bundler — Vite, webpack, Next.js, Nuxt, SvelteKit, Angular CLI, AstroESM, below
Plain HTML, Rails, Django, WordPress — no build step at allScript tag, below
React@traceitx/react — a provider and hook over this same core

Both are ES modules and both produce the identical envelope. They differ only in who resolves the dependencies, and they are not interchangeable — see The two builds.

ESM

npm i @traceitx/web
import { init } from '@traceitx/web';

const traceitx = init({
  apiKey: 'txx_live_…',
  appVersion: '2.4.0',
});

document.getElementById('report-bug')?.addEventListener('click', () => {
  traceitx.open();
});

Script tag

<script type="module">
  import { init } from 'https://cdn.jsdelivr.net/npm/@traceitx/web@0.7.0/dist/browser/index.js';

  const traceitx = init({ apiKey: 'txx_live_…', appVersion: '2.4.0' });
  document.getElementById('report-bug').addEventListener('click', () => {
    traceitx.open();
  });
</script>

Three things about that block, each of which has bitten someone:

Pin the version. This SDK runs on every page of your site. @latest would auto-deploy a new release to all of your traffic at once, with no staged rollout and no way to hold it back. Pinned paths are immutable on jsDelivr and cached hard. If you would rather take patches automatically, @^0.7 is the opt-in.

One <script> block, not two. type="module" is deferred, so a classic inline <script> placed after it runs first — before init() was ever called. Do the import and the init() in the same module block.

Point it at dist/browser/index.js, not dist/index.js. The ESM entry reaches two of its capture dependencies by a bare specifier that a browser cannot resolve without an import map you would have to write yourself.

The tag may go in <head> with no defer and no DOMContentLoaded wrapper. Including the same URL twice is safe — the browser’s module registry is keyed by URL, so the duplicate never evaluates.

Call it from a client-side hook

init() touches window, document and localStorage, so it throws an actionable error if it runs during server-side rendering. Every recipe below is a client-side lifecycle hook for that reason; the one thing to avoid is calling init() at module scope in a file that also runs on the server.

// Vue
import { onMounted } from 'vue';
import { init } from '@traceitx/web';

onMounted(() => init({ apiKey: 'txx_live_…' }));
// Svelte
import { onMount } from 'svelte';
import { init } from '@traceitx/web';

onMount(() => init({ apiKey: 'txx_live_…' }));
// Angular
import { Component, OnInit } from '@angular/core';
import { init } from '@traceitx/web';

@Component({ selector: 'app-root', template: '...' })
export class AppComponent implements OnInit {
  ngOnInit(): void {
    init({ apiKey: 'txx_live_…' });
  }
}

In an .astro file, a plain <script> tag is already client-side — Astro ships it as-is. No client: directive is needed or accepted there; those are for framework islands.

Open the reporter

There is no floating “report a bug” button — you wire your own trigger, same contract as every other TraceItX SDK. What init() does install is the ⌘/Ctrl + Shift + B hotkey; see Configuration to rebind or disable it.

const result = await traceitx.open();
// result.status: 'submitted' | 'queued' | 'cancelled'

A small floating bubble appears once a device has existing reply conversations — that is an inbox entry point, not a report trigger, and nothing shows before then.

Re-initialising and teardown

init() is idempotent per page: calling it again while an instance is live logs a warning and hands back the existing handle rather than starting a second, racing SDK. That is what makes it safe under hot reload.

To genuinely restart it, tear the first one down:

traceitx.destroy();

destroy() removes the hotkey, the listeners, the thread polling, the mounted host element and the client. An app with client-side routing can init()/destroy() many times per page load without accumulating listeners.

Verify it

Open the reporter, file a report, and check the delivery log in the dashboard. A report that reached ingest but not your receiver is a webhook problem, not an SDK one — the log tells you which.

If nothing arrives at all, wire onError:

init({
  apiKey: 'txx_live_…',
  onError: (err) => console.warn('[traceitx]', err.name, err.message),
});

(debug: true installs the replay diagnostic seam rather than verbose logging — it will not print anything here.)

The three usual causes are a wrong key, a disabled: true left in a build, and a strict CSP with no cspNonce.

The two builds

dist/index.js (ESM)dist/browser/index.js (script tag)
Always-loaded cost≈121 KB gzipped≈121 KB gzipped
Loads from a bare <script type="module">?No — needs a bundler, or your own import mapYes; that is the point
React, konva, rrweb, screenshot rendererLeft external for your bundlerInlined into this build’s own lazy chunks

Both code-split, so React and the reporter UI are fetched only if someone actually opens the reporter — which is why the two cost the same up front. If you have a bundler, use the ESM install: it lets your bundler deduplicate anything you already ship. The script tag exists for pages that genuinely have no build step, not as a shortcut around adding one.

Requirements

FrameworkNone. Anything that runs JavaScript in a browser.
Bundle≈121 KB gzipped, either build
BrowsersEvergreen — the builds target ES2022 and the reporter uses Shadow DOM
Module formatESM only

Next

  • Configuration — every option
  • Sensitive content — mask before anything leaves the browser
  • Screen tracking — what is automatic
  • A worked Vue app lives in examples/vue-web in the repository, covering every option on this page