Analysis
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”Call getAnalysis() on a tracker after the user finishes typing:
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 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”getSessionReport() bundles raw data and analysis into a single object — useful when you want to send everything to your server in one payload:
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”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:
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}Analysis Categories
Section titled “Analysis Categories”Each category contains an indicator (an IndicatorOutput with a machine-readable code and numeric params) and a metrics object with the underlying data. Use 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”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”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”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”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”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”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”The analysis includes a segmented timeline classifying each period of the session:
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 – 12000msSegment types: typing, paste, pause, tabAway, autocomplete, navigating.
WASM Loading
Section titled “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:
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:
const tracker = new WriteTrack({ target: textarea, wasmUrl: '/assets/writetrack.wasm',});Next Steps
Section titled “Next Steps”- Verification — Verify that analysis output is authentic and untampered
- API Reference — Full method signatures and type definitions
- Output Sinks — Route session data to your backend
- Privacy & Security — What WriteTrack collects and doesn’t collect