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 / Analysis Returns Null {#wasm-loading}
Section titled “WASM Loading / Analysis Returns Null {#wasm-loading}”getAnalysis() returns null when the WASM module can’t be loaded. Check the browser console for a warning with details.
Next.js dev mode
Section titled “Next.js dev mode”Next.js 15+ uses Turbopack for next dev, which doesn’t resolve WASM paths automatically. See the Next.js guide for the fix.
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 is only needed in dev — Vite production builds handle WASM correctly.
Vite 8+ does not need this workaround. Vite 8 swapped the dev-time pre-bundler to Rolldown, which preserves import.meta.url so WASM resolves correctly out of the box. Verified against Vite 8.0.9 with no custom config.
Custom WASM location
Section titled “Custom WASM location”If your bundler or hosting setup doesn’t serve the WASM file at the default URL, pass wasmUrl pointing to the file:
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.
”License expired — X day(s) remaining in grace period”
Section titled “”License expired — X day(s) remaining in grace period””Your license has expired but is in a 14-day grace period. Analysis still works during this window. Renew at writetrack.dev before the grace period ends.
”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().