writetrack
v0.12.1
Published
Lightweight keystroke dynamics capture for web applications
Maintainers
Readme
WriteTrack
Capture and analyze the writing process through keystroke dynamics.
WriteTrack is an embeddable SDK that instruments text inputs to record how text was written — timing, rhythm, corrections, clipboard usage, and composition patterns. It gives platforms behavioral evidence of the writing process, not just the finished text.
- ~142KB gzipped (20KB JS + 122KB WASM)
- 0 runtime dependencies
- <1ms per keystroke
- 100% client-side
Installation
npm install writetrackOptionally, register for a free trial license:
npx writetrack init # starts a 28-day trial and writes the key to .envA license is required for production domains. On localhost, all features work without a license key.
Quick Start
import { WriteTrack } from 'writetrack';
const responseField = document.getElementById('response-field')!;
const tracker = new WriteTrack({
target: responseField,
});
tracker.start();
// User types...
// Collect captured data
const data = tracker.getData();
const analysis = await tracker.getAnalysis();
tracker.stop();Works with any text input, textarea, or contenteditable element. TypeScript definitions included.
Note: WriteTrack uses WASM, which requires an HTTP server —
file://won't work. Any bundler (Vite, webpack, etc.) handles this automatically. For a no-bundler setup, see the vanilla JS guide.
React / Next.js
import { useRef } from 'react';
import { useWriteTrack } from 'writetrack/react';
function ResponseForm() {
const textareaRef = useRef<HTMLTextAreaElement>(null);
const { tracker } = useWriteTrack(textareaRef);
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
if (tracker) {
const data = tracker.getData();
const analysis = await tracker.getAnalysis();
console.log('Session:', data.quality, analysis);
}
};
return (
<form onSubmit={handleSubmit}>
<textarea ref={textareaRef} rows={10} />
<button type="submit">Submit</button>
</form>
);
}For App Router, Server Actions, and dev-mode WASM setup, see the Next.js guide.
Production
Add your license key to enable analysis on production domains:
const tracker = new WriteTrack({
target: textarea,
license: process.env.WRITETRACK_LICENSE_KEY,
});On localhost, all features work without a key. See the licensing guide for trial setup, domain binding, and expiration details.
Framework Integrations
First-party bindings available as subpath exports:
import { useWriteTrack } from 'writetrack/react';
import { useWriteTrack } from 'writetrack/vue';
import { WriteTrackExtension } from 'writetrack/tiptap';
import { WriteTrackPlugin } from 'writetrack/ckeditor';
import { WriteTrackPlugin } from 'writetrack/prosemirror';
import { WriteTrackModule } from 'writetrack/quill';
import { createWriteTrackLexical } from 'writetrack/lexical';
import { createWriteTrackSlate } from 'writetrack/slate';
import 'writetrack/tinymce'; // registers native TinyMCE pluginOutput Sinks
Stream session data to your analytics stack via .pipe():
import { webhook, datadog, segment, opentelemetry } from 'writetrack/pipes';
tracker
.pipe(webhook({ url: '/api/writetrack' }))
.pipe(datadog({ client: dd }))
.pipe(segment({ client: analytics }))
.pipe(opentelemetry({ tracer }));What It Captures
WriteTrack instruments text inputs and records the behavioral signals behind every keystroke:
- Timing intervals — milliseconds between each keystroke
- Rhythm variance — consistency or erratic cadence
- Correction patterns — backspaces, rewrites, hesitation
- Clipboard events — paste, copy, cut with content and context
- Selection events — text selections via mouse, keyboard, or programmatic
- Composition events — IME input for CJK and other non-Latin scripts
What It Produces
WASM-powered analysis returns a SessionAnalysis covering seven dimensions:
- Content origin — what proportion was typed, pasted, or autocompleted
- Timing authenticity — rhythm consistency, periodicity, entropy
- Revision behavior — corrections, navigation, undo/redo patterns
- Session continuity — focus changes, tab-aways, session structure
- Physical plausibility — impossible sequences, synthetic event markers
- Temporal patterns — speed over time, warmup/fatigue, burst patterns
- Writing process — planning, drafting, and revision phase segmentation
Each dimension produces a machine-readable indicator code and detailed metrics suitable for visualization.
Use Cases
Education — Give instructors behavioral context alongside submitted work. See whether text was composed or pasted.
Assessment — Add a behavioral layer to written responses. Distinguish engaged composition from copy-paste submission.
Compliance — Know when form attestations were typed versus pasted.
Research — Capture writing process data for studies on composition behavior, revision patterns, and typing fluency.
API
Constructor
new WriteTrack(options: WriteTrackOptions | HTMLElement)Options
interface WriteTrackOptions {
target: HTMLElement;
license?: string; // Omit for localhost evaluation
userId?: string; // Opaque user identifier, included in output metadata
contentId?: string; // Opaque document identifier, included in output metadata
metadata?: Record<string, unknown>; // Arbitrary metadata, included in output
wasmUrl?: string; // Custom URL for the WASM analysis binary
persist?: boolean; // Enable IndexedDB persistence (requires contentId)
cursorPositionProvider?: () => number; // Custom cursor position for rich-text editors
inputSourceProvider?: () => InputSource | undefined; // Input source classification (set by editor integrations)
}Methods
| Method | Returns | Description |
| ---------------------------------- | -------------------------------- | --------------------------------------------- |
| start() | void | Begin capturing events |
| stop() | void | Stop capturing and clean up |
| stopAndWait() | Promise<void> | Stop and wait for pending IndexedDB save |
| getData() | WriteTrackDataSchema | Export full session data |
| getText() | string | Current text content |
| getRawEvents() | KeystrokeEvent[] | All captured keystroke events |
| getClipboardEvents() | ClipboardEvent[] | Paste/copy/cut events |
| getSelectionEvents() | SelectionEvent[] | Text selection events |
| getUndoRedoEvents() | UndoRedoEvent[] | Undo/redo events |
| getCompositionEvents() | CompositionEvent[] | IME composition events |
| getProgrammaticInsertionEvents() | ProgrammaticInsertionEvent[] | Programmatic insertion events |
| getSessionDuration() | number | Session duration in ms |
| getActiveTime() | number | Active (visible) session time in ms |
| getKeystrokeCount() | number | Total keystrokes captured |
| getAnalysis() | Promise<SessionAnalysis\|null> | WASM-powered session analysis |
| getSessionReport() | Promise<SessionReport> | Combined data + analysis |
| pipe(sink) | this | Register an output sink |
| on(event, handler) | () => void | Register event listener (returns unsubscribe) |
| isLicenseValid() | boolean | Whether license is valid |
| isLicenseValidated() | boolean | Whether license check has completed |
| isTargetDetached() | boolean | Whether the target element was removed |
| clearPersistedData() | Promise<void> | Clear IndexedDB persisted session |
getData() returns a WriteTrackDataSchema with four top-level fields: version, metadata (session context, user/content IDs, custom fields), session (raw events and text), and quality (completeness score, validation status). See the API reference for the full shape.
Events
The on(event, handler) method supports the following events:
| Event | Payload | Description |
| ------------ | ---------------------------------------------------------------- | --------------------------------------------------------------- |
| change | { eventCount: number; keystrokeCount: number } | Fires on any data mutation (keystroke, paste, selection, etc.) |
| tick | { activeTime: number; totalTime: number; tracker: WriteTrack } | Fires every ~1s while session is active |
| analysis | SessionAnalysis | Fires when analysis updates (lazy-loads WASM on first listener) |
| ready | (none) | Fires when tracker is ready (late listeners auto-fire) |
| wasm:ready | (none) | Fires when WASM binary loads successfully |
| wasm:error | Error | Fires if WASM binary fails to load |
| stop | (none) | Fires when stop() is called |
| pipe:error | Error, WriteTrackSink | Fires when an output sink fails |
const unsub = tracker.on('change', ({ eventCount, keystrokeCount }) => {
console.log(`${keystrokeCount} keystrokes, ${eventCount} total events`);
});
// Later: unsub();Static Methods
| Method | Returns | Description |
| ------------------------------------------------ | --------------------------------- | ---------------------------------------- |
| WriteTrack.listPersistedSessions() | Promise<PersistedSessionInfo[]> | List all persisted sessions in IndexedDB |
| WriteTrack.deletePersistedSession(key, field?) | Promise<void> | Delete persisted session data |
Properties
| Property | Type | Description |
| ----------- | --------------- | ------------------------------------------- |
| wasmReady | boolean | Whether WASM module is loaded (getter) |
| ready | Promise<void> | Resolves when persisted session is restored |
Server-Side Analysis
Analyze previously captured data without a DOM element:
import { analyzeEvents } from 'writetrack';
const data = JSON.parse(savedSessionJson); // from your API, database, etc.
const analysis = await analyzeEvents(data, {
licenseKey: process.env.WRITETRACK_LICENSE_KEY,
});Analysis Helpers
Convenience functions for extracting commonly needed values from a SessionAnalysis:
| Function | Returns | Description |
| ------------------------------------- | ---------------------------------------------------------- | -------------------------------- |
| getSessionDurationMs(analysis) | number | Session duration in milliseconds |
| getTypingSpeed(analysis) | { cpm: number; wpm: number } | Characters and words per minute |
| getContentOriginBreakdown(analysis) | { typed: number; pasted: number; autocompleted: number } | Content origin ratios (0-1 each) |
import { getTypingSpeed, getContentOriginBreakdown } from 'writetrack';
const analysis = await tracker.getAnalysis();
if (analysis) {
const { wpm } = getTypingSpeed(analysis);
const { typed, pasted } = getContentOriginBreakdown(analysis);
}Testing Utilities
The writetrack/testing subpath export provides framework-agnostic factories for creating mock WriteTrack instances and SessionAnalysis objects in consumer tests:
import { createMockAnalysis, createMockTracker } from 'writetrack/testing';
const analysis = createMockAnalysis({ keydownCount: 500 });
const tracker = createMockTracker({ analysis });| Function | Returns | Description |
| -------------------------------- | ----------------- | -------------------------------------------------- |
| createMockAnalysis(overrides?) | SessionAnalysis | Complete mock analysis with deep-partial overrides |
| createMockTracker(options?) | MockWriteTrack | Mock tracker with configurable analysis and data |
Exported Types
Full type definitions are included in the package:
Functions: analyzeEvents, createSessionReport, formatIndicator, getHighResolutionTime, WriteTrackController
Types: WriteTrackOptions, PersistedSessionInfo, KeystrokeEvent, ClipboardEvent, SelectionEvent, UndoRedoEvent, CompositionEvent, ProgrammaticInsertionEvent, ModifierState, InputSource, WriteTrackDataSchema, DataQualityMetrics, SessionMetadata, SessionAnalysis, SessionReport, IndicatorOutput, ContentOriginAnalysis, TimingAuthenticityAnalysis, SessionContinuityAnalysis, PhysicalPlausibilityAnalysis, RevisionBehaviorAnalysis, TemporalPatternsAnalysis, WritingProcessAnalysis, WritingProcessSegment, PhaseTransition, WindowFeatures, AnalyzeEventsOptions, WriteTrackSink, WebhookOptions, DatadogOptions, DatadogClient, SegmentOptions, SegmentClient, OpenTelemetryOptions, OTelTracer, OTelSpan, BaseBindingOptions, OnTickData, ControllerState, ControllerOptions
Browser Support
| Browser | Version | | ------- | ------- | | Chrome | 90+ | | Firefox | 88+ | | Safari | 14+ | | Edge | 90+ |
Privacy
WriteTrack runs entirely client-side. No servers, no tracking, no external requests. Event data (including paste content and selected text) stays in the browser unless you explicitly export it via getData() or an output sink.
Documentation
Full documentation at writetrack.dev/docs.
License
See LICENSE for details.
