Skip to content

API Reference

Complete reference for all public exports from the WriteTrack SDK.

WriteTrack provides multiple entry points via package.json exports:

Import PathFormatDescription
writetrackESM, CJS, BrowserCore SDK — WriteTrack class and types
writetrack/pipesESM, CJS, BrowserOutput sink factories (also re-exported from writetrack)
writetrack/browserESMBrowser bundle (same as core, explicit browser entry)
Import PathFormatDescription
writetrack/reactESMReact hook — useWriteTrack
writetrack/vueESMVue composable — useWriteTrack
Import PathFormatDescription
writetrack/tiptapESMTipTap extension — WriteTrackExtension
writetrack/ckeditorESMCKEditor 5 plugin — WriteTrackPlugin
writetrack/prosemirrorESMProseMirror plugin — WriteTrackPlugin
writetrack/quillESMQuill module — WriteTrackModule
writetrack/lexicalESMLexical integration — createWriteTrackLexical
writetrack/slateESMSlate integration — createWriteTrackSlate
writetrack/tinymceESMTinyMCE native plugin — side-effect import registers with PluginManager
Import PathFormatDescription
writetrack/vizESMSession visualization components
writetrack/testingESMTest factories — createMockAnalysis, createMockTracker
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 'writetrack/tinymce'; // registers TinyMCE plugin via side effect
import { verifyAnalysisSignatureAsync } from 'writetrack/verify';
import { WtScorecard } from 'writetrack/viz';
const { WriteTrack, webhook } = require('writetrack');

All types are exported from their respective entry points:

import type {
WriteTrackOptions,
WriteTrackDataSchema,
SessionAnalysis,
SessionReport,
IndicatorOutput,
KeystrokeEvent,
ClipboardEvent,
SelectionEvent,
UndoRedoEvent,
ProgrammaticInsertionEvent,
CompositionEvent,
InputSource,
WriteTrackSink,
WebhookOptions,
DatadogOptions,
SegmentOptions,
OpenTelemetryOptions,
} from 'writetrack';

Choosing between WriteTrack and analyzeEvents()

Section titled “Choosing between WriteTrack and analyzeEvents()”

WriteTrack has two entry points with distinct purposes. Pick one by where analysis runs.

new WriteTrack(options)analyzeEvents(data, options)
RunsClient-side, in the browserServer-side, in Node
InputAn HTMLElement to instrumentA captured WriteTrackDataSchema session
OutputContinuous capture + on-device analysis via getAnalysis() / getSessionReport()A single SessionAnalysis for the provided session
Options typeWriteTrackOptionstarget, license, event callbacks, persistence, editor hooksAnalyzeEventsOptionslicense, wasmUrl, errors behaviour
Use whenYou’re embedding the tracker in a page and want live signalsYou’ve already captured a session and want to analyze it off the client

Both shapes accept the license key on a field named license.

For the end-to-end path when the two meet (analyze on the server, render on the client), see Server → client → scorecard.

The main class for keystroke tracking and behavioral analysis.

new WriteTrack(optionsOrTarget: WriteTrackOptions | HTMLElement)

Creates a new WriteTrack instance. Accepts either a full options object or an HTMLElement directly for localhost evaluation.

ParameterTypeDescription
optionsOrTargetWriteTrackOptions | HTMLElementConfiguration options or target element
// 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(): 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
const tracker = new WriteTrack({ target: textarea });
tracker.start(); // Now recording
// User types...
const events = tracker.getRawEvents();
stop(): void

Stops recording keystrokes.

  • Disables event capture
  • Cleans up selection polling interval and mutation observer
  • Data remains available after stopping
tracker.stop();
// Data still accessible
const events = tracker.getRawEvents();
async stopAndWait(): Promise<void>

Stops recording and waits for any pending IndexedDB save to complete. Use this instead of stop() when you need to delete persisted data or navigate away immediately after stopping. When persist is not enabled, behaves identically to stop().

await tracker.stopAndWait();
await tracker.clearPersistedData(); // Safe — save already flushed
getData(): WriteTrackDataSchema

