This is the full developer documentation for WriteTrack # WriteTrack Documentation > Client-side keystroke dynamics capture and behavioral analysis WriteTrack is a TypeScript SDK that captures and analyzes typing behavior. It records behavioral signals like timing, rhythm, corrections, and clipboard interactions from any text input element. [Get Started ](/docs/quickstart/)Install and integrate WriteTrack in under 5 minutes [Download Full Docs ](/llms-full.txt)Get all documentation as a single file for LLMs ## Key Features [Section titled “Key Features”](#key-features) * **100% Client-side** — All capture and analysis happens in the browser; no data is sent anywhere unless you configure [output sinks](/docs/output-sinks/) * **Framework agnostic** — Works with vanilla JS, React, Vue, Svelte, or any frontend framework * **Lightweight** — \~115KB gzipped, zero runtime dependencies * **Detailed Behavioural Data** — Captures timing, rhythm, corrections, clipboard usage, and more ## Quick Example [Section titled “Quick Example”](#quick-example) ```typescript import { WriteTrack, webhook } from 'writetrack'; const textarea = document.querySelector('#response-field')!; const tracker = new WriteTrack({ target: textarea }); // Session data is POSTed to your endpoint when getData() is called tracker.pipe(webhook({ url: 'https://api.example.com/writetrack' })); tracker.start(); // ... user types ... // Retrieve session data and dispatch to all registered sinks const data = tracker.getData(); ``` ## How It Works [Section titled “How It Works”](#how-it-works) WriteTrack instruments your text inputs and captures behavioral signals: 1. **Timing Patterns** — Dwell time (key hold duration), flight time (between keystrokes), rhythm consistency 2. **Correction Behavior** — Backspaces, deletions, arrow key navigation, revision patterns 3. **Clipboard Interactions** — Paste events, selection patterns, copy/cut operations 4. **Session Dynamics** — Typing bursts, natural pauses, fatigue progression 5. **Authenticity Analysis** — Optional WASM-powered analysis scores each session across six categories: content origin, timing authenticity, session continuity, physical plausibility, revision behavior, and temporal patterns ## Browser Compatibility [Section titled “Browser Compatibility”](#browser-compatibility) * **Desktop**: Chrome 90+, Firefox 88+, Safari 14+, Edge 90+ * **Mobile**: iOS Safari 14+, Android Chrome 90+, Mobile Firefox 88+ ## Next Steps [Section titled “Next Steps”](#next-steps) * [Quickstart](/docs/quickstart/) — Install and integrate in under 5 minutes * [Licensing](/docs/license/) — License keys for production use * [API Reference](/docs/api-reference/) — Complete method reference # Analysis > Run typing authenticity analysis on captured sessions WriteTrack can analyze captured typing sessions to assess authenticity. Analysis runs in a compiled WebAssembly module — it loads on demand and returns structured results across six behavioral categories. ## Browser-Side Analysis [Section titled “Browser-Side Analysis”](#browser-side-analysis) Call `getAnalysis()` on a tracker after the user finishes typing: ```typescript import { WriteTrack, formatIndicator } from 'writetrack'; const tracker = new WriteTrack({ target: textarea }); tracker.start(); // ... user types ... tracker.stop(); const analysis = await tracker.getAnalysis(); if (analysis) { console.log(analysis.contentOrigin.indicator.code); // → "WT-100" console.log(formatIndicator(analysis.contentOrigin.indicator)); // → "All text was typed directly" console.log(formatIndicator(analysis.physicalPlausibility.indicator)); // → "All keystroke timing is physically plausible" } ``` `getAnalysis()` returns a [`SessionAnalysis`](/docs/api-reference/#sessionanalysis) object, or `null` if the WASM module fails to load. The first call triggers a lazy load of the WASM binary (\~100KB); subsequent calls reuse the cached module. The `sufficientData` field on the result tells you whether enough events were captured for meaningful analysis. Short sessions or sessions with very few keystrokes may return `sufficientData: false` — the metrics will still be populated, but treat them with caution. ### Session Reports [Section titled “Session Reports”](#session-reports) `getSessionReport()` bundles raw data and analysis into a single object — useful when you want to send everything to your server in one payload: ```typescript const report = await tracker.getSessionReport(); // { data: WriteTrackDataSchema, analysis: SessionAnalysis | null } await fetch('/api/submit', { method: 'POST', body: JSON.stringify(report), }); ``` ## Server-Side Analysis [Section titled “Server-Side Analysis”](#server-side-analysis) The `analyzeEvents()` function analyzes a `WriteTrackDataSchema` object without needing a DOM element. Use this when you’ve already collected session data and want to run analysis on your server or in a background job: ```typescript import { analyzeEvents } from 'writetrack'; // data is a WriteTrackDataSchema object from getData() const analysis = await analyzeEvents(data); if (analysis) { const { contentOrigin, timingAuthenticity, physicalPlausibility } = analysis; // Inspect category metrics and indicators } ``` Note Server-side analysis requires a Node.js environment with WebAssembly support (Node 16+). The `locale` field will be empty since `navigator.language` is unavailable; set it on the returned object if you have locale context. ## Analysis Categories [Section titled “Analysis Categories”](#analysis-categories) Each category contains an `indicator` (an [`IndicatorOutput`](/docs/api-reference/#indicatoroutput) with a machine-readable `code` and numeric `params`) and a `metrics` object with the underlying data. Use [`formatIndicator()`](/docs/api-reference/#formatindicator) to convert an indicator to a plain-English string, or make your own decisions from the raw metrics. ### Content Origin [Section titled “Content Origin”](#content-origin) Where the text came from: typed, pasted, or autocompleted. | Metric | Type | Description | | ------------------------------------------ | ----------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `charactersByOrigin.typed` | `number` | Ratio of directly typed characters (0–1) | | `charactersByOrigin.pasted` | `number` | Ratio of pasted characters (0–1) | | `charactersByOrigin.autocompleted` | `number` | Ratio of autocompleted characters (0–1) | | `pasteEvents` | `array` | Each paste with `timestamp`, `characterCount`, `source` (`internal` / `external` / `unknown`), `precedingTabAwayDuration` (ms away before pasting), `contentMatchedDocument`, `editsInRegion` (keydowns within paste region within 60s), `firstEditDelayMs` (ms to first edit), and `reworkRatio` (edits / pasted chars, 0.0 = untouched) | | `pasteClassification.internalPasteRatio` | `number` | Ratio of chars from internal paste (cut-paste restructuring) to total chars (0–1) | | `pasteClassification.externalPasteRatio` | `number` | Ratio of chars from external paste (outside content) to total chars (0–1) | | `pasteClassification.unknownPasteRatio` | `number` | Ratio of chars from unknown-source paste to total chars (0–1) | | `pasteEditSummary.pasteRetentionRate` | `number` | Fraction of pastes with zero edits in their region within 60 seconds (0–1) | | `pasteEditSummary.meanModificationDelayMs` | `number` | Average delay (ms) from paste to first edit, across pastes that were edited | | `pasteEditSummary.pasteToTypeRatio` | `number` | Total pasted characters / total typed characters | | `pasteEditSummary.meanReworkRatio` | `number` | Average rework ratio across all pastes (0 = no editing, >1 = heavy rework) | | `pasteEditSummary.pasteLengthMean` | `number` | Mean paste size in characters | | `pasteEditSummary.pasteLengthStd` | `number` | Standard deviation of paste sizes | | `pasteEditSummary.pasteLengthMin` | `number` | Smallest paste size in characters | | `pasteEditSummary.pasteLengthMax` | `number` | Largest paste size in characters | | `contentTimeline` | `MultiTimeSeries` | Character origin ratios over time | Content origin has two indicators: a primary `indicator` for overall content source (WT-100 through WT-104) and a `pasteReworkIndicator` for paste editing behavior (WT-105 through WT-107). Example primary indicators: * *“All text was typed directly”* * *“62% of text was pasted from external sources”* * *“35% of text was inserted via autocomplete”* Example paste rework indicators: * *“Unmodified external content detected — pasted text was not edited”* (WT-105) * *“40% of pasted content was subsequently reworked”* (WT-106, positive authenticity signal) * *“75% paste ratio with 90% left unmodified”* (WT-107) ### Timing Authenticity [Section titled “Timing Authenticity”](#timing-authenticity) Whether keystroke timing looks human or mechanical. | Metric | Type | Description | | ------------------------ | ----------------- | ------------------------------------------------------------------------------------------------ | | `dwellTimeDistribution` | `object` | Mean, coefficient of variation (CV), histogram, and human reference range for key hold durations | | `flightTimeDistribution` | `object` | Same structure for intervals between keystrokes | | `periodicityScore` | `number` | How periodic the keystroke rhythm is (0–1, higher = more periodic) | | `entropy` | `number` | Shannon entropy of timing intervals | | `timingOverTime` | `MultiTimeSeries` | Timing metrics across the session | Example indicators: * *“Timing variability is within normal range”* * *“Keystroke timing is mechanically uniform”* * *“Highly periodic keystroke pattern detected”* ### Session Continuity [Section titled “Session Continuity”](#session-continuity) Behavioral consistency throughout the session — detects abrupt rhythm shifts and tab-away patterns. | Metric | Type | Description | | ------------------- | ----------------- | -------------------------------------------------------------------------------------------------------------------------------------------------- | | `changePoints` | `array` | Points where typing behavior shifted abruptly. Each has `timestamp`, `metric` (which metric changed), `valueBefore`, `valueAfter`, and `magnitude` | | `tabAwayEvents` | `array` | Each tab-away with `leftAt`, `returnedAt`, `duration` (ms), and `activityAfterReturn` (`typing`, `paste`, or `idle`) | | `segmentComparison` | `MultiTimeSeries` | Behavioral metrics compared across session segments | Example indicators: * *“Consistent behavior throughout session”* * *“Typing rhythm changed abruptly at 45s (magnitude 3.2)”* * *“7 tab-away events detected during session”* ### Physical Plausibility [Section titled “Physical Plausibility”](#physical-plausibility) Whether the events could have been produced by a physical keyboard. | Metric | Type | Description | | --------------------------- | -------- | ------------------------------------------------------------------------------------------------------ | | `impossibleSequences` | `array` | Keystroke pairs faster than humanly possible. Each has `timestamp`, `durationMs`, and `humanMinimumMs` | | `syntheticEventRatio` | `number` | Ratio of events with `isTrusted: false` (0–1) | | `mouseActivityDuringTyping` | `object` | `periodsWithMouseActivity` (ratio 0–1) and `longestGapWithoutMouse` (ms) | | `zeroLatencyEventCount` | `number` | Events with zero milliseconds between them | | `unmatchedKeydownCount` | `number` | Keydowns without a corresponding keyup | Example indicators: * *“All keystroke timing is physically plausible”* * *“42% of events are synthetic (untrusted)”* * *“83 keydown events without matching keyup detected”* ### Revision Behavior [Section titled “Revision Behavior”](#revision-behavior) How the author corrected and revised their work. | Metric | Type | Description | | -------------------------- | ------------ | ---------------------------------------------------------------------------------------------------------------------------- | | `correctionRate` | `number` | Ratio of correction keystrokes to total keystrokes (0–1) | | `correctionCount` | `number` | Total correction keystrokes | | `navigationCount` | `number` | Arrow key navigation events | | `undoRedoCount` | `number` | Undo/redo operations | | `editPatterns` | `array` | Categorized edit sequences (`select-delete-type`, `select-paste`, `paste-only`, `select-edit`, `multi-step`) with counts | | `correctionTimeline` | `TimeSeries` | Correction rate over time | | `revisionDepth` | `object` | Max/average depth and count of regions edited multiple times | | `proportionBehindFrontier` | `number` | Ratio of keydowns behind the text frontier (0–1). Higher values indicate revision/composition vs linear transcription | | `substitutedWordsCount` | `number` | Count of word-level select-then-replace events (selection >= 2 chars followed by typing or paste) | | `jumpToEditFrequency` | `number` | Ratio of non-local cursor jumps (>= 5 chars) that precede an edit action (0–1) | | `jumpDistanceSd` | `number` | Standard deviation of cursor travel distances during non-local jumps. High = structural revision; low = clustered typo fixes | | `revisionAtPriorRatio` | `number` | Ratio of corrections at a prior position (behind the frontier) to total corrections (0–1) | | `revisionAtFrontierRatio` | `number` | Ratio of corrections at the point of inscription (at the frontier) to total corrections (0–1) | Example indicators: * *“Normal correction rate (4.2%)”* * *“Zero corrections across session”* * *“Very low correction rate (0.3%)“* ### Temporal Patterns [Section titled “Temporal Patterns”](#temporal-patterns) Typing speed changes, fatigue, warmup, and burst patterns over the session. | Metric | Type | Description | | ------------------- | ------------ | ------------------------------------------------------------------------------- | | `sessionDurationMs` | `number` | Total session length | | `speedTimeline` | `TimeSeries` | Typing speed over time | | `fatigueRatio` | `number` | Typing speed trend across the session (< 1 = slowing down) | | `warmupRatio` | `number` | Initial typing speed relative to overall (< 1 = started slow) | | `pauseDistribution` | `object` | Histogram, count, and mean duration of pauses | | `burstPattern` | `object` | Average burst length, speed, ratio of burst typing to total, duration SD and CV | Example indicators: * *“Temporal pattern within normal range”* * *“Natural warmup and fatigue pattern detected”* * *“No speed variation over 8-minute session”* ## Session Timeline [Section titled “Session Timeline”](#session-timeline) The analysis includes a segmented timeline classifying each period of the session: ```typescript analysis.sessionTimeline.forEach((segment) => { console.log(`${segment.type}: ${segment.startTime}ms – ${segment.endTime}ms`); }); // typing: 0ms – 3200ms // pause: 3200ms – 5100ms // typing: 5100ms – 8400ms // paste: 8400ms – 8420ms // typing: 8420ms – 12000ms ``` Segment types: `typing`, `paste`, `pause`, `tabAway`, `autocomplete`, `navigating`. ## WASM Loading [Section titled “WASM Loading”](#wasm-loading) The WASM binary loads lazily on the first `getAnalysis()` call and is cached for the lifetime of the page. Path resolution is automatic — in the browser, the binary is resolved relative to the bundled JS; in Node.js, it’s resolved relative to the installed package. You don’t need to configure anything. You can check readiness with the `wasmReady` property: ```typescript console.log(tracker.wasmReady); // false (not loaded yet) await tracker.getAnalysis(); console.log(tracker.wasmReady); // true (loaded and cached) ``` If you need to serve the WASM binary from a non-standard location (e.g., a separate CDN domain), pass `wasmUrl`: ```typescript const tracker = new WriteTrack({ target: textarea, wasmUrl: '/assets/writetrack.wasm', }); ``` Note The WASM module is cached globally after the first load. If you create multiple tracker instances, only the first instance’s `wasmUrl` takes effect. Caution If the WASM module fails to load (network error, unsupported environment), `getAnalysis()` returns `null` and `wasmReady` remains `false`. Your application should handle this gracefully. ## Next Steps [Section titled “Next Steps”](#next-steps) * [Verification](/docs/verification/) — Verify that analysis output is authentic and untampered * [API Reference](/docs/api-reference/) — Full method signatures and type definitions * [Output Sinks](/docs/output-sinks/) — Route session data to your backend * [Privacy & Security](/docs/privacy/) — What WriteTrack collects and doesn’t collect # Analysis Charts > Visualizing WriteTrack Session Analysis Analysis charts consume a [`SessionAnalysis`](/docs/api-reference/#sessionanalysis) object from `getAnalysis()`. They require the WASM analysis pipeline. ## Usage [Section titled “Usage”](#usage) All analysis charts follow the same pattern — import, register once, then pass analysis data: ```typescript import { SpeedTimeline } from 'writetrack/viz'; SpeedTimeline.register(); // call once per component const analysis = await tracker.getAnalysis(); document.querySelector('wt-speed-timeline').setData(analysis); ``` ## Speed Timeline `` [Section titled “Speed Timeline \”](#speed-timeline-wt-speed-timeline) Line + area chart showing typing speed (CPM) over time from `temporalPatterns.metrics.speedTimeline`. Recommended size: `width: 100%; height: 200px`. ## Rhythm Heatmap `` [Section titled “Rhythm Heatmap \”](#rhythm-heatmap-wt-rhythm-heatmap) 2D binned heatmap of dwell time vs flight time from `timingAuthenticity.metrics.timingOverTime`. Reveals typing style clusters — touch typists show a tight cluster; hunt-and-peck typists show scattered points. Recommended size: `350px × 350px`. ## Pause Distribution `` [Section titled “Pause Distribution \”](#pause-distribution-wt-pause-distribution) Histogram of pause durations from `temporalPatterns.metrics.pauseDistribution`. Uses pre-binned data from the analysis pipeline. Recommended size: `width: 100%; height: 200px`. # API Reference > Complete WriteTrack SDK API documentation Complete reference for all public exports from the WriteTrack SDK. ## Import Paths [Section titled “Import Paths”](#import-paths) WriteTrack provides multiple entry points via `package.json` exports: #### Core [Section titled “Core”](#core) | Import Path | Format | Description | | -------------------- | ----------------- | ---------------------------------------------------------- | | `writetrack` | ESM, CJS, Browser | Core SDK — WriteTrack class and types | | `writetrack/pipes` | ESM, CJS, Browser | Output sink factories (also re-exported from `writetrack`) | | `writetrack/browser` | ESM | Browser bundle (same as core, explicit browser entry) | #### Frameworks [Section titled “Frameworks”](#frameworks) | Import Path | Format | Description | | ------------------ | ------ | -------------------------------- | | `writetrack/react` | ESM | React hook — `useWriteTrack` | | `writetrack/vue` | ESM | Vue composable — `useWriteTrack` | #### Rich-Text Editors [Section titled “Rich-Text Editors”](#rich-text-editors) | Import Path | Format | Description | | ------------------------ | ------ | ----------------------------------------------- | | `writetrack/tiptap` | ESM | TipTap extension — `WriteTrackExtension` | | `writetrack/ckeditor` | ESM | CKEditor 5 plugin — `WriteTrackPlugin` | | `writetrack/prosemirror` | ESM | ProseMirror plugin — `WriteTrackPlugin` | | `writetrack/quill` | ESM | Quill module — `WriteTrackModule` | | `writetrack/lexical` | ESM | Lexical integration — `createWriteTrackLexical` | | `writetrack/slate` | ESM | Slate integration — `createWriteTrackSlate` | | `writetrack/tinymce` | ESM | TinyMCE integration — `createWriteTrackTinyMCE` | #### Utilities [Section titled “Utilities”](#utilities) | Import Path | Format | Description | | ------------------- | ------ | ---------------------------------- | | `writetrack/verify` | ESM | Server-side signature verification | | `writetrack/viz` | ESM | Session visualization components | ### ESM (recommended) [Section titled “ESM (recommended)”](#esm-recommended) ```typescript import { WriteTrack, webhook, datadog, segment, opentelemetry, analyzeEvents, formatIndicator, } from 'writetrack'; 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 { createWriteTrackTinyMCE } from 'writetrack/tinymce'; import { verifyAnalysisSignatureAsync } from 'writetrack/verify'; import { WtScorecard } from 'writetrack/viz'; ``` ### CommonJS [Section titled “CommonJS”](#commonjs) ```javascript const { WriteTrack, webhook } = require('writetrack'); ``` ### Script Tag [Section titled “Script Tag”](#script-tag) ```html ``` ### Types [Section titled “Types”](#types) All types are exported from their respective entry points: ```typescript import type { WriteTrackOptions, WriteTrackDataSchema, SessionAnalysis, SessionReport, IndicatorOutput, KeystrokeEvent, ClipboardEvent, SelectionEvent, UndoRedoEvent, ProgrammaticInsertionEvent, CompositionEvent, InputSource, WriteTrackSink, WebhookOptions, DatadogOptions, SegmentOptions, OpenTelemetryOptions, } from 'writetrack'; ``` ## WriteTrack Class [Section titled “WriteTrack Class”](#writetrack-class) The main class for keystroke tracking and behavioral analysis. ### Constructor [Section titled “Constructor”](#constructor) ```typescript new WriteTrack(optionsOrTarget: WriteTrackOptions | HTMLElement) ``` Creates a new WriteTrack instance. Accepts either a full options object or an HTMLElement directly for localhost evaluation. | Parameter | Type | Description | | ----------------- | ---------------------------------- | --------------------------------------- | | `optionsOrTarget` | `WriteTrackOptions \| HTMLElement` | Configuration options or target element | ```typescript // Full options with context const tracker = new WriteTrack({ target: document.querySelector('#response-field'), license: 'wt_live_...', userId: 'u_abc123', contentId: 'post_draft_42', metadata: { formName: 'signup' }, }); // Shorthand (localhost evaluation) const tracker = new WriteTrack(document.querySelector('#response-field')); ``` ### start() [Section titled “start()”](#start) ```typescript start(): void ``` Begins recording keystroke events. Clears any previously captured data. * Resets all event arrays (keystrokes, clipboard, selection, undo/redo, programmatic insertion, composition) * Records session start time * Enables event capture ```typescript const tracker = new WriteTrack({ target: textarea }); tracker.start(); // Now recording // User types... const events = tracker.getRawEvents(); ``` Note Calling `start()` resets all captured data, including keystrokes, clipboard events, and selections. The session timer also resets to zero. ### stop() [Section titled “stop()”](#stop) ```typescript stop(): void ``` Stops recording keystrokes. * Disables event capture * Cleans up selection polling interval and mutation observer * Data remains available after stopping ```typescript tracker.stop(); // Data still accessible const events = tracker.getRawEvents(); ``` Tip After calling `stop()`, all captured data remains accessible. You can retrieve events and features even after stopping the tracker. ### getData() [Section titled “getData()”](#getdata) ```typescript getData(): WriteTrackDataSchema ``` Returns the complete session as a structured [`WriteTrackDataSchema`](#writetrackdataschema) object, including metadata, all captured events, and computed quality metrics. ```typescript tracker.stop(); const data = tracker.getData(); // { // version: "2.1.0", // metadata: { tool, targetElement, timestamp, duration }, // session: { events, clipboardEvents, selectionEvents, compositionEvents, ... }, // quality: { overallScore, sequenceValid, qualityLevel, ... }, // } // Send to your server await fetch('/api/submit', { method: 'POST', body: JSON.stringify(data), }); ``` ### getText() [Section titled “getText()”](#gettext) ```typescript getText(): string ``` Returns the current text content of the target element. Reads `value` for input/textarea elements, or `textContent` for other elements. ### getRawEvents() [Section titled “getRawEvents()”](#getrawevents) ```typescript getRawEvents(): KeystrokeEvent[] ``` Returns a copy of all captured keystroke events. ```typescript const events = tracker.getRawEvents(); console.log(`Captured ${events.length} keystrokes`); events .filter((e) => e.type === 'keydown') .forEach((e) => { console.log(`${e.key} at ${e.timestamp}ms, flight: ${e.flightTime}ms`); }); ``` ### getClipboardEvents() [Section titled “getClipboardEvents()”](#getclipboardevents) ```typescript getClipboardEvents(): ClipboardEvent[] ``` Returns a copy of all clipboard events (copy, paste, cut). ```typescript const pastes = tracker.getClipboardEvents().filter((e) => e.type === 'paste'); console.log(`User pasted ${pastes.length} times`); pastes.forEach((e) => { console.log(`Pasted ${e.length} chars at position ${e.position}`); }); ``` ### getSelectionEvents() [Section titled “getSelectionEvents()”](#getselectionevents) ```typescript getSelectionEvents(): SelectionEvent[] ``` Returns text selection events. ```typescript const selections = tracker.getSelectionEvents(); selections.forEach((e) => { console.log(`Selected ${e.selectedLength} chars via ${e.method}`); }); ``` ### getUndoRedoEvents() [Section titled “getUndoRedoEvents()”](#getundoredoevents) ```typescript getUndoRedoEvents(): UndoRedoEvent[] ``` Returns undo/redo operation events. ### getProgrammaticInsertionEvents() [Section titled “getProgrammaticInsertionEvents()”](#getprogrammaticinsertionevents) ```typescript getProgrammaticInsertionEvents(): ProgrammaticInsertionEvent[] ``` Returns detected programmatic insertion events. These are emitted when text appears in the input without corresponding keystrokes, indicating browser autocomplete, autofill, predictive text, AI extension injection, voice dictation, or programmatic value assignment. ```typescript const insertions = tracker.getProgrammaticInsertionEvents(); insertions.forEach((e) => { console.log( `Programmatic insertion: "${e.insertedText}" (${e.insertedLength} chars)` ); }); ``` ### getCompositionEvents() [Section titled “getCompositionEvents()”](#getcompositionevents) ```typescript getCompositionEvents(): CompositionEvent[] ``` Returns IME composition events captured during the session. A composition event is emitted when a CJK (Chinese, Japanese, Korean) input method completes a composition sequence. Intermediate keystrokes during composition are suppressed. ```typescript const compositions = tracker.getCompositionEvents(); compositions.forEach((e) => { console.log(`Composition: ${e.insertedLength} chars in ${e.duration}ms`); }); ``` ### getSessionDuration() [Section titled “getSessionDuration()”](#getsessionduration) ```typescript getSessionDuration(): number ``` Returns the total session duration in milliseconds since `start()` was called, including time when the tab was hidden. ```typescript const duration = tracker.getSessionDuration(); console.log(`Session: ${(duration / 1000).toFixed(1)} seconds`); ``` ### getActiveTime() [Section titled “getActiveTime()”](#getactivetime) ```typescript getActiveTime(): number ``` Returns the active (visible) session time in milliseconds. This subtracts time spent with the tab hidden from the total session duration. Returns `0` before `start()` is called. ```typescript const active = tracker.getActiveTime(); const total = tracker.getSessionDuration(); console.log( `Active: ${(active / 1000).toFixed(1)}s of ${(total / 1000).toFixed(1)}s total` ); ``` ### getKeystrokeCount() [Section titled “getKeystrokeCount()”](#getkeystrokecount) ```typescript getKeystrokeCount(): number ``` Returns the total number of keydown events captured. ### pipe() [Section titled “pipe()”](#pipe) ```typescript pipe(sink: WriteTrackSink): this ``` Registers an output sink to receive session data when `getData()` is called. Returns `this` for chaining. ```typescript import { WriteTrack, webhook } from 'writetrack'; tracker .pipe(webhook({ url: 'https://api.example.com/typing' })) .pipe({ send: async (data) => console.log(data) }); ``` See [Output Sinks](/docs/output-sinks/) for full documentation of the pipe system and available sinks. ### on() [Section titled “on()”](#on) ```typescript on(event: string, handler: (...args: unknown[]) => void): this ``` Registers an event listener. **Supported events:** | Event | Handler signature | Description | | ------------ | ----------------------------------------------------------- | ------------------------------------------------------------------------- | | `pipe:error` | `(err: Error, sink: WriteTrackSink) => void` | Fires when a sink fails | | `tick` | `(data: { activeTime: number; totalTime: number }) => void` | Fires every \~1 second while the session is active and the tab is visible | ```typescript tracker.on('pipe:error', (err, sink) => { console.error('Sink failed:', err); }); // Display a live session timer tracker.on('tick', ({ activeTime, totalTime }) => { const seconds = Math.floor(activeTime / 1000); console.log(`Active: ${seconds}s`); }); ``` Note The `tick` interval starts automatically when a `tick` listener is registered (via `on('tick', ...)`) and the session is active. If listeners are registered before `start()`, the interval begins on `start()`. The interval is cleared on `stop()`. ### isLicenseValid() [Section titled “isLicenseValid()”](#islicensevalid) ```typescript isLicenseValid(): boolean ``` Returns whether a valid, non-expired license key has been verified for the current domain. Returns `false` when no license key is provided, during the grace period, or when validation fails. Recording (event capture) works regardless of license status — only `getAnalysis()` requires a valid license, grace period, or localhost. ### isLicenseValidated() [Section titled “isLicenseValidated()”](#islicensevalidated) ```typescript isLicenseValidated(): boolean ``` Returns whether license validation has been performed. ```typescript if (tracker.isLicenseValidated() && tracker.isLicenseValid()) { console.log('License valid, ready to use'); } ``` ### isTargetDetached() [Section titled “isTargetDetached()”](#istargetdetached) ```typescript isTargetDetached(): boolean ``` Returns whether the monitored target element has been removed from the DOM. Useful for detecting SPA navigation that destroys the tracked element. ```typescript if (tracker.isTargetDetached()) { console.warn('Target element removed — call stop() and create a new tracker'); } ``` ### ready [Section titled “ready”](#ready) ```typescript ready: Promise; ``` A promise that resolves when the tracker is ready for use. When `persist: true`, this resolves after IndexedDB loads any prior session data. When `persist: false`, it resolves immediately. After `stop()` with persistence enabled, `ready` resolves when the save completes. ```typescript const tracker = new WriteTrack({ target: textarea, contentId: 'doc-1', persist: true, }); await tracker.ready; // IndexedDB loaded tracker.start(); // Will auto-resume prior session if available ``` ### clearPersistedData() [Section titled “clearPersistedData()”](#clearpersisteddata) ```typescript async clearPersistedData(): Promise ``` Removes any persisted session data from IndexedDB for this field. No-op when `persist` is not enabled. Use this to reset a field’s session history. ```typescript await tracker.clearPersistedData(); ``` ### getAnalysis() [Section titled “getAnalysis()”](#getanalysis) ```typescript async getAnalysis(): Promise ``` Runs WASM-powered analysis on the captured session data. The WASM binary is lazy-loaded on the first call and cached for subsequent calls. Returns `null` if the WASM module fails to load. ```typescript const tracker = new WriteTrack({ target: textarea, wasmUrl: '/writetrack.wasm', }); tracker.start(); // ... user types ... tracker.stop(); const analysis = await tracker.getAnalysis(); // { // version: "0.9.1", // sufficientData: true, // contentOrigin: { ... }, // timingAuthenticity: { ... }, // ... // } ``` Note All WASM imports are dynamic — if `getAnalysis()` is never called, zero analysis code is loaded. Capture-only consumers pay no bundle cost. ### getSessionReport() [Section titled “getSessionReport()”](#getsessionreport) ```typescript async getSessionReport(): Promise ``` Returns both the raw capture data and the WASM analysis in a single call. ```typescript const report = await tracker.getSessionReport(); // { // data: WriteTrackDataSchema, // Same as getData() // analysis: SessionAnalysis, // Same as getAnalysis() // } await fetch('/api/submit', { method: 'POST', body: JSON.stringify(report), }); ``` ### wasmReady [Section titled “wasmReady”](#wasmready) ```typescript get wasmReady(): boolean ``` Whether the WASM module has been loaded and is ready for analysis. Returns `false` until `getAnalysis()` is called for the first time and the WASM binary loads successfully. ```typescript const tracker = new WriteTrack({ target: textarea }); console.log(tracker.wasmReady); // false await tracker.getAnalysis(); console.log(tracker.wasmReady); // true (if WASM loaded) ``` ### Static: listPersistedSessions() [Section titled “Static: listPersistedSessions()”](#static-listpersistedsessions) ```typescript static async listPersistedSessions(): Promise ``` Returns metadata for all persisted sessions stored in IndexedDB. Useful for multi-document apps that need to display or manage saved writing sessions. ```typescript const sessions = await WriteTrack.listPersistedSessions(); // [{ contentId: 'doc_1', fieldId: 'default', keystrokeCount: 42, lastSavedAt: 1709..., sessionId: '...' }] ``` ### Static: deletePersistedSession() [Section titled “Static: deletePersistedSession()”](#static-deletepersistedsession) ```typescript static async deletePersistedSession(contentId: string, fieldId?: string): Promise ``` Deletes persisted session data from IndexedDB. If `fieldId` is omitted, deletes all sessions for the given `contentId`. ```typescript // Delete all sessions for a document await WriteTrack.deletePersistedSession('doc_1'); // Delete a specific field's session await WriteTrack.deletePersistedSession('doc_1', 'response'); ``` ## Pipes Exports [Section titled “Pipes Exports”](#pipes-exports) ```typescript import { webhook, datadog, segment, opentelemetry } from 'writetrack'; ``` | Export | Returns | Description | | ------------------------ | ---------------- | --------------------------------- | | `webhook(options)` | `WriteTrackSink` | POST session data to a URL | | `datadog(options)` | `WriteTrackSink` | Send as Datadog RUM custom action | | `segment(options)` | `WriteTrackSink` | Send as Segment track event | | `opentelemetry(options)` | `WriteTrackSink` | Send as OpenTelemetry span | See [Output Sinks](/docs/output-sinks/) for configuration options and usage guides. ## Standalone Exports [Section titled “Standalone Exports”](#standalone-exports) ### analyzeEvents() [Section titled “analyzeEvents()”](#analyzeevents) ```typescript import { analyzeEvents } from 'writetrack'; async function analyzeEvents( rawEventData: unknown, options?: { wasmUrl?: string; licenseKey?: string } ): Promise; ``` Analyze a `WriteTrackDataSchema` object without needing a live DOM element. Useful for server-side analysis of previously captured session data. ```typescript // Server-side: analyze data received from the client const data = await request.json(); // WriteTrackDataSchema from getData() const analysis = await analyzeEvents(data, { licenseKey: process.env.WRITETRACK_LICENSE_KEY, }); ``` ### formatIndicator() [Section titled “formatIndicator()”](#formatindicator) ```typescript import { formatIndicator } from 'writetrack'; function formatIndicator(indicator: IndicatorOutput): string; ``` Converts a machine-readable WT-NNN indicator code (from `SessionAnalysis`) into a human-readable English string. ```typescript const analysis = await tracker.getAnalysis(); if (analysis) { const msg = formatIndicator(analysis.contentOrigin.indicator); // e.g., "85% of text was pasted from external sources" } ``` ### createSessionReport() [Section titled “createSessionReport()”](#createsessionreport) ```typescript import { createSessionReport } from 'writetrack'; async function createSessionReport( rawData: unknown, options?: { wasmUrl?: string; licenseKey?: string } ): Promise; ``` Analyze raw session data and return a complete `SessionReport` (data + analysis). Wraps `analyzeEvents()` — accepts `WriteTrackDataSchema` (from `getData()`) or raw typing-sample format (with `rawEvents` instead of `session`). The returned report is ready for `scorecard.setData()`. ```typescript const data = await request.json(); const report = await createSessionReport(data, { licenseKey: process.env.WRITETRACK_LICENSE_KEY, }); // report.data — normalized input data // report.analysis — SessionAnalysis result ``` ## Types [Section titled “Types”](#types-1) ### WriteTrackOptions [Section titled “WriteTrackOptions”](#writetrackoptions) Configuration options for the WriteTrack constructor. ```typescript interface WriteTrackOptions { target: HTMLElement; // Required: Input element to monitor license?: string; // Optional: License key for production userId?: string; // Optional: User identifier (included in metadata) contentId?: string; // Optional: Content/document identifier (included in metadata) metadata?: Record; // Optional: Arbitrary tags (appears as metadata.custom) wasmUrl?: string; // Optional: URL to WASM binary for analysis persist?: boolean; // Optional: Enable IndexedDB session persistence (requires contentId) cursorPositionProvider?: () => number; // Optional: Custom cursor position for rich-text editors inputSourceProvider?: () => InputSource | undefined; // Optional: Callback returning current input source classification. Set automatically by editor integrations. } ``` ### PersistedSessionInfo [Section titled “PersistedSessionInfo”](#persistedsessioninfo) Metadata about a persisted writing session, returned by `listPersistedSessions()`. ```typescript interface PersistedSessionInfo { contentId: string; // Document/content identifier fieldId: string; // Field identifier within the document keystrokeCount: number; // Number of keystrokes in the persisted session lastSavedAt: number; // Unix timestamp of last save sessionId: string; // Unique session ID } ``` ### WriteTrackDataSchema [Section titled “WriteTrackDataSchema”](#writetrackdataschema) The complete session output returned by `getData()`. ```typescript interface WriteTrackDataSchema { version: '2.1.0'; metadata: SessionMetadata; session: RawEventData; quality: DataQualityMetrics; unlicensed?: boolean; } ``` ### SessionMetadata [Section titled “SessionMetadata”](#sessionmetadata) Session metadata included in the export. ```typescript interface SessionMetadata { tool: { name: string; version: string }; targetElement: string; // e.g., 'textarea', 'input' fieldId: string; // Derived from data-writetrack-field, id, or name attribute sessionId: string; // Unique session ID (stable across resume) segment?: number; // Resume segment counter (present when session was resumed) timestamp: string; // ISO 8601 duration: number; // ms userId?: string; // From WriteTrackOptions.userId contentId?: string; // From WriteTrackOptions.contentId custom?: Record; // From WriteTrackOptions.metadata } ``` ### DataQualityMetrics [Section titled “DataQualityMetrics”](#dataqualitymetrics) Quality assessment of the captured session. ```typescript interface DataQualityMetrics { overallScore: number; // 0-1 completeness: number; // 0-1 sequenceValid: boolean; // Timestamps monotonically increasing timingValid: boolean; // Session has valid timing data sessionDuration: number; // ms eventCount: number; qualityLevel: 'POOR' | 'FAIR' | 'GOOD' | 'EXCELLENT'; issues: string[]; // Human-readable quality issues validatedAt: string; // ISO 8601 } ``` ### KeystrokeEvent [Section titled “KeystrokeEvent”](#keystrokeevent) Individual keystroke event data. ```typescript interface KeystrokeEvent { key: string; // Character or key name code: string; // Physical key code type: 'keydown' | 'keyup'; timestamp: number; // High-precision timestamp dwellTime?: number; // Key hold duration (keyup only) flightTime?: number; // Time since last key (keydown only) isCorrection?: boolean; // Backspace/Delete/ArrowLeft/ArrowRight windowFocused: boolean; timeSinceLastMouse: number; isTrusted?: boolean; // true if user-generated, false if scripted inputSource?: InputSource; // Input source classification, set by editor integrations or beforeinput fallback isBurst?: boolean; // Schema field, populated by analysis pipelines cursorPosition?: number; // Schema field, populated by analysis pipelines modifiers?: ModifierState; // Schema field, populated by analysis pipelines } ``` Note `isBurst`, `cursorPosition`, and `modifiers` are defined in the schema type but not populated by the core `WriteTrack` class during capture. They may be set by external analysis pipelines or WASM processing. ### ClipboardEvent [Section titled “ClipboardEvent”](#clipboardevent) Clipboard operation event data. ```typescript interface ClipboardEvent { type: 'copy' | 'paste' | 'cut'; timestamp: number; position: number; length: number; shortcut: string; // e.g., 'Ctrl+V', 'Cmd+V' content?: string; // Clipboard content (paste only) beforeText?: string; // Text state before operation afterText?: string; // Text state after operation replacedText?: string; // Text that was replaced replacedRange?: { start: number; end: number; }; isTrusted?: boolean; // true if user-generated, false if scripted inputSource?: InputSource; // Input source classification, set by editor integrations or beforeinput fallback } ``` ### SelectionEvent [Section titled “SelectionEvent”](#selectionevent) Text selection event data. ```typescript interface SelectionEvent { type: 'select'; timestamp: number; startPosition: number; endPosition: number; selectedLength: number; selectedText?: string; // The selected text content (omitted when privacy-sensitive) method: 'mouse' | 'keyboard' | 'programmatic'; isTrusted?: boolean; // true if user-generated, false if scripted } ``` ### ProgrammaticInsertionEvent [Section titled “ProgrammaticInsertionEvent”](#programmaticinsertionevent) Programmatic insertion event data. ```typescript interface ProgrammaticInsertionEvent { type: 'programmatic-insertion'; timestamp: number; insertedText: string; // Text that was inserted insertedLength: number; // Character count position: number; // Cursor position where text was inserted isTrusted: boolean; // Whether the triggering input event was user-generated inputType?: string; // InputEvent.inputType if available inputSource?: InputSource; // Input source classification, set by editor integrations or beforeinput fallback } ``` ### UndoRedoEvent [Section titled “UndoRedoEvent”](#undoredoevent) Undo/redo operation event data. ```typescript interface UndoRedoEvent { type: 'undo' | 'redo'; timestamp: number; shortcut: string; // e.g., 'Ctrl+Z' beforeText: string; afterText: string; } ``` ### CompositionEvent [Section titled “CompositionEvent”](#compositionevent) IME composition event data. Emitted when a CJK input method or dead-key sequence completes. ```typescript interface CompositionEvent { timestamp: number; // When the composition started duration: number; // Duration of the composition sequence (ms) insertedLength: number; // Characters inserted (0 if composition was abandoned) isTrusted: boolean; // Whether the compositionend event was user-generated } ``` ### SessionAnalysis [Section titled “SessionAnalysis”](#sessionanalysis) The WASM analysis output returned by `getAnalysis()`. Contains indicator codes and rich metrics for each analysis dimension. ```typescript interface SessionAnalysis { version: string; locale: string; analyzedAt: number; sufficientData: boolean; keydownCount: number; integrity: IntegrityProof; sessionTimeline: SessionSegment[]; initialTextLength: number; initialTextHash: string; finalTextLength: number; finalTextHash: string; contentOrigin: ContentOriginAnalysis; timingAuthenticity: TimingAuthenticityAnalysis; sessionContinuity: SessionContinuityAnalysis; physicalPlausibility: PhysicalPlausibilityAnalysis; revisionBehavior: RevisionBehaviorAnalysis; temporalPatterns: TemporalPatternsAnalysis; writingProcess: WritingProcessAnalysis; outputSignature: string; signedPayload?: string; } ``` Each analysis dimension (e.g., `contentOrigin`, `timingAuthenticity`) contains an `indicator` with a machine-readable `code` (WT-100 through WT-606) and numeric `params`, plus a `metrics` object with detailed data and time series suitable for visualization. Content origin additionally has a `pasteReworkIndicator` (WT-105 through WT-107) for paste editing behavior. The `outputSignature` field contains a cryptographic signature over the analysis output, verifiable via `verifyAnalysisSignatureAsync()` from `writetrack/verify`. The optional `signedPayload` field contains the exact JSON bytes that were signed, for cross-language verification. ### WritingProcessAnalysis [Section titled “WritingProcessAnalysis”](#writingprocessanalysis) Writing process stage classification returned in `SessionAnalysis.writingProcess`. ```typescript interface WritingProcessAnalysis { indicator: IndicatorOutput; metrics: { segments: WritingProcessSegment[]; transitions: PhaseTransition[]; timeInPhase: { planning: number; // Fraction of session time (0-1) drafting: number; revision: number; }; phaseChangeCount: number; windowDurationMs: number; phaseTimeline: TimeSeries; }; } interface WritingProcessSegment { startTime: number; // ms from session start endTime: number; phase: 'planning' | 'drafting' | 'revision'; confidence: number; // 0-1 features: WindowFeatures; } interface PhaseTransition { from: 'planning' | 'drafting' | 'revision'; to: 'planning' | 'drafting' | 'revision'; count: number; probability: number; // 0-1 } ``` ### SessionReport [Section titled “SessionReport”](#sessionreport) Combined output of `getSessionReport()`. ```typescript interface SessionReport { data: WriteTrackDataSchema; analysis: SessionAnalysis | null; } ``` ### IndicatorOutput [Section titled “IndicatorOutput”](#indicatoroutput) Machine-readable indicator returned by each analysis dimension. ```typescript interface IndicatorOutput { code: string; // WT-NNN code (e.g., "WT-101") params: Record; // Numeric parameters (e.g., { pct: 85 }) } ``` Use `formatIndicator()` to convert to a human-readable English string. # Changelog > WriteTrack SDK release notes and version history ## v0.10.1 Current [Section titled “v0.10.1 ”](#v0101) Scorecard empty-state polish and writetracker UX improvements. ### Bug Fixes [Section titled “Bug Fixes”](#bug-fixes) * Scorecard cells and detail panel no longer show “flagged” status when session has insufficient data * Empty compact charts no longer display placeholder text * Fixed scorecard flag section appearing on empty sessions *** ## v0.10.0 [Section titled “v0.10.0”](#v0100) Redesigned scorecard with new chart components, share/download actions, and improved empty-state handling. ### Redesigned Scorecard New [Section titled “Redesigned Scorecard ”](#redesigned-scorecard) * New 6-cell grid with compact chart previews for each analysis category * Content origin bar showing typed vs pasted proportion at a glance * Share and download action buttons with PNG export * **``** — per-keystroke edit visualization * **``** — headline metric badges for key session stats ### Improvements [Section titled “Improvements”](#improvements) * More responsive chart rendering * Improved license validation error messages * Reduced false positives for paste-heavy sessions ### Bug Fixes [Section titled “Bug Fixes”](#bug-fixes-1) * Fixed paste source labeling in scorecard * Fixed indicator formatting and priority ordering *** ## v0.9.1 [Section titled “v0.9.1”](#v091) Detection improvements and bug fixes. ### Detection [Section titled “Detection”](#detection) * Enhanced modifier key tracking for automation detection * Three new indicators targeting uniform timing, headless execution, and synthetic input patterns * Reduced false positives for autocomplete and paste-heavy sessions * Richer burst analysis diagnostics ### Bug Fixes [Section titled “Bug Fixes”](#bug-fixes-2) * Fixed paste event capture in nested contenteditable elements * Fixed stale timestamps on persisted session restore * Fixed `analysis.version` reporting hardcoded `1.0.0` instead of SDK version * Fixed persistence race condition on async session restore * Fixed chart rendering artifacts on speed timeline *** ## v0.9.0 [Section titled “v0.9.0”](#v090) Better editor integration, active session tracking, and improved paste-edit heuristics. ### Editor Input Source Classification New [Section titled “Editor Input Source Classification ”](#editor-input-source-classification) All editor integrations now classify the source of each input event — `keyboard`, `paste`, `drop`, `cut`, `autocomplete`, `spellcheck`, `composition`, `undo`, `redo`, or `api`. Uses editor transaction metadata with W3C `beforeinput` fallback for plain elements. New `inputSourceProvider` option for custom integrations. ### Active Session Duration New [Section titled “Active Session Duration ”](#active-session-duration) Track how long a user has been actively writing without rolling your own timer. `on('tick', handler)` fires every second while the session is active and the tab is visible, providing `activeTime` and `totalTime` in milliseconds. `getActiveTime()` returns active time on demand. ### Paste-Edit Analysis [Section titled “Paste-Edit Analysis”](#paste-edit-analysis) * Per-paste `reworkRatio` metric tracking edits within paste regions over a 60-second window * 6 aggregate paste metrics in `PasteEditSummary` * New WT-105/106/107 paste rework indicators *** ## v0.8.0 [Section titled “v0.8.0”](#v080) ### Writing Process Stage Detection New [Section titled “Writing Process Stage Detection ”](#writing-process-stage-detection) New analysis category that classifies temporal windows as planning, drafting, or revision using existing keystroke metrics. Outputs phase transition matrix for flow visualization, aggregate time-in-phase percentages, and per-segment phase labels with confidence scores. ### Editor Integrations New [Section titled “Editor Integrations ”](#editor-integrations) Three new first-party editor integrations: * **Lexical** (`writetrack/lexical`) — cursor tracking via `$getSelection()` * **Slate** (`writetrack/slate`) — element-to-path cursor tracking * **TinyMCE** (`writetrack/tinymce`) — inline mode with init/destroy lifecycle ### Analysis & Bug Fixes [Section titled “Analysis & Bug Fixes”](#analysis--bug-fixes) * Paste-edit correlation metrics — track edits within paste regions * Fixed false Physical Plausibility flags caused by dwell time encoding bugs * Improved burst ratio and timing threshold accuracy * Zero-latency events now flagged as physically implausible * Documentation hardening and stale reference cleanup *** ## v0.7.0 [Section titled “v0.7.0”](#v070) This release adds a visualization module with 9 chart components, a redesigned session scorecard, and improvements across the analysis pipeline. ### Visualization Module New [Section titled “Visualization Module ”](#visualization-module) New `writetrack/viz` subpath export with chart components rendered as web components powered by Observable Plot. * **Analysis charts** — SpeedTimeline, CompositionTimeline, RhythmHeatmap, PauseDistribution * **Event-detail charts** — Sparkline, DocumentGrowth, DocumentRibbon, EditWaterfall, CorrectionsBubble * **Session scorecard** — Experimental `` implementation with indicator grid, detail panels, and integrity verification * **Theming** — CSS custom properties with warm editorial palette (light/dark), overridable fonts via `--wt-scorecard-font-*` * **Extract functions** — Standalone data-extraction functions for each chart, usable without web components * **Hover tooltips** — Interactive tooltips on all chart components * **LTTB downsampling** — Time-series charts automatically downsample large datasets via WASM for responsive rendering ### Analysis [Section titled “Analysis”](#analysis) * Improved analysis module with new indicators and metrics for more accurate detection of automated and authentic writing patterns ### Bug Fixes [Section titled “Bug Fixes”](#bug-fixes-3) * Fixed paste content capture in ProseMirror/TipTap editors * Fixed contenteditable cursor position tracking * Fixed WASM loading reliability across deployment environments *** ## v0.6.1 [Section titled “v0.6.1”](#v061) * **Improved bundler compatibility** — WASM analysis module works reliably across webpack 5, Rollup, Next.js, and Vite * **Bug fixes** — Improvements to the customer portal *** ## v0.6.0 [Section titled “v0.6.0”](#v060) This release improves analysis accuracy and adds convenience methods for managing persisted sessions. ### Analysis Improvements [Section titled “Analysis Improvements”](#analysis-improvements) * **New analysis features** — Enhanced timing, behavioral, and revision pattern detection for more accurate indicators * **Consistent indicator codes** — Indicators now use a category-prefixed scheme (WT-1xx through WT-6xx) ### Persistence Management New [Section titled “Persistence Management ”](#persistence-management) * **[`listPersistedSessions()`](/docs/api-reference/#static-listpersistedsessions)** — List all stored sessions without creating a tracker instance * **[`deletePersistedSession()`](/docs/api-reference/#static-deletepersistedsession)** — Delete sessions by `contentId`, useful for cleanup when users delete content ### Editor Integrations [Section titled “Editor Integrations”](#editor-integrations) * All integrations (TipTap, CKEditor, ProseMirror, Quill, React hook, Vue hook) now support `persist` and `wasmUrl` option passthrough ### Verification API New [Section titled “Verification API ”](#verification-api) New [`writetrack/verify`](/docs/api-reference/#writtrackverify) subpath export for standalone session verification without a DOM element. *** ## v0.5.0 [Section titled “v0.5.0”](#v050) The latest version of WriteTrack introduces typing pattern analysis, session persistence, and expanded input capture. ### Analysis Module New [Section titled “Analysis Module ”](#analysis-module) WriteTrack now includes an integrated analysis engine. Each indicator returns a standardized diagnostic code and human-readable explanation. Results include a signed integrity proof. * **Content origin** — typed vs. pasted vs. autocompleted ratio * **Timing authenticity** — keystroke rhythm and variability * **Session continuity** — tab-away events and behavioral shifts * **Physical plausibility** — input consistency checks * **Revision behavior** — correction rate and editing patterns * **Temporal patterns** — speed variation, fatigue, and burst detection ### Session Persistence New [Section titled “Session Persistence ”](#session-persistence) Sessions can now survive page reloads. Pass `persist: true` with a `contentId` to automatically save session state to IndexedDB and restore it on the next page load. New `clearPersistedData()` method for cleanup. ### Capture Improvements [Section titled “Capture Improvements”](#capture-improvements) * **IME composition support** — [CJK](https://en.wikipedia.org/wiki/CJK_characters) input methods, dead keys, and accent input now tracked correctly * **Tab blur detection** — keystrokes arriving while the tab is backgrounded are identified * **Paste source classification** — pastes categorized by source * **Programmatic insertion** — now captures the browser’s `inputType` for finer-grained classification ### Bug Fixes [Section titled “Bug Fixes”](#bug-fixes-4) * Fixed memory leak where event listeners weren’t removed on `stop()` * Improved accuracy of behavioral analysis * Various fixes to contenteditable support and React hook ## v0.4.3 [Section titled “v0.4.3”](#v043) ### Editor Integrations New [Section titled “Editor Integrations ”](#editor-integrations--1) We’ve added support for the most popular rich-text editors via dedicated imports. Each editor library is an optional peer dependency, only included in your bundle if you use it. * **[TipTap](/docs/integrations/tiptap/)** – headless and extensible (`writetrack/tiptap`) * **[CKEditor 5](/docs/integrations/ckeditor/)** – feature-rich, used by enterprise teams (`writetrack/ckeditor`) * **[ProseMirror](/docs/integrations/prosemirror/)** – low-level toolkit for custom editors (`writetrack/prosemirror`) * **[Quill](/docs/integrations/quill/)** – lightweight and easy to get started (`writetrack/quill`) ### Documentation [Section titled “Documentation”](#documentation) * Consolidated API reference into a single [page](/docs/api-reference/) * Improved styling and clarity across all docs pages ## v0.3.1 [Section titled “v0.3.1”](#v031) * Documentation now accurately reflects the public npm package API ## v0.3.0 [Section titled “v0.3.0”](#v030) This release introduces the output sink system for routing session data to external services, alongside broader browser support and improved event capture. ### Output Sinks New [Section titled “Output Sinks ”](#output-sinks) New [`.pipe()` API](/docs/output-sinks/) for routing session data to one or more destinations. Ships with built-in sinks for [Webhook](/docs/sink-webhook/), [Datadog](/docs/sink-datadog/), [Segment](/docs/sink-segment/), and [OpenTelemetry](/docs/sink-opentelemetry/), plus support for [custom sinks](/docs/sink-custom/). ### Event Capture New [Section titled “Event Capture ”](#event-capture) * **`isTrusted` flag** — Keystroke events now include the browser’s `isTrusted` property to distinguish real user input from synthetic events * **Programmatic insertion detection** — Browser autofill, predictive text, and other non-keystroke insertions are captured as dedicated events * **Context fields** — New `userId`, `contentId`, and `metadata` constructor options that flow through to `getData()` output and all sinks ### Browser Support [Section titled “Browser Support”](#browser-support) * Broader browser support for license validation — now works in **Chrome 37+**, Firefox 34+, Safari 11+, Edge 12+ ## v0.1.0 [Section titled “v0.1.0”](#v010) Initial release of the SDK and [writetrack.dev](https://writetrack.dev). ### SDK New [Section titled “SDK ”](#sdk) * **Keystroke Capture** — Keydown/keyup events with high-resolution timestamps, dwell time, flight time, cursor position and selection tracking * **Paste & Edit Tracking** — Clipboard event capture with before/after text states, selection events via mouse/keyboard/programmatic methods, edit sequence pattern detection, revision depth tracking * **Privacy-First Architecture** — Client-side only processing, no data transmitted externally, offline license validation, no PII collection or device fingerprinting * **Framework Support** — React hook (`useWriteTrack`), Vue 3 composable, Next.js (App Router and Pages Router), vanilla JS * **Multi-Format Output** — ESM, CommonJS, and browser bundle. Zero dependencies ### Documentation New [Section titled “Documentation ”](#documentation) * **Getting Started** — Introduction, quickstart guide, configuration reference * **Framework Guides** — React, Vue, and Next.js integration walkthroughs * **Core API** — WriteTrack class reference, feature extraction guide, full TypeScript type definitions * **Advanced** — Data schema specification, privacy and security guidance * **Reference** — API quick-reference tables, testing strategies, troubleshooting guide ### Customer Portal New [Section titled “Customer Portal ”](#customer-portal) * **Authentication** — Email/password login and signup with email verification * **License Dashboard** — License tier display, expiry tracking * **Domain Management** — Add/remove licensed domains with tier-based limits # Data Schema > WriteTrack event types and output format WriteTrack captures multiple types of events during typing sessions. This page documents the complete data schema. > **Schema Version:** v2.1.0 Note Schema v2.0.0 added enhanced paste and edit tracking. v2.1.0 added `compositionEvents`. Samples created with earlier versions may lack newer fields. ## Event Types [Section titled “Event Types”](#event-types) ### Keystroke Events [Section titled “Keystroke Events”](#keystroke-events) The primary event type, capturing individual key presses and releases. ```typescript interface KeystrokeEvent { key: string; // Character or key name code: string; // Physical key code type: 'keydown' | 'keyup'; timestamp: number; // High-precision timestamp (ms) dwellTime?: number; // Key hold duration (keyup only) flightTime?: number; // Time since last key (keydown only) isCorrection?: boolean; // True for Backspace, Delete, ArrowLeft, ArrowRight windowFocused: boolean; // Window focus state timeSinceLastMouse: number; // ms since last mouse activity isTrusted?: boolean; // true if user-generated, false if scripted inputSource?: InputSource; // Input source classification, set by editor integrations or beforeinput fallback } ``` #### Dwell and Flight Time [Section titled “Dwell and Flight Time”](#dwell-and-flight-time) **Dwell time**: How long a key is held down (keydown to keyup). * Only present on `keyup` events * Typical human range: 50-200ms **Flight time**: Time between releasing the previous key and pressing this one. * Only present on `keydown` events * Typical human range: 50-500ms ### Clipboard Events [Section titled “Clipboard Events”](#clipboard-events) Captures copy, paste, and cut operations. ```typescript interface ClipboardEvent { type: 'copy' | 'paste' | 'cut'; timestamp: number; position: number; // Cursor position length: number; // Length of content shortcut: string; // e.g., 'Ctrl+V', 'Cmd+V' content?: string; // Actual content (paste only) beforeText?: string; // Text state before operation afterText?: string; // Text state after operation replacedText?: string; // Text that was replaced replacedRange?: { start: number; end: number; }; isTrusted?: boolean; // true if user-generated, false if scripted inputSource?: InputSource; // Input source classification, set by editor integrations or beforeinput fallback } ``` #### Example Paste Event [Section titled “Example Paste Event”](#example-paste-event) ```typescript { type: 'paste', timestamp: 1234567890.12, position: 42, length: 150, shortcut: 'Cmd+V', content: 'The pasted text content...', beforeText: 'Some existing text', afterText: 'Some existing textThe pasted text content...', replacedText: '', // Nothing was selected/replaced replacedRange: undefined } ``` ### Selection Events [Section titled “Selection Events”](#selection-events) Captures text selection actions. ```typescript interface SelectionEvent { type: 'select'; timestamp: number; startPosition: number; endPosition: number; selectedLength: number; selectedText?: string; // The selected text content (omitted when privacy-sensitive) method: 'mouse' | 'keyboard' | 'programmatic'; isTrusted?: boolean; // true if user-generated, false if scripted } ``` ### Undo/Redo Events [Section titled “Undo/Redo Events”](#undoredo-events) Captures undo and redo operations. ```typescript interface UndoRedoEvent { type: 'undo' | 'redo'; timestamp: number; shortcut: string; // e.g., 'Ctrl+Z', 'Meta+Z' beforeText: string; afterText: string; } ``` ### Programmatic Insertion Events [Section titled “Programmatic Insertion Events”](#programmatic-insertion-events) Detects text inserted without corresponding keystrokes, such as browser autocomplete, autofill, predictive text, AI extension injection, voice dictation, or programmatic value assignment. ```typescript interface ProgrammaticInsertionEvent { type: 'programmatic-insertion'; timestamp: number; insertedText: string; // Text that was inserted insertedLength: number; // Character count position: number; // Cursor position where text was inserted isTrusted: boolean; // Whether the triggering input event was user-generated inputType?: string; // InputEvent.inputType if available inputSource?: InputSource; // Input source classification, set by editor integrations or beforeinput fallback } ``` WriteTrack monitors text length changes against keystroke count. When text grows faster than keystrokes can explain, a programmatic insertion event is emitted. ### Composition Events [Section titled “Composition Events”](#composition-events) Captures completed IME composition sequences ([CJK](https://en.wikipedia.org/wiki/CJK_characters) input methods, dead keys, accent input). Intermediate keystrokes during composition are suppressed — only the final result is recorded. ```typescript interface CompositionEvent { timestamp: number; // When the composition started duration: number; // Duration of the composition sequence (ms) insertedLength: number; // Characters inserted (0 if composition was abandoned) isTrusted: boolean; // Whether the compositionend event was user-generated } ``` ## Output Format [Section titled “Output Format”](#output-format) ### Raw Data Access [Section titled “Raw Data Access”](#raw-data-access) Access captured events directly: ```typescript const keystrokes = tracker.getRawEvents(); const clipboard = tracker.getClipboardEvents(); const selections = tracker.getSelectionEvents(); const undoRedo = tracker.getUndoRedoEvents(); const insertions = tracker.getProgrammaticInsertionEvents(); const compositions = tracker.getCompositionEvents(); ``` ### Session Data Export [Section titled “Session Data Export”](#session-data-export) `getData()` returns the complete session as a `WriteTrackDataSchema` object: ```typescript const data = tracker.getData(); // { // version: "2.1.0", // metadata: { tool, targetElement, timestamp, duration }, // session: { events, clipboardEvents, selectionEvents, ... }, // quality: { overallScore, sequenceValid, qualityLevel, ... }, // } ``` ## Special Events [Section titled “Special Events”](#special-events) ### Focus Events [Section titled “Focus Events”](#focus-events) Window focus changes are recorded as keystroke events: ```typescript { key: 'FOCUS', // or 'BLUR' code: 'FOCUS', // or 'BLUR' type: 'keydown', timestamp: 1234567890.12, windowFocused: true, // or false timeSinceLastMouse: 0 } ``` ### Visibility Changes [Section titled “Visibility Changes”](#visibility-changes) Document visibility changes (tab switching): ```typescript { key: 'VISIBILITY_CHANGE', code: 'VISIBILITY_CHANGE', type: 'keydown', timestamp: 1234567890.12, windowFocused: false, timeSinceLastMouse: 1234.5, dwellTime: 5000 // Duration of visibility } ``` ## Timing Precision [Section titled “Timing Precision”](#timing-precision) WriteTrack uses `performance.now()` for high-precision timestamps: * Resolution: Sub-millisecond (typically 5μs) * Monotonic: Always increasing, not affected by system clock changes * Relative: Starts at 0 when the page loads ```typescript // Convert to absolute time if needed const absoluteTime = Date.now() - performance.now() + event.timestamp; ``` Caution Some browsers reduce `performance.now()` precision in cross-origin iframes for security reasons. If running WriteTrack in an iframe, verify timing precision is adequate for your use case. # Event-Detail Charts > Visualizing WriteTrack Capture Data Event-detail charts consume raw [`WriteTrackDataSchema`](/docs/api-reference/#writetrackdataschema) from `getData()`. They work without WASM — no analysis pipeline needed. ## Usage [Section titled “Usage”](#usage) All event-detail charts follow the same pattern — import, register once, then pass capture data: ```typescript import { Sparkline } from 'writetrack/viz'; Sparkline.register(); // call once per component document.querySelector('wt-sparkline').setData(tracker.getData()); ``` ## Sparkline `` [Section titled “Sparkline \”](#sparkline-wt-sparkline) Compact speed chart with no axes or labels — designed for inline use. Recommended size: `200px × 40px`. You can also create a Sparkline programmatically without a pre-existing element: ```typescript const sparkline = Sparkline.create(containerElement, tracker.getData()); ``` ## Document Growth `` [Section titled “Document Growth \”](#document-growth-wt-document-growth) Cumulative character count over time. The slope shows typing speed; vertical jumps indicate paste events, labeled with the character count pasted. Recommended size: `width: 100%; height: 200px`. ## Edit Beeswarm `` [Section titled “Edit Beeswarm \”](#edit-beeswarm-wt-edit-beeswarm) Keystroke-level beeswarm showing every keypress as a dot, dodge-stacked over time. Paste and cut events appear as larger prominent dots. Density indicates typing speed; gaps indicate pauses. Recommended size: `width: 100%; height: 160px`. ## Edit Waterfall `` [Section titled “Edit Waterfall \”](#edit-waterfall-wt-edit-waterfall) Cursor position over time. Diagonal lines show sequential typing; jumps indicate navigation. Cyan dots mark paste events. The Y-axis is reversed so position 0 (start of document) is at the top. Recommended size: `width: 100%; height: 200px`. ## Corrections Bubble `` [Section titled “Corrections Bubble \”](#corrections-bubble-wt-corrections-bubble) Edit runs visualized as bubbles — green for insertions, red for deletions, cyan for pastes. Size is proportional to log₂(character count). Consecutive same-type actions are grouped into runs. Recommended size: `width: 100%; height: 80px`. # CKEditor 5 Integration > Using WriteTrack with CKEditor 5 rich text editor WriteTrack includes a first-party CKEditor 5 plugin, compatible with the unified `ckeditor5` package v42+ and tested against v47.5. ## Installation [Section titled “Installation”](#installation) Install WriteTrack alongside CKEditor 5: * npm ```sh npm i writetrack ckeditor5 ``` * pnpm ```sh pnpm add writetrack ckeditor5 ``` * yarn ```sh yarn add writetrack ckeditor5 ``` * bun ```sh bun add writetrack ckeditor5 ``` ## Basic Usage [Section titled “Basic Usage”](#basic-usage) ```ts import { ClassicEditor, Essentials, Paragraph, Bold, Italic } from 'ckeditor5'; import { WriteTrackPlugin } from 'writetrack/ckeditor'; ClassicEditor.create(document.querySelector('#editor'), { plugins: [Essentials, Paragraph, Bold, Italic, WriteTrackPlugin], writetrack: { license: 'your-license-key', }, }).then((editor) => { // Access WriteTrack data when needed const plugin = editor.plugins.get('WriteTrack'); const data = plugin.tracker.getData(); }); ``` ## Configuration [Section titled “Configuration”](#configuration) Pass options via the `writetrack` config key: ```ts ClassicEditor.create(element, { plugins: [, /* ... */ WriteTrackPlugin], writetrack: { license: 'your-license-key', userId: 'user-123', contentId: 'document-456', metadata: { formName: 'signup' }, autoStart: true, // default }, }); ``` | Option | Type | Default | Description | | ----------- | ------------------------- | ----------- | ----------------------------------------------------------- | | `license` | `string` | `undefined` | WriteTrack license key | | `userId` | `string` | `undefined` | User identifier included in metadata | | `contentId` | `string` | `undefined` | Content identifier included in metadata | | `metadata` | `Record` | `undefined` | Additional metadata | | `autoStart` | `boolean` | `true` | Start tracking when editor is ready | | `wasmUrl` | `string` | `undefined` | URL to WASM binary for analysis | | `persist` | `boolean` | `false` | Enable IndexedDB session persistence (requires `contentId`) | ## Accessing Data [Section titled “Accessing Data”](#accessing-data) The plugin is retrieved via CKEditor’s plugin system: ```ts const plugin = editor.plugins.get('WriteTrack'); // Get the tracker instance const tracker = plugin.tracker; // Get typing data const data = tracker.getData(); // Check tracking status const isTracking = plugin.isTracking; ``` ## Manual DOM Attachment [Section titled “Manual DOM Attachment”](#manual-dom-attachment) If you prefer not to use the plugin, you can attach WriteTrack directly to CKEditor’s editable element: ```ts import WriteTrack from 'writetrack'; import { ClassicEditor, Essentials, Paragraph } from 'ckeditor5'; ClassicEditor.create(document.querySelector('#editor'), { plugins: [Essentials, Paragraph], }).then((editor) => { // Wait for UI to be ready, then attach editor.ui.once('ready', () => { const element = editor.ui.getEditableElement(); const tracker = new WriteTrack({ target: element, license: 'your-license-key', }); tracker.start(); }); }); ``` Note The editable element is obtained via `editor.ui.getEditableElement()` — a `
` with `contenteditable="true"`. You must wait for the `ready` event since the element doesn’t exist during `create()`. ## Cleanup [Section titled “Cleanup”](#cleanup) The plugin automatically stops tracking and cleans up when the editor is destroyed: ```ts await editor.destroy(); // WriteTrack is stopped automatically ``` ## TypeScript [Section titled “TypeScript”](#typescript) ```ts import type { WriteTrackPluginOptions } from 'writetrack/ckeditor'; ``` # Lexical Integration > Using WriteTrack with Meta's Lexical text editor WriteTrack includes a first-party Lexical integration, compatible with Lexical v0.12+. ## Installation [Section titled “Installation”](#installation) Install WriteTrack alongside Lexical: * npm ```sh npm i writetrack lexical ``` * pnpm ```sh pnpm add writetrack lexical ``` * yarn ```sh yarn add writetrack lexical ``` * bun ```sh bun add writetrack lexical ``` ## Basic Usage [Section titled “Basic Usage”](#basic-usage) ```ts import { createEditor, $getSelection } from 'lexical'; import { createWriteTrackLexical } from 'writetrack/lexical'; const editor = createEditor({ namespace: 'MyEditor', onError: console.error }); editor.setRootElement(document.getElementById('editor')); const handle = createWriteTrackLexical(editor, $getSelection, { license: 'your-license-key', }); // Access WriteTrack data when needed const data = handle.tracker?.getData(); ``` ## Configuration [Section titled “Configuration”](#configuration) Pass `$getSelection` as the second argument and options as the third: ```ts const handle = createWriteTrackLexical(editor, $getSelection, { license: 'your-license-key', userId: 'user-123', contentId: 'document-456', metadata: { formName: 'signup' }, autoStart: true, // default }); ``` | Option | Type | Default | Description | | ----------- | ------------------------- | ----------- | ----------------------------------------------------------- | | `license` | `string` | `undefined` | WriteTrack license key | | `userId` | `string` | `undefined` | User identifier included in metadata | | `contentId` | `string` | `undefined` | Content identifier included in metadata | | `metadata` | `Record` | `undefined` | Additional metadata | | `autoStart` | `boolean` | `true` | Start tracking when root element is available | | `wasmUrl` | `string` | `undefined` | URL to WASM binary for analysis | | `persist` | `boolean` | `false` | Enable IndexedDB session persistence (requires `contentId`) | ## Accessing Data [Section titled “Accessing Data”](#accessing-data) ```ts // Get the tracker instance (null until root element is available) const tracker = handle.tracker; // Get typing data const data = tracker?.getData(); // Check tracking status const isTracking = handle.isTracking; ``` ## Manual DOM Attachment [Section titled “Manual DOM Attachment”](#manual-dom-attachment) If you prefer not to use the integration, you can attach WriteTrack directly to the root element: ```ts import WriteTrack from 'writetrack'; import { createEditor } from 'lexical'; const editor = createEditor({ namespace: 'MyEditor', onError: console.error }); const rootElement = document.getElementById('editor')!; editor.setRootElement(rootElement); const tracker = new WriteTrack({ target: rootElement, license: 'your-license-key', }); tracker.start(); ``` ## Cleanup [Section titled “Cleanup”](#cleanup) Call `destroy()` to stop tracking and release resources: ```ts handle.destroy(); ``` The integration also automatically cleans up if the root element is removed from the editor. ## TypeScript [Section titled “TypeScript”](#typescript) ```ts import type { WriteTrackLexicalOptions, WriteTrackLexicalHandle, } from 'writetrack/lexical'; ``` # ProseMirror Integration > Using WriteTrack with ProseMirror editor framework WriteTrack includes a first-party ProseMirror plugin, compatible with `prosemirror-state` v1.0+ and tested against v1.4. ## Installation [Section titled “Installation”](#installation) Install WriteTrack alongside your ProseMirror packages: * npm ```sh npm i writetrack prosemirror-state prosemirror-view prosemirror-model ``` * pnpm ```sh pnpm add writetrack prosemirror-state prosemirror-view prosemirror-model ``` * yarn ```sh yarn add writetrack prosemirror-state prosemirror-view prosemirror-model ``` * bun ```sh bun add writetrack prosemirror-state prosemirror-view prosemirror-model ``` ## Basic Usage [Section titled “Basic Usage”](#basic-usage) ```ts import { EditorState } from 'prosemirror-state'; import { EditorView } from 'prosemirror-view'; import { schema } from 'prosemirror-schema-basic'; import { WriteTrackPlugin, WriteTrackPluginKey } from 'writetrack/prosemirror'; const state = EditorState.create({ schema, plugins: [ WriteTrackPlugin({ license: 'your-license-key', }), ], }); const view = new EditorView(document.querySelector('#editor'), { state }); // Access WriteTrack data when needed const pluginState = WriteTrackPluginKey.getState(view.state); const data = pluginState.tracker.getData(); ``` ## Configuration [Section titled “Configuration”](#configuration) Pass options to the `WriteTrackPlugin()` function: ```ts WriteTrackPlugin({ license: 'your-license-key', userId: 'user-123', contentId: 'document-456', metadata: { formName: 'signup' }, autoStart: true, // default }); ``` | Option | Type | Default | Description | | ----------- | ------------------------- | ----------- | ----------------------------------------------------------- | | `license` | `string` | `undefined` | WriteTrack license key | | `userId` | `string` | `undefined` | User identifier included in metadata | | `contentId` | `string` | `undefined` | Content identifier included in metadata | | `metadata` | `Record` | `undefined` | Additional metadata | | `autoStart` | `boolean` | `true` | Start tracking when view is created | | `wasmUrl` | `string` | `undefined` | URL to WASM binary for analysis | | `persist` | `boolean` | `false` | Enable IndexedDB session persistence (requires `contentId`) | ## Accessing Data [Section titled “Accessing Data”](#accessing-data) Use the `WriteTrackPluginKey` to read plugin state from the editor: ```ts import { WriteTrackPluginKey } from 'writetrack/prosemirror'; const pluginState = WriteTrackPluginKey.getState(view.state); // Get the tracker instance const tracker = pluginState.tracker; // Get typing data const data = tracker.getData(); // Check tracking status const isTracking = pluginState.isTracking; ``` ## Manual DOM Attachment [Section titled “Manual DOM Attachment”](#manual-dom-attachment) If you prefer not to use the plugin, you can attach WriteTrack directly to ProseMirror’s DOM element: ```ts import WriteTrack from 'writetrack'; import { EditorState } from 'prosemirror-state'; import { EditorView } from 'prosemirror-view'; import { schema } from 'prosemirror-schema-basic'; const state = EditorState.create({ schema }); const view = new EditorView(document.querySelector('#editor'), { state }); const tracker = new WriteTrack({ target: view.dom, license: 'your-license-key', }); tracker.start(); ``` Note The editable element is `view.dom` — a `
` with `contenteditable="true"`. It’s available synchronously after `new EditorView()`. ## Cleanup [Section titled “Cleanup”](#cleanup) The plugin automatically stops tracking when the view is destroyed: ```ts view.destroy(); // WriteTrack is stopped automatically ``` ## TypeScript [Section titled “TypeScript”](#typescript) ```ts import type { WriteTrackPluginOptions, WriteTrackPluginState, } from 'writetrack/prosemirror'; ``` # Quill Integration > Using WriteTrack with Quill rich text editor WriteTrack includes a first-party Quill module, compatible with Quill v1.3+ and tested against v2.0. ## Installation [Section titled “Installation”](#installation) Install WriteTrack alongside Quill: * npm ```sh npm i writetrack quill ``` * pnpm ```sh pnpm add writetrack quill ``` * yarn ```sh yarn add writetrack quill ``` * bun ```sh bun add writetrack quill ``` ## Basic Usage [Section titled “Basic Usage”](#basic-usage) ```ts import Quill from 'quill'; import { WriteTrackModule } from 'writetrack/quill'; // Register the module before creating the editor Quill.register('modules/writetrack', WriteTrackModule); const quill = new Quill('#editor', { theme: 'snow', modules: { writetrack: { license: 'your-license-key', }, }, }); // Access WriteTrack data when needed const mod = quill.getModule('writetrack'); const data = mod.tracker.getData(); ``` ## Configuration [Section titled “Configuration”](#configuration) Pass options via the `writetrack` module config: ```ts const quill = new Quill('#editor', { theme: 'snow', modules: { writetrack: { license: 'your-license-key', userId: 'user-123', contentId: 'document-456', metadata: { formName: 'signup' }, autoStart: true, // default }, }, }); ``` | Option | Type | Default | Description | | ----------- | ------------------------- | ----------- | ----------------------------------------------------------- | | `license` | `string` | `undefined` | WriteTrack license key | | `userId` | `string` | `undefined` | User identifier included in metadata | | `contentId` | `string` | `undefined` | Content identifier included in metadata | | `metadata` | `Record` | `undefined` | Additional metadata | | `autoStart` | `boolean` | `true` | Start tracking when module is created | | `wasmUrl` | `string` | `undefined` | URL to WASM binary for analysis | | `persist` | `boolean` | `false` | Enable IndexedDB session persistence (requires `contentId`) | ## Accessing Data [Section titled “Accessing Data”](#accessing-data) Retrieve the module via Quill’s module system: ```ts const mod = quill.getModule('writetrack'); // Get the tracker instance const tracker = mod.tracker; // Get typing data const data = tracker.getData(); // Check tracking status const isTracking = mod.isTracking; ``` ## Manual DOM Attachment [Section titled “Manual DOM Attachment”](#manual-dom-attachment) If you prefer not to use the module, you can attach WriteTrack directly to Quill’s editable element: ```ts import WriteTrack from 'writetrack'; import Quill from 'quill'; const quill = new Quill('#editor', { theme: 'snow' }); const tracker = new WriteTrack({ target: quill.root, license: 'your-license-key', }); tracker.start(); ``` Note The editable element is `quill.root` — a `
` with `contenteditable="true"`. It’s available synchronously after `new Quill()`. ## Cleanup [Section titled “Cleanup”](#cleanup) Quill does not have a built-in destroy lifecycle, so the WriteTrack module’s `destroy()` method must be called manually before unmounting the editor: ```ts const mod = quill.getModule('writetrack'); mod.destroy(); ``` ## TypeScript [Section titled “TypeScript”](#typescript) ```ts import type { WriteTrackModuleOptions } from 'writetrack/quill'; ``` # Slate Integration > Using WriteTrack with the Slate rich text framework WriteTrack includes a first-party Slate integration, compatible with Slate v0.94+. ## Installation [Section titled “Installation”](#installation) Install WriteTrack alongside Slate: * npm ```sh npm i writetrack slate slate-react ``` * pnpm ```sh pnpm add writetrack slate slate-react ``` * yarn ```sh yarn add writetrack slate slate-react ``` * bun ```sh bun add writetrack slate slate-react ``` ## Basic Usage [Section titled “Basic Usage”](#basic-usage) ```tsx import { useEffect, useMemo, useRef } from 'react'; import { createEditor } from 'slate'; import { Slate, Editable, withReact, ReactEditor } from 'slate-react'; import { createWriteTrackSlate } from 'writetrack/slate'; function Editor() { const editor = useMemo(() => withReact(createEditor()), []); const handleRef = useRef(null); useEffect(() => { const domNode = ReactEditor.toDOMNode(editor, editor); const handle = createWriteTrackSlate(editor, domNode, { license: 'your-license-key', }); handleRef.current = handle; return () => handle.destroy(); }, [editor]); return ( ); } ``` ## Configuration [Section titled “Configuration”](#configuration) Pass options as the third argument: ```ts const handle = createWriteTrackSlate(editor, domNode, { license: 'your-license-key', userId: 'user-123', contentId: 'document-456', metadata: { formName: 'signup' }, autoStart: true, // default }); ``` | Option | Type | Default | Description | | ----------- | ------------------------- | ----------- | ----------------------------------------------------------- | | `license` | `string` | `undefined` | WriteTrack license key | | `userId` | `string` | `undefined` | User identifier included in metadata | | `contentId` | `string` | `undefined` | Content identifier included in metadata | | `metadata` | `Record` | `undefined` | Additional metadata | | `autoStart` | `boolean` | `true` | Start tracking when created | | `wasmUrl` | `string` | `undefined` | URL to WASM binary for analysis | | `persist` | `boolean` | `false` | Enable IndexedDB session persistence (requires `contentId`) | ## Accessing Data [Section titled “Accessing Data”](#accessing-data) ```ts // Get the tracker instance const tracker = handle.tracker; // Get typing data const data = tracker?.getData(); // Check tracking status const isTracking = handle.isTracking; ``` ## Cleanup [Section titled “Cleanup”](#cleanup) Slate does not have a built-in destroy lifecycle, so `destroy()` must be called manually — typically in a React `useEffect` cleanup: ```ts useEffect(() => { const handle = createWriteTrackSlate(editor, domNode, options); return () => handle.destroy(); }, [editor]); ``` ## TypeScript [Section titled “TypeScript”](#typescript) ```ts import type { WriteTrackSlateOptions, WriteTrackSlateHandle, } from 'writetrack/slate'; ``` # TinyMCE Integration > Using WriteTrack with TinyMCE rich text editor WriteTrack includes a first-party TinyMCE integration, compatible with TinyMCE v6+. ## Installation [Section titled “Installation”](#installation) Install WriteTrack alongside TinyMCE: * npm ```sh npm i writetrack tinymce ``` * pnpm ```sh pnpm add writetrack tinymce ``` * yarn ```sh yarn add writetrack tinymce ``` * bun ```sh bun add writetrack tinymce ``` ## Basic Usage [Section titled “Basic Usage”](#basic-usage) ```ts import tinymce from 'tinymce'; import { createWriteTrackTinyMCE } from 'writetrack/tinymce'; tinymce.init({ selector: '#editor', inline: true, // recommended for full WriteTrack support setup: (editor) => { const handle = createWriteTrackTinyMCE(editor, { license: 'your-license-key', }); // Access WriteTrack data after editor init editor.on('init', () => { const data = handle.tracker?.getData(); }); }, }); ``` Inline mode recommended TinyMCE’s default iframe mode isolates the editor in a separate document context. WriteTrack’s focus and selection tracking relies on `window` and `document` listeners, which only work correctly when the editor is in the same document. Use `inline: true` in your TinyMCE configuration for full WriteTrack support. In iframe mode, keystroke and clipboard tracking will work, but focus loss detection and selection change tracking may be degraded. ## Configuration [Section titled “Configuration”](#configuration) Pass options as the second argument: ```ts const handle = createWriteTrackTinyMCE(editor, { license: 'your-license-key', userId: 'user-123', contentId: 'document-456', metadata: { formName: 'signup' }, autoStart: true, // default }); ``` | Option | Type | Default | Description | | ----------- | ------------------------- | ----------- | ----------------------------------------------------------- | | `license` | `string` | `undefined` | WriteTrack license key | | `userId` | `string` | `undefined` | User identifier included in metadata | | `contentId` | `string` | `undefined` | Content identifier included in metadata | | `metadata` | `Record` | `undefined` | Additional metadata | | `autoStart` | `boolean` | `true` | Start tracking when editor initializes | | `wasmUrl` | `string` | `undefined` | URL to WASM binary for analysis | | `persist` | `boolean` | `false` | Enable IndexedDB session persistence (requires `contentId`) | ## Accessing Data [Section titled “Accessing Data”](#accessing-data) The tracker is available after TinyMCE’s `init` event fires: ```ts tinymce.init({ selector: '#editor', inline: true, setup: (editor) => { const handle = createWriteTrackTinyMCE(editor, { license: 'your-license-key', }); editor.on('init', () => { const tracker = handle.tracker; const data = tracker?.getData(); const isTracking = handle.isTracking; }); }, }); ``` ## Manual DOM Attachment [Section titled “Manual DOM Attachment”](#manual-dom-attachment) If you prefer not to use the integration, you can attach WriteTrack directly in inline mode: ```ts import WriteTrack from 'writetrack'; import tinymce from 'tinymce'; tinymce.init({ selector: '#editor', inline: true, setup: (editor) => { editor.on('init', () => { const tracker = new WriteTrack({ target: editor.getBody(), license: 'your-license-key', }); tracker.start(); }); }, }); ``` ## Cleanup [Section titled “Cleanup”](#cleanup) The integration automatically cleans up when TinyMCE’s `remove` event fires. You can also call `destroy()` manually: ```ts handle.destroy(); ``` ## TypeScript [Section titled “TypeScript”](#typescript) ```ts import type { WriteTrackTinyMCEOptions, WriteTrackTinyMCEHandle, } from 'writetrack/tinymce'; ``` # TipTap Integration > Using WriteTrack with TipTap rich text editor WriteTrack includes a first-party TipTap extension, compatible with `@tiptap/core` v2.0+ and tested against v3.19. See [React](#react) and [Vue](#vue) examples below. ## Installation [Section titled “Installation”](#installation) Install WriteTrack alongside your TipTap packages: * npm ```sh npm i writetrack @tiptap/core @tiptap/starter-kit ``` * pnpm ```sh pnpm add writetrack @tiptap/core @tiptap/starter-kit ``` * yarn ```sh yarn add writetrack @tiptap/core @tiptap/starter-kit ``` * bun ```sh bun add writetrack @tiptap/core @tiptap/starter-kit ``` ## Basic Usage [Section titled “Basic Usage”](#basic-usage) ```ts import { Editor } from '@tiptap/core'; import StarterKit from '@tiptap/starter-kit'; import { WriteTrackExtension } from 'writetrack/tiptap'; const editor = new Editor({ element: document.querySelector('#editor'), extensions: [ StarterKit, WriteTrackExtension.configure({ license: 'your-license-key', }), ], }); // Access WriteTrack data when needed const data = editor.storage.writetrack.tracker.getData(); ``` ## Configuration [Section titled “Configuration”](#configuration) Pass options via `.configure()`: ```ts WriteTrackExtension.configure({ license: 'your-license-key', userId: 'user-123', contentId: 'document-456', metadata: { formName: 'signup' }, autoStart: true, // default }); ``` | Option | Type | Default | Description | | ----------- | ------------------------- | ----------- | ----------------------------------------------------------- | | `license` | `string` | `undefined` | WriteTrack license key | | `userId` | `string` | `undefined` | User identifier included in metadata | | `contentId` | `string` | `undefined` | Content identifier included in metadata | | `metadata` | `Record` | `undefined` | Additional metadata | | `autoStart` | `boolean` | `true` | Start tracking when editor is created | | `wasmUrl` | `string` | `undefined` | URL to WASM binary for analysis | | `persist` | `boolean` | `false` | Enable IndexedDB session persistence (requires `contentId`) | ## Accessing Data [Section titled “Accessing Data”](#accessing-data) The extension stores the WriteTrack instance in TipTap’s storage system: ```ts // Get the tracker instance const tracker = editor.storage.writetrack.tracker; // Get typing data const data = tracker.getData(); // Check tracking status const isTracking = editor.storage.writetrack.isTracking; ``` ## React [Section titled “React”](#react) ```tsx import { useEditor, EditorContent } from '@tiptap/react'; import StarterKit from '@tiptap/starter-kit'; import { WriteTrackExtension } from 'writetrack/tiptap'; const writetrack = WriteTrackExtension.configure({ license: 'your-license-key', }); function FormEditor() { const editor = useEditor({ extensions: [StarterKit, writetrack], }); const handleSubmit = () => { const data = editor?.storage.writetrack.tracker?.getData(); console.log('Typing data:', data); }; return (
); } ``` ## Vue [Section titled “Vue”](#vue) ```vue ``` ## Manual DOM Attachment [Section titled “Manual DOM Attachment”](#manual-dom-attachment) If you prefer not to use the extension, you can attach WriteTrack directly to TipTap’s editable element: ```ts import WriteTrack from 'writetrack'; import { Editor } from '@tiptap/core'; import StarterKit from '@tiptap/starter-kit'; const editor = new Editor({ element: document.querySelector('#editor'), extensions: [StarterKit], onCreate({ editor }) { const tracker = new WriteTrack({ target: editor.view.dom, license: 'your-license-key', }); tracker.start(); }, }); ``` Note The editable element is `editor.view.dom` — a `
` with `contenteditable="true"` managed by ProseMirror under the hood. ## TypeScript [Section titled “TypeScript”](#typescript) ```ts import type { WriteTrackExtensionOptions, WriteTrackExtensionStorage, } from 'writetrack/tiptap'; ``` # Licensing > License keys for production use ## Getting a License Key [Section titled “Getting a License Key”](#getting-a-license-key) You can get a license key from the [web portal](/portal/signup) or via the CLI. Run the CLI to register a free 28-day trial: ```bash npx writetrack init ``` The CLI will prompt for your email and production domain, then save the license key to `.env`: ```plaintext WriteTrack — Set up a free 28-day trial Email address: you@example.com Production domain (e.g. example.com): example.com Registering you@example.com for example.com... Done — license key saved to .env Trial expires: 3/10/2026 ``` Then pass the key when initializing: ```typescript const tracker = new WriteTrack({ target: textarea, license: process.env.WRITETRACK_LICENSE_KEY, }); ``` Tip The CLI writes `WRITETRACK_LICENSE_KEY` to your `.env` file and checks that `.env` is in `.gitignore`. If you already have a key configured, it will show the existing key instead of registering again. ## Evaluation Mode [Section titled “Evaluation Mode”](#evaluation-mode) On localhost, WriteTrack runs with full analysis enabled — no license key required. Use this to evaluate the SDK during development and testing. A license key is required for production deployments. ```typescript // Evaluation mode — localhost only, no license required const tracker = new WriteTrack({ target: textarea }); ``` # Next.js > Using WriteTrack with Next.js applications WriteTrack uses browser APIs and must run client-side only. Here’s how to integrate it with Next.js. ## App Router (Next.js 13+) [Section titled “App Router (Next.js 13+)”](#app-router-nextjs-13) Add the `'use client'` directive to components using WriteTrack: ```tsx 'use client'; import { useRef, useEffect } from 'react'; import { WriteTrack } from 'writetrack'; export function ResponseForm() { const textareaRef = useRef(null); const trackerRef = useRef(null); useEffect(() => { if (textareaRef.current) { trackerRef.current = new WriteTrack({ target: textareaRef.current }); trackerRef.current.start(); } return () => { trackerRef.current?.stop(); }; }, []); const handleSubmit = (e: React.FormEvent) => { e.preventDefault(); if (trackerRef.current) { const data = trackerRef.current.getData(); const events = trackerRef.current.getRawEvents(); console.log(`Captured ${events.length} events`, data.quality); } }; return (
``` ## Cleanup [Section titled “Cleanup”](#cleanup) Always stop the tracker when you’re done to remove event listeners: ```typescript tracker.stop(); ``` Caution Failing to call `stop()` can result in memory leaks from orphaned event listeners. ## Context Fields [Section titled “Context Fields”](#context-fields) Tag sessions with identifiers that flow through to `getData()` output and all sinks: ```typescript const tracker = new WriteTrack({ target: textarea, userId: 'u_abc123', contentId: 'post_draft_42', metadata: { formName: 'signup', variant: 'B' }, }); ``` | Field | Type | Description | | ----------- | ------------------------- | ---------------------------------------------------------- | | `userId` | `string` | Who is typing. Appears in `metadata.userId`. | | `contentId` | `string` | What they’re typing into. Appears in `metadata.contentId`. | | `metadata` | `Record` | Arbitrary key-value pairs. Appears as `metadata.custom`. | ## Analyze the Session (Optional) [Section titled “Analyze the Session (Optional)”](#analyze-the-session-optional) If you have a license key (or are running on localhost), you can analyze the captured session for authenticity signals: ```typescript import { formatIndicator } from 'writetrack'; const analysis = await tracker.getAnalysis(); if (analysis) { console.log(formatIndicator(analysis.contentOrigin.indicator)); console.log(formatIndicator(analysis.timingAuthenticity.indicator)); } ``` Or combine capture data with analysis in one call: ```typescript const report = await tracker.getSessionReport(); // report.data — captured events (same as getData()) // report.analysis — authenticity analysis (same as getAnalysis()) ``` See the [Analysis guide](/docs/analysis/) for full details. ## Next Steps [Section titled “Next Steps”](#next-steps) * [Session Persistence](/docs/persistence/) — Save and resume sessions across page reloads * [Licensing](/docs/license/) — License keys for production use * [React Integration](/docs/react/) — `useWriteTrack` hook with automatic lifecycle management * [Vue Integration](/docs/vue/) — `useWriteTrack` composable for Vue 3 * [Output Sinks](/docs/output-sinks/) — Route data to webhooks, Datadog, Segment, or OpenTelemetry * [API Reference](/docs/api-reference/) — Complete method and type reference # React Integration > Using WriteTrack with React applications WriteTrack provides a React hook for easy integration with React applications. ## Installation [Section titled “Installation”](#installation) * npm ```sh npm i writetrack ``` * pnpm ```sh pnpm add writetrack ``` * yarn ```sh yarn add writetrack ``` * bun ```sh bun add writetrack ``` React is an optional peer dependency - non-React users won’t have it in their bundle. Note The React hook uses `autoStart: true` by default. The tracker begins recording as soon as the element is mounted and the ref is attached. Set `autoStart: false` if you need to control when tracking begins. ## Basic Usage [Section titled “Basic Usage”](#basic-usage) ResponseForm.tsx ```tsx import React, { useRef } from 'react'; import { useWriteTrack } from 'writetrack/react'; export function ResponseForm() { const textareaRef = useRef(null); const { tracker, isTracking } = useWriteTrack(textareaRef); const handleSubmit = (e: React.FormEvent) => { e.preventDefault(); if (tracker) { const data = tracker.getData(); console.log('Session data:', data); } }; return (
'; const textarea = document.getElementById('input') as HTMLTextAreaElement; const tracker = new WriteTrack({ target: textarea }); tracker.start(); // Simulate keystrokes textarea.focus(); fireEvent.keyDown(textarea, { key: 'a', code: 'KeyA' }); fireEvent.keyUp(textarea, { key: 'a', code: 'KeyA' }); fireEvent.keyDown(textarea, { key: 'b', code: 'KeyB' }); fireEvent.keyUp(textarea, { key: 'b', code: 'KeyB' }); const events = tracker.getRawEvents(); expect(events.length).toBeGreaterThan(0); tracker.stop(); }); ``` ### Simulating Typing Patterns [Section titled “Simulating Typing Patterns”](#simulating-typing-patterns) Create helpers to simulate realistic typing: ```typescript async function simulateTyping( element: HTMLElement, text: string, options: { avgDelay?: number; variance?: number } = {} ) { const { avgDelay = 100, variance = 30 } = options; element.focus(); for (const char of text) { const delay = avgDelay + (Math.random() - 0.5) * variance * 2; await new Promise((r) => setTimeout(r, delay)); const code = `Key${char.toUpperCase()}`; fireEvent.keyDown(element, { key: char, code }); await new Promise((r) => setTimeout(r, 50 + Math.random() * 30)); fireEvent.keyUp(element, { key: char, code }); } } test('captures keystroke events from typing', async () => { const textarea = document.createElement('textarea'); document.body.appendChild(textarea); const tracker = new WriteTrack({ target: textarea }); tracker.start(); await simulateTyping(textarea, 'hello world', { avgDelay: 120 }); const events = tracker.getRawEvents(); expect(events.length).toBeGreaterThan(0); tracker.stop(); }); ``` ## E2E Testing [Section titled “E2E Testing”](#e2e-testing) ### Playwright [Section titled “Playwright”](#playwright) ```typescript import { test, expect } from '@playwright/test'; test('captures typing data on form submit', async ({ page }) => { await page.goto('/form'); const textarea = page.locator('#response-field'); // Type with realistic delays await textarea.focus(); await textarea.pressSequentially('This is my response content.', { delay: 100, }); await page.click('button[type="submit"]'); // Check submission succeeded await expect(page.locator('.success-message')).toBeVisible(); }); ``` ### Cypress [Section titled “Cypress”](#cypress) ```typescript describe('Response Form', () => { it('captures typing data', () => { cy.visit('/form'); cy.get('#response-field') .focus() .type('This is typed content.', { delay: 100 }); cy.get('button[type="submit"]').click(); cy.contains('Submitted successfully').should('be.visible'); }); }); ``` ## Testing Utilities [Section titled “Testing Utilities”](#testing-utilities) ### Factory Function [Section titled “Factory Function”](#factory-function) test/factories.ts ```typescript import type { KeystrokeEvent } from 'writetrack'; export function createKeystrokeEvent( overrides: Partial = {} ): KeystrokeEvent { return { key: 'a', code: 'KeyA', type: 'keydown', timestamp: Date.now(), windowFocused: true, timeSinceLastMouse: 0, ...overrides, }; } // Usage const event = createKeystrokeEvent({ key: 'Backspace', code: 'Backspace', isCorrection: true, }); ``` # Troubleshooting > Common issues and solutions when using WriteTrack ## Tracker Not Capturing Keystrokes [Section titled “Tracker Not Capturing Keystrokes”](#tracker-not-capturing-keystrokes) For rich text editors (TipTap, ProseMirror, CKEditor, Quill), make sure you’re targeting the actual editable element, not a wrapper: ```typescript // Wrong: targeting a container div new WriteTrack({ target: document.querySelector('.editor')! }); // Right: targeting the actual contenteditable element new 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”](#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”](#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. Note Mobile keyboard detection is best-effort. Consider the input context when interpreting results from mobile users. *** ## React / Vue Issues [Section titled “React / Vue Issues”](#react--vue-issues) ### Ref not attached [Section titled “Ref not attached”](#ref-not-attached) The hook/composable won’t work if the ref isn’t attached to a DOM element: ```tsx // Wrong: ref not assigned to element const { tracker } = useWriteTrack(textareaRef); return