Troubleshooting
Tracker Not Capturing Keystrokes
Section titled “Tracker Not Capturing Keystrokes”For rich text editors (TipTap, ProseMirror, CKEditor, Quill), make sure you’re targeting the actual editable element, not a wrapper:
// Wrong: targeting a container divnew WriteTrack({ target: document.querySelector('.editor')! });
// Right: targeting the actual contenteditable elementnew WriteTrack({ target: document.querySelector('.editor .ProseMirror')!,});Use the framework-specific bindings (useWriteTrack, WriteTrackExtension, etc.) to avoid this — they handle element targeting automatically.
If events still aren’t captured, check whether other code is calling event.stopPropagation() on keyboard events.
POOR Session Quality
Section titled “POOR Session Quality”Session quality is a composite score (0–1) based on four factors: whether keystroke events exist, whether text content exists, whether timestamps are valid, and whether the session lasted more than 1 second.
| Score | Quality Level |
|---|---|
| >= 0.9 | EXCELLENT |
| >= 0.7 | GOOD |
| >= 0.4 | FAIR |
| < 0.4 | POOR |
A session with many keystrokes can still be FAIR or POOR if timestamps are out of order or the session duration is under 1 second.
If the user pasted most of the content, there won’t be enough keystroke data for analysis. Check tracker.getClipboardEvents() to see if paste events dominate the session.
Mobile / Swipe Typing
Section titled “Mobile / Swipe Typing”Swipe/gesture typing produces unusual patterns — fewer distinct keydown/keyup events and different timing. This may result in FAIR quality or unexpected analysis results.
React / Vue Issues
Section titled “React / Vue Issues”Ref not attached
Section titled “Ref not attached”The hook/composable won’t work if the ref isn’t attached to a DOM element:
// Wrong: ref not assigned to elementconst { tracker } = useWriteTrack(textareaRef);return <textarea />; // Missing ref={textareaRef}
// Rightreturn <textarea ref={textareaRef} />;Component unmounted before getData
Section titled “Component unmounted before getData”Retrieve data before the component unmounts:
const handleSubmit = () => { if (tracker) { const data = tracker.getData(); // Call before unmount submitForm(data); }};WASM Loading
Section titled “WASM Loading”getAnalysis() returns null when licensing, WASM loading, or analyzer-output
validation prevents analysis. Check the browser console for the corresponding
warning.
Analyzer returned no usable result
Section titled “Analyzer returned no usable result”Preserve the raw result from getData() and report the keydown and keyboard
event counts included in the warning.
Issue #842 tracks
the current reproduction status.
Bundler production builds
Section titled “Bundler production builds”Vite, webpack, and Rollup production builds emit writetrack.wasm from the
package’s static asset reference. If the console reports a WASM load failure,
check that the deployed build contains the emitted asset and that its URL is
reachable.
For a custom asset pipeline, serve the file yourself and point wasmUrl at it.
See Custom WASM location below.
Next.js
Section titled “Next.js”Turbopack does not treat .wasm as an emitted static asset, so the packaged
binary is not resolved for you. Copy the asset to public/ and pass wasmUrl.
See the Next.js guide for details.
Vite dev mode (Vite 7 and earlier only)
Section titled “Vite dev mode (Vite 7 and earlier only)”Vite ≤7 uses esbuild to pre-bundle dependencies in the dev server, which rewrites import.meta.url to import_meta.url and breaks WASM path resolution. If getAnalysis() returns null in dev with Vite 7 or earlier, add this to your Vite config:
import { defineConfig } from 'vite';
export default defineConfig({ optimizeDeps: { exclude: ['writetrack'], },});This config only affects the dev server. Vite 8’s Rolldown pre-bundler preserves
import.meta.url and does not need this exclusion. Production builds emit the
WASM asset automatically.
Custom WASM location
Section titled “Custom WASM location”If your hosting setup serves the WASM file somewhere other than /writetrack.wasm, point wasmUrl at it:
const tracker = new WriteTrack({ target: textarea, wasmUrl: '/static/writetrack.wasm',});Copy the WASM file from node_modules/writetrack/dist/writetrack.wasm to your public/static directory.
License Key Issues
Section titled “License Key Issues””Production use requires a license key”
Section titled “”Production use requires a license key””This warning appears when using WriteTrack on a non-localhost domain without a license. Analysis (getAnalysis()) still works on localhost for evaluation. To fix:
npx writetrack initThis starts a 28-day free trial and writes your key to .env.
”Secure context required”
Section titled “”Secure context required””License validation uses the Web Crypto API (crypto.subtle), which browsers only expose in a secure context — HTTPS or localhost. On a plain-HTTP origin (internal staging, a LAN IP, an intranet host), validation fails even with a valid key. Serve the page over HTTPS to fix it; the key itself is fine.
”License expired”
Section titled “”License expired””Your license has expired. Renew at writetrack.dev to keep analysis available.
”getAnalysis() requires a license key”
Section titled “”getAnalysis() requires a license key””Analysis returns null without a valid license on production domains. Capture (getData()) always works — only WASM-powered analysis requires a license.
Target Element Removed from DOM
Section titled “Target Element Removed from DOM”If the tracked element is removed from the DOM (e.g., by a framework re-render), WriteTrack logs:
“Target element was removed from the DOM. Recording stopped.”
Recording stops automatically. To recover, create a new WriteTrack instance targeting the new element — or use the framework bindings (useWriteTrack, WriteTrackExtension) which handle this via refs and lifecycle hooks.
IndexedDB Persistence Failures
Section titled “IndexedDB Persistence Failures””IndexedDB unavailable”
Section titled “”IndexedDB unavailable””Persistence silently degrades if IndexedDB is unavailable (private browsing in some browsers, storage quota exceeded, or restrictive iframe policies). Capture continues normally — only cross-page-load resume is lost.
”failed to persist session to IndexedDB”
Section titled “”failed to persist session to IndexedDB””This can happen if:
- The browser’s storage quota is full
- The page is in a cross-origin iframe without storage access
- IndexedDB was cleared by the browser during the session
Persistence is best-effort — capture data is always available via getData() regardless.
Multiple Tracker Instances
Section titled “Multiple Tracker Instances”Multiple WriteTrack instances on the same page are fully independent — each tracks its own element with its own events, timers, and persistence. The WASM module is loaded once and shared across instances.
// Safe: two independent trackersconst titleTracker = new WriteTrack({ target: titleInput });const bodyTracker = new WriteTrack({ target: bodyTextarea });When using persist: true on multiple fields, each must have a unique contentId.
Content Security Policy
Section titled “Content Security Policy”If your site uses a strict CSP and you’re using the analysis module, you’ll need to allow WebAssembly compilation. Add wasm-unsafe-eval to your script-src directive:
Content-Security-Policy: script-src 'self' 'wasm-unsafe-eval';This is narrower than unsafe-eval — it only permits WebAssembly compilation, not eval() or new Function().