Returns the complete session as a structured WriteTrackDataSchema object, including metadata, all captured events, and computed quality metrics.

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),
});
Example return value
{
"version": "2.0.0",
"metadata": {
"tool": { "name": "writetrack", "version": "0.11.0" },
"targetElement": "textarea",
"timestamp": "2026-04-03T12:00:00.000Z",
"duration": 45000,
"userId": "user-123",
"contentId": "response-1",
"custom": { "assignmentId": "hw-5" },
"sessionId": "a1b2c3d4"
},
"session": {
"events": ["/* KeystrokeEvent[] */"],
"clipboardEvents": ["/* ClipboardEvent[] */"],
"rawText": "The user's final text...",
"sessionStartTime": 1743681600000,
"sessionEndTime": 1743681645000
},
"quality": {
"overallScore": 0.92,
"completeness": 98.5,
"sequenceValid": true,
"timingValid": true,
"sessionDuration": 45000,
"eventCount": 312,
"qualityLevel": "EXCELLENT",
"issues": [],
"validatedAt": "2026-04-03T12:00:45.000Z"
}
}

See WriteTrackDataSchema for full type definitions.

getText(): string

Returns the current text content of the target element. Reads value for input/textarea elements, or textContent for other elements.

getRawEvents(): KeystrokeEvent[]

Returns a copy of all captured keystroke events.

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(): ClipboardEvent[]

Returns a copy of all clipboard events (copy, paste, cut).

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(): SelectionEvent[]

Returns text selection events.

const selections = tracker.getSelectionEvents();
selections.forEach((e) => {
console.log(`Selected ${e.selectedLength} chars via ${e.method}`);
});
getUndoRedoEvents(): UndoRedoEvent[]

Returns undo/redo operation events.

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.

const insertions = tracker.getProgrammaticInsertionEvents();
insertions.forEach((e) => {
console.log(
`Programmatic insertion: "${e.insertedText}" (${e.insertedLength} chars)`
);
});
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.

const compositions = tracker.getCompositionEvents();
compositions.forEach((e) => {
console.log(`Composition: ${e.insertedLength} chars in ${e.duration}ms`);
});
getSessionDuration(): number

Returns the total session duration in milliseconds since start() was called, including time when the tab was hidden.

const duration = tracker.getSessionDuration();
console.log(`Session: ${(duration / 1000).toFixed(1)} seconds`);
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.

const active = tracker.getActiveTime();
const total = tracker.getSessionDuration();
console.log(
`Active: ${(active / 1000).toFixed(1)}s of ${(total / 1000).toFixed(1)}s total`
);
getKeystrokeCount(): number

Returns the total number of keydown events captured.

pipe(sink: WriteTrackSink): this

Registers an output sink to receive session data when getData() is called. Returns this for chaining.

import { WriteTrack, webhook } from 'writetrack';
tracker
.pipe(webhook({ url: 'https://api.example.com/typing' }))
.pipe({ send: async (data) => console.log(data) });

See Output Sinks for full documentation of the pipe system and available sinks.

tracker.on(event, handler, opts?);

Registers an event listener. Returns an unsubscribe function. The analysis event accepts the optional opts.throttle (milliseconds) to control update frequency (default 1000ms).

Supported events:

EventHandler signatureDescription
ready() => voidFires when tracker is ready (persistence loaded). Late listeners auto-fire.
change(data: { eventCount: number; keystrokeCount: number }) => voidFires on any data mutation (keystroke, paste, selection, etc.)
analysis(analysis: SessionAnalysis) => voidFires when analysis changes (1s default throttle). Lazy-loads WASM on first listener.
tick(data: { activeTime: number; totalTime: number; tracker: WriteTrack }) => voidFires every ~1s while session is active and tab is visible
wasm:ready() => voidFires when WASM binary loads successfully. Late listeners auto-fire.
wasm:error(error: Error) => voidFires if WASM binary fails to load
stop() => voidFires when stop() is called
pipe:error(err: Error, sink: WriteTrackSink) => voidFires when an output sink fails
// Unsubscribe pattern
const unsub = tracker.on('tick', ({ activeTime }) => {
console.log(`Active: ${Math.floor(activeTime / 1000)}s`);
});
// Later: unsub();
// Analysis updates (WASM loads automatically on first listener)
tracker.on('analysis', (analysis) => {
console.log(`Indicator: ${analysis.contentOrigin.indicator.code}`);
});
// Readiness
tracker.on('ready', () => {
console.log('Tracker ready');
});
tracker.off(event, handler);

