Skip to content

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.

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.

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),
});

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
}

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.

Where the text came from: typed, pasted, or autocompleted.

MetricTypeDescription
charactersByOrigin.typednumberRatio of directly typed characters (0–1)
charactersByOrigin.pastednumberRatio of pasted characters (0–1)
charactersByOrigin.autocompletednumberRatio of autocompleted characters (0–1)
pasteEventsarrayEach 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.internalPasteRationumberRatio of chars from internal paste (cut-paste restructuring) to total chars (0–1)
pasteClassification.externalPasteRationumberRatio of chars from external paste (outside content) to total chars (0–1)
pasteClassification.unknownPasteRationumberRatio of chars from unknown-source paste to total chars (0–1)
pasteEditSummary.pasteRetentionRatenumberFraction of pastes with zero edits in their region within 60 seconds (0–1)
pasteEditSummary.meanModificationDelayMsnumberAverage delay (ms) from paste to first edit, across pastes that were edited
pasteEditSummary.pasteToTypeRationumberTotal pasted characters / total typed characters
pasteEditSummary.meanReworkRationumberAverage rework ratio across all pastes (0 = no editing, >1 = heavy rework)
pasteEditSummary.pasteLengthMeannumberMean paste size in characters
pasteEditSummary.pasteLengthStdnumberStandard deviation of paste sizes
pasteEditSummary.pasteLengthMinnumberSmallest paste size in characters
pasteEditSummary.pasteLengthMaxnumberLargest paste size in characters
contentTimelineMultiTimeSeriesCharacter 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)

Whether keystroke timing looks human or mechanical.

MetricTypeDescription
dwellTimeDistributionobjectMean, coefficient of variation (CV), histogram, and human reference range for key hold durations
flightTimeDistributionobjectSame structure for intervals between keystrokes
periodicityScorenumberHow periodic the keystroke rhythm is (0–1, higher = more periodic)
entropynumberShannon entropy of timing intervals
timingOverTimeMultiTimeSeriesTiming metrics across the session

Example indicators:

  • “Timing variability is within normal range”
  • “Keystroke timing is mechanically uniform”
  • “Highly periodic keystroke pattern detected”

Behavioral consistency throughout the session — detects abrupt rhythm shifts and tab-away patterns.

MetricTypeDescription
changePointsarrayPoints where typing behavior shifted abruptly. Each has timestamp, metric (which metric changed), valueBefore, valueAfter, and magnitude
tabAwayEventsarrayEach tab-away with leftAt, returnedAt, duration (ms), and activityAfterReturn (typing, paste, or idle)
segmentComparisonMultiTimeSeriesBehavioral 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”

Whether the events could have been produced by a physical keyboard.

MetricTypeDescription
impossibleSequencesarrayKeystroke pairs faster than humanly possible. Each has timestamp, durationMs, and humanMinimumMs
syntheticEventRationumberRatio of events with isTrusted: false (0–1)
mouseActivityDuringTypingobjectperiodsWithMouseActivity (ratio 0–1) and longestGapWithoutMouse (ms)
zeroLatencyEventCountnumberEvents with zero milliseconds between them
unmatchedKeydownCountnumberKeydowns 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”

How the author corrected and revised their work.

MetricTypeDescription
correctionRatenumberRatio of correction keystrokes to total keystrokes (0–1)
correctionCountnumberTotal correction keystrokes
navigationCountnumberArrow key navigation events
undoRedoCountnumberUndo/redo operations
editPatternsarrayCategorized edit sequences (select-delete-type, select-paste, paste-only, select-edit, multi-step) with counts
correctionTimelineTimeSeriesCorrection rate over time
revisionDepthobjectMax/average depth and count of regions edited multiple times
proportionBehindFrontiernumberRatio of keydowns behind the text frontier (0–1). Higher values indicate revision/composition vs linear transcription
substitutedWordsCountnumberCount of word-level select-then-replace events (selection >= 2 chars followed by typing or paste)
jumpToEditFrequencynumberRatio of non-local cursor jumps (>= 5 chars) that precede an edit action (0–1)
jumpDistanceSdnumberStandard deviation of cursor travel distances during non-local jumps. High = structural revision; low = clustered typo fixes
revisionAtPriorRationumberRatio of corrections at a prior position (behind the frontier) to total corrections (0–1)
revisionAtFrontierRationumberRatio 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%)“

Typing speed changes, fatigue, warmup, and burst patterns over the session.

MetricTypeDescription
sessionDurationMsnumberTotal session length
speedTimelineTimeSeriesTyping speed over time
fatigueRationumberTyping speed trend across the session (< 1 = slowing down)
warmupRationumberInitial typing speed relative to overall (< 1 = started slow)
pauseDistributionobjectHistogram, count, and mean duration of pauses
burstPatternobjectAverage 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”

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 – 12000ms

Segment types: typing, paste, pause, tabAway, autocomplete, navigating.

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',
});