Removes a specific event listener. The easier pattern is to call the unsubscribe function returned by on().

removeAllListeners(event?: string): void

Removes all listeners for a given event, or all listeners if no event is specified.

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(): boolean

Returns whether license validation has been performed.

if (tracker.isLicenseValidated() && tracker.isLicenseValid()) {
console.log('License valid, ready to use');
}
isTargetDetached(): boolean

Returns whether the monitored target element has been removed from the DOM. Useful for detecting SPA navigation that destroys the tracked element.

if (tracker.isTargetDetached()) {
console.warn('Target element removed — call stop() and create a new tracker');
}
ready: Promise<void>;

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.

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
async clearPersistedData(): Promise<void>

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.

await tracker.clearPersistedData();
async getAnalysis(): Promise<SessionAnalysis | null>

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.

For a combined bundle of session data and analysis in one call, use getSessionReport() instead.

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: { ... },
// ...
// }
async getSessionReport(): Promise<SessionReport>

Returns both the raw capture data and the WASM analysis in a single call.

This is a convenience method that combines getData() and getAnalysis() into a single SessionReport object. Use this when you need both the raw session data and the analysis together. For streaming data to external services, see output sinks (.pipe()) instead.

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

const tracker = new WriteTrack({ target: textarea });
console.log(tracker.wasmReady); // false
await tracker.getAnalysis();
console.log(tracker.wasmReady); // true (if WASM loaded)
static async listPersistedSessions(): Promise<PersistedSessionInfo[]>

Returns metadata for all persisted sessions stored in IndexedDB. Useful for multi-document apps that need to display or manage saved writing sessions.

const sessions = await WriteTrack.listPersistedSessions();
// [{ contentId: 'doc_1', fieldId: 'default', keystrokeCount: 42, lastSavedAt: 1709..., sessionId: '...' }]
static async deletePersistedSession(contentId: string, fieldId?: string): Promise<void>

Deletes persisted session data from IndexedDB. If fieldId is omitted, deletes all sessions for the given contentId.

// Delete all sessions for a document
await WriteTrack.deletePersistedSession('doc_1');
// Delete a specific field's session
await WriteTrack.deletePersistedSession('doc_1', 'response');
import { webhook, datadog, segment, opentelemetry } from 'writetrack';
ExportReturnsDescription
webhook(options)WriteTrackSinkPOST session data to a URL
datadog(options)WriteTrackSinkSend as Datadog RUM custom action
segment(options)WriteTrackSinkSend as Segment track event
opentelemetry(options)WriteTrackSinkSend as OpenTelemetry span

See Output Sinks for configuration options and usage guides.

import { analyzeEvents } from 'writetrack';
type AnalyzeEventsInput =
| WriteTrackDataSchema
| { data: WriteTrackDataSchema; analysis?: unknown };
async function analyzeEvents(
input: AnalyzeEventsInput,
options?: {
wasmUrl?: string;
license?: string;
errors?: 'return' | 'throw';
}
): Promise<SessionAnalysis | null>;

Analyze a WriteTrackDataSchema object without needing a live DOM element. Useful for server-side analysis of previously captured session data.

The input argument accepts either a raw WriteTrackDataSchema (the output of WriteTrack.getData()) or a wrapped { data, analysis? } report — the same shape createSessionReport() produces. Wrapped input is unwrapped automatically so stored reports can be re-analyzed without hand-unwrapping; any analysis field on the wrapper is ignored and the inner data is always re-analyzed.

// Server-side: analyze data received from the client
const data = await request.json(); // WriteTrackDataSchema from getData()
const analysis = await analyzeEvents(data, {
license: process.env.WRITETRACK_LICENSE_KEY,
});
// Or pass a stored SessionReport directly — analyzeEvents unwraps it
const storedReport = await db.loadReport(reportId); // { data, analysis }
const freshAnalysis = await analyzeEvents(storedReport, {
license: process.env.WRITETRACK_LICENSE_KEY,
});

By default analyzeEvents() returns null for every failure mode — missing license, invalid input, WASM load failure, or analysis validation failure — which makes diagnosis painful for batch callers. Pass errors: 'throw' to receive typed errors instead:

import {
analyzeEvents,
LicenseError,
InputValidationError,
WasmLoadError,
AnalysisError,
} from 'writetrack';
try {
const analysis = await analyzeEvents(data, {
license: process.env.WRITETRACK_LICENSE_KEY,
errors: 'throw',
});
} catch (e) {
if (e instanceof LicenseError) {
// WRITETRACK_LICENSE — missing, malformed, or unverifiable key
} else if (e instanceof InputValidationError) {
// WRITETRACK_INPUT — data shape is not a WriteTrackDataSchema
} else if (e instanceof WasmLoadError) {
// WRITETRACK_WASM — WASM failed to load; underlying error in `e.cause`
} else if (e instanceof AnalysisError) {
// WRITETRACK_ANALYSIS — analyzer output failed schema validation
} else {
throw e;
}
}

All four error classes extend the abstract WriteTrackError base class and expose a stable code field (WRITETRACK_LICENSE, WRITETRACK_INPUT, WRITETRACK_WASM, WRITETRACK_ANALYSIS) for log aggregation. WasmLoadError forwards the loader exception as cause when one is available.

The default (errors: 'return') is preserved for backward compatibility; a future major version will flip the default to 'throw'.

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.

const analysis = await tracker.getAnalysis();
if (analysis) {
const msg = formatIndicator(analysis.contentOrigin.indicator);
// e.g., "85% of text was pasted from external sources"
}
import { createSessionReport } from 'writetrack';
async function createSessionReport(
rawData: unknown,
options?: { wasmUrl?: string; license?: string }
): Promise<SessionReport>;

Produces a SessionReport ({ data, analysis }) ready for <wt-scorecard>.setData(). Accepts three input shapes:

  1. WriteTrackDataSchema (from getData()) — analyzed via analyzeEvents().
  2. Typing-sample format with rawEvents instead of session — normalized, then analyzed.
  3. Wrapped SessionReport ({ data, analysis }) — returned as-is. Use this when analysis has already run on the server. No re-analysis, no license key needed. If analysis is omitted, the inner data is analyzed.
const data = await request.json();
const report = await createSessionReport(data, {
license: process.env.WRITETRACK_LICENSE_KEY,
});
// report.data — normalized input data
// report.analysis — SessionAnalysis result

When analyzeEvents() runs on your server, you typically persist or return { data, analysis } together. On the client, pass that wrapped object straight to createSessionReport() — analysis is trusted and preserved:

// Server
const analysis = await analyzeEvents(data, {
license: process.env.WRITETRACK_LICENSE_KEY,
});
return Response.json({ data, analysis });
// Client
const wrapped = await fetch(`/sessions/${id}`).then((r) => r.json());
const report = await createSessionReport(wrapped);
document.querySelector('wt-scorecard').setData(report);

A framework-agnostic state management wrapper around WriteTrack. Used internally by the React and Vue hooks — exported for Svelte, Solid, vanilla JS, or any other framework.

import { WriteTrackController } from 'writetrack';
import type { ControllerOptions, ControllerState } from 'writetrack';
const controller = new WriteTrackController(options: ControllerOptions);
MethodReturnsDescription
start()voidStart capturing events
stop()voidStop capturing events
destroy()voidStop and clean up all subscriptions
reset()voidDestroy, then re-initialize with the same options
getState()ControllerStateGet the current state snapshot
PropertyTypeDefaultDescription
targetHTMLElementThe element to observe
writeTrackOptionsOmit<WriteTrackOptions, 'target'>Options forwarded to the WriteTrack constructor
autoStartbooleantrueStart capturing immediately after initialization
analysisbooleanfalseSubscribe to WASM analysis updates
onTick(data: OnTickData) => voidundefinedCalled on each tick (~1s) with timing data
onReady(tracker: WriteTrack) => voidundefinedCalled when the tracker is ready
onStateChange(state: ControllerState) => voidCalled whenever state changes (required)
PropertyTypeDescription
trackerWriteTrack | nullThe underlying tracker instance
isReadybooleanWhether the tracker is ready
isTrackingbooleanWhether capture is active
analysisSessionAnalysis | nullLatest analysis result
errorError | nullLatest error (e.g., WASM load failure)

The <wt-scorecard> custom element renders a full session analysis report. See the Scorecard page for complete usage guide.

AttributeTypeDefaultDescription
themestringauto"light" or "dark". Overrides system preference.
sharebooleanfalseShow share button and make QR clickable.
downloadbooleanfalseShow download button.
datastringundefinedJSON-serialized SessionReport.
compactbooleanfalseCompact rendering mode.
labelstringundefinedCustom aria-label.
MethodSignatureDescription
setData()setData(data: SessionReport): voidSet data and trigger re-render
getData()getData(): SessionReport | nullGet current data
register()static register(): voidRegister the custom element
EventDetailDescription
wt-share{ report: SessionReport }Share button/QR clicked
wt-download{ report: SessionReport }Download button clicked
wt-rendernoneRender complete

The <wt-badge> custom element renders a compact metric badge from a SessionAnalysis.

AttributeTypeDefaultDescription
datastringundefinedJSON-serialized SessionAnalysis.
themestringauto"light" or "dark". Overrides system preference.
compactbooleanfalseCompact rendering mode.
metricstringundefined'effort-ratio' | 'largest-paste' | 'corrections' | 'tab-aways'.
MethodSignatureDescription
setData()setData(data: SessionAnalysis): voidSet data and trigger re-render
destroy()destroy(): voidClean up resources
EventDetailDescription
wt-rendernoneRender complete

The <wt-origin-bar> custom element renders a horizontal stacked bar showing content origin breakdown from a SessionAnalysis.

AttributeTypeDefaultDescription
datastringundefinedJSON-serialized SessionAnalysis.
themestringauto"light" or "dark". Overrides system preference.
compactbooleanfalseCompact rendering mode.

Methods and events: same as WtBadge.

The <wt-document-growth> custom element renders the writing-process graph — document length over time with paste markers and phase shading. Accepts WriteTrackDataSchema from getData(), not SessionAnalysis. ProcessGraph is exported as an alias of DocumentGrowth for callers that prefer the process-oriented name.

import { ProcessGraph, extractProcessData } from 'writetrack/viz';
ProcessGraph.register();
document.querySelector('wt-document-growth').setData(tracker.getData());
AttributeTypeDefaultDescription
datastringundefinedJSON-serialized WriteTrackDataSchema.
themestringauto"light" or "dark". Overrides system preference.
compactbooleanfalseCompact rendering mode.

Methods and events: same as WtBadge.

The <wt-document-ribbon> custom element renders a document composition timeline. Accepts WriteTrackDataSchema from getData(), not SessionAnalysis.

AttributeTypeDefaultDescription
datastringundefinedJSON-serialized WriteTrackDataSchema.
themestringauto"light" or "dark". Overrides system preference.
compactbooleanfalseCompact rendering mode.

Methods and events: same as WtBadge.

The <wt-integrity-footer> custom element renders a footer bar with hash verification, event count, and session ID.

AttributeTypeDefaultDescription
datastringundefinedJSON-serialized IntegrityFooterData.
themestringauto"light" or "dark". Overrides system preference.
compactbooleanfalseCompact rendering mode.

Methods and events: same as WtBadge.

import { getSessionDurationMs } from 'writetrack';
function getSessionDurationMs(analysis: SessionAnalysis): number;

Extract the session duration in milliseconds from a SessionAnalysis.

import { getTypingSpeed } from 'writetrack';
function getTypingSpeed(analysis: SessionAnalysis): {
cpm: number;
wpm: number;
};

Compute characters-per-minute and words-per-minute from the keydown count and session duration in a SessionAnalysis. Returns { cpm: 0, wpm: 0 } when session duration is zero.

import { getContentOriginBreakdown } from 'writetrack';
function getContentOriginBreakdown(analysis: SessionAnalysis): {
typed: number;
pasted: number;
autocompleted: number;
};

Extract the content origin ratios (0-1 each) from a SessionAnalysis. These indicate what fraction of the final text was typed, pasted, or autocompleted.

const analysis = await tracker.getAnalysis();
if (analysis) {
const { wpm } = getTypingSpeed(analysis);
const { typed, pasted } = getContentOriginBreakdown(analysis);
console.log(`${wpm} WPM, ${Math.round(typed * 100)}% typed`);
}
import { createMockAnalysis, createMockTracker } from 'writetrack/testing';

Framework-agnostic factories for creating mock WriteTrack instances and SessionAnalysis objects in consumer tests. No vitest or jest dependency required.

function createMockAnalysis(
overrides?: DeepPartial<SessionAnalysis>
): SessionAnalysis;

Returns a complete SessionAnalysis with sensible defaults. Accepts deep-partial overrides that are merged into the defaults.

// Default analysis with realistic values
const analysis = createMockAnalysis();
// Override specific nested fields
const analysis = createMockAnalysis({
keydownCount: 500,
contentOrigin: {
metrics: { charactersByOrigin: { typed: 0.5, pasted: 0.5 } },
},
});
function createMockTracker(options?: MockTrackerOptions): MockWriteTrack;

Returns a mock object with the same public interface as WriteTrack. The mock supports on()/off()/removeAllListeners() event listeners, and exposes emit() for test-driven event firing.

const tracker = createMockTracker();
// With custom analysis
const tracker = createMockTracker({
analysis: createMockAnalysis({ keydownCount: 200 }),
});
// With null analysis (simulates WASM load failure)
const tracker = createMockTracker({ analysis: null });
// Test event flow
tracker.on('tick', (payload) => console.log(payload));
tracker.emit('tick', { activeTime: 5000, totalTime: 6000 });

Configuration options for the WriteTrack constructor.

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<string, unknown>; // Optional: Arbitrary tags (appears as metadata.custom)
captureContext?: string; // Optional: Label for where in your app the tracker runs (appears as metadata.captureContext)
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.
}

Metadata about a persisted writing session, returned by listPersistedSessions().

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
}

The complete session output returned by getData().

interface WriteTrackDataSchema {
version: '2.1.0';
metadata: SessionMetadata;
session: RawEventData;
quality: DataQualityMetrics;
unlicensed?: boolean;
}

Session metadata included in the export.

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<string, unknown>; // From WriteTrackOptions.metadata
}

Quality assessment of the captured session.

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
}

Individual keystroke event data.

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
}

Clipboard operation event data.

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
}

Text selection event data.

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
}

Programmatic insertion event data.

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
}

Undo/redo operation event data.

interface UndoRedoEvent {
type: 'undo' | 'redo';
timestamp: number;
shortcut: string; // e.g., 'Ctrl+Z'
beforeText: string;
afterText: string;
}

IME composition event data. Emitted when a CJK input method or dead-key sequence completes.

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
}

The WASM analysis output returned by getAnalysis(). Contains indicator codes and rich metrics for each analysis dimension.

interface SessionAnalysis {
version: string;
locale: string;
analyzedAt: number;
sufficientData: boolean;
licensed: 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 and signedPayload fields are used internally for integrity checking.

Writing process stage classification returned in SessionAnalysis.writingProcess.

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
}

Combined output of getSessionReport().

interface SessionReport {
data: WriteTrackDataSchema;
analysis: SessionAnalysis | null;
}

Machine-readable indicator returned by each analysis dimension.

interface IndicatorOutput {
code: string; // WT-NNN code (e.g., "WT-101")
params: Record<string, number>; // Numeric parameters (e.g., { pct: 85 })
}

Use formatIndicator() to convert to a human-readable English string.