npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

writetrack

v0.12.1

Published

Lightweight keystroke dynamics capture for web applications

Readme

WriteTrack

Capture and analyze the writing process through keystroke dynamics.

WriteTrack is an embeddable SDK that instruments text inputs to record how text was written — timing, rhythm, corrections, clipboard usage, and composition patterns. It gives platforms behavioral evidence of the writing process, not just the finished text.

  • ~142KB gzipped (20KB JS + 122KB WASM)
  • 0 runtime dependencies
  • <1ms per keystroke
  • 100% client-side

Installation

npm install writetrack

Optionally, register for a free trial license:

npx writetrack init  # starts a 28-day trial and writes the key to .env

A license is required for production domains. On localhost, all features work without a license key.

Quick Start

import { WriteTrack } from 'writetrack';

const responseField = document.getElementById('response-field')!;
const tracker = new WriteTrack({
  target: responseField,
});

tracker.start();

// User types...

// Collect captured data
const data = tracker.getData();
const analysis = await tracker.getAnalysis();

tracker.stop();

Works with any text input, textarea, or contenteditable element. TypeScript definitions included.

Note: WriteTrack uses WASM, which requires an HTTP server — file:// won't work. Any bundler (Vite, webpack, etc.) handles this automatically. For a no-bundler setup, see the vanilla JS guide.

React / Next.js

import { useRef } from 'react';
import { useWriteTrack } from 'writetrack/react';

function ResponseForm() {
  const textareaRef = useRef<HTMLTextAreaElement>(null);
  const { tracker } = useWriteTrack(textareaRef);

  const handleSubmit = async (e: React.FormEvent) => {
    e.preventDefault();
    if (tracker) {
      const data = tracker.getData();
      const analysis = await tracker.getAnalysis();
      console.log('Session:', data.quality, analysis);
    }
  };

  return (
    <form onSubmit={handleSubmit}>
      <textarea ref={textareaRef} rows={10} />
      <button type="submit">Submit</button>
    </form>
  );
}

For App Router, Server Actions, and dev-mode WASM setup, see the Next.js guide.

Production

Add your license key to enable analysis on production domains:

const tracker = new WriteTrack({
  target: textarea,
  license: process.env.WRITETRACK_LICENSE_KEY,
});

On localhost, all features work without a key. See the licensing guide for trial setup, domain binding, and expiration details.

Framework Integrations

First-party bindings available as subpath exports:

import { useWriteTrack } from 'writetrack/react';
import { useWriteTrack } from 'writetrack/vue';
import { WriteTrackExtension } from 'writetrack/tiptap';
import { WriteTrackPlugin } from 'writetrack/ckeditor';
import { WriteTrackPlugin } from 'writetrack/prosemirror';
import { WriteTrackModule } from 'writetrack/quill';
import { createWriteTrackLexical } from 'writetrack/lexical';
import { createWriteTrackSlate } from 'writetrack/slate';
import 'writetrack/tinymce'; // registers native TinyMCE plugin

Output Sinks

Stream session data to your analytics stack via .pipe():

import { webhook, datadog, segment, opentelemetry } from 'writetrack/pipes';

tracker
  .pipe(webhook({ url: '/api/writetrack' }))
  .pipe(datadog({ client: dd }))
  .pipe(segment({ client: analytics }))
  .pipe(opentelemetry({ tracer }));

What It Captures

WriteTrack instruments text inputs and records the behavioral signals behind every keystroke:

  • Timing intervals — milliseconds between each keystroke
  • Rhythm variance — consistency or erratic cadence
  • Correction patterns — backspaces, rewrites, hesitation
  • Clipboard events — paste, copy, cut with content and context
  • Selection events — text selections via mouse, keyboard, or programmatic
  • Composition events — IME input for CJK and other non-Latin scripts

What It Produces

WASM-powered analysis returns a SessionAnalysis covering seven dimensions:

  • Content origin — what proportion was typed, pasted, or autocompleted
  • Timing authenticity — rhythm consistency, periodicity, entropy
  • Revision behavior — corrections, navigation, undo/redo patterns
  • Session continuity — focus changes, tab-aways, session structure
  • Physical plausibility — impossible sequences, synthetic event markers
  • Temporal patterns — speed over time, warmup/fatigue, burst patterns
  • Writing process — planning, drafting, and revision phase segmentation

Each dimension produces a machine-readable indicator code and detailed metrics suitable for visualization.

Use Cases

Education — Give instructors behavioral context alongside submitted work. See whether text was composed or pasted.

Assessment — Add a behavioral layer to written responses. Distinguish engaged composition from copy-paste submission.

Compliance — Know when form attestations were typed versus pasted.

Research — Capture writing process data for studies on composition behavior, revision patterns, and typing fluency.

API

Constructor

new WriteTrack(options: WriteTrackOptions | HTMLElement)

Options

interface WriteTrackOptions {
  target: HTMLElement;
  license?: string; // Omit for localhost evaluation
  userId?: string; // Opaque user identifier, included in output metadata
  contentId?: string; // Opaque document identifier, included in output metadata
  metadata?: Record<string, unknown>; // Arbitrary metadata, included in output
  wasmUrl?: string; // Custom URL for the WASM analysis binary
  persist?: boolean; // Enable IndexedDB persistence (requires contentId)
  cursorPositionProvider?: () => number; // Custom cursor position for rich-text editors
  inputSourceProvider?: () => InputSource | undefined; // Input source classification (set by editor integrations)
}

Methods

| Method | Returns | Description | | ---------------------------------- | -------------------------------- | --------------------------------------------- | | start() | void | Begin capturing events | | stop() | void | Stop capturing and clean up | | stopAndWait() | Promise<void> | Stop and wait for pending IndexedDB save | | getData() | WriteTrackDataSchema | Export full session data | | getText() | string | Current text content | | getRawEvents() | KeystrokeEvent[] | All captured keystroke events | | getClipboardEvents() | ClipboardEvent[] | Paste/copy/cut events | | getSelectionEvents() | SelectionEvent[] | Text selection events | | getUndoRedoEvents() | UndoRedoEvent[] | Undo/redo events | | getCompositionEvents() | CompositionEvent[] | IME composition events | | getProgrammaticInsertionEvents() | ProgrammaticInsertionEvent[] | Programmatic insertion events | | getSessionDuration() | number | Session duration in ms | | getActiveTime() | number | Active (visible) session time in ms | | getKeystrokeCount() | number | Total keystrokes captured | | getAnalysis() | Promise<SessionAnalysis\|null> | WASM-powered session analysis | | getSessionReport() | Promise<SessionReport> | Combined data + analysis | | pipe(sink) | this | Register an output sink | | on(event, handler) | () => void | Register event listener (returns unsubscribe) | | isLicenseValid() | boolean | Whether license is valid | | isLicenseValidated() | boolean | Whether license check has completed | | isTargetDetached() | boolean | Whether the target element was removed | | clearPersistedData() | Promise<void> | Clear IndexedDB persisted session |

getData() returns a WriteTrackDataSchema with four top-level fields: version, metadata (session context, user/content IDs, custom fields), session (raw events and text), and quality (completeness score, validation status). See the API reference for the full shape.

Events

The on(event, handler) method supports the following events:

| Event | Payload | Description | | ------------ | ---------------------------------------------------------------- | --------------------------------------------------------------- | | change | { eventCount: number; keystrokeCount: number } | Fires on any data mutation (keystroke, paste, selection, etc.) | | tick | { activeTime: number; totalTime: number; tracker: WriteTrack } | Fires every ~1s while session is active | | analysis | SessionAnalysis | Fires when analysis updates (lazy-loads WASM on first listener) | | ready | (none) | Fires when tracker is ready (late listeners auto-fire) | | wasm:ready | (none) | Fires when WASM binary loads successfully | | wasm:error | Error | Fires if WASM binary fails to load | | stop | (none) | Fires when stop() is called | | pipe:error | Error, WriteTrackSink | Fires when an output sink fails |

const unsub = tracker.on('change', ({ eventCount, keystrokeCount }) => {
  console.log(`${keystrokeCount} keystrokes, ${eventCount} total events`);
});
// Later: unsub();

Static Methods

| Method | Returns | Description | | ------------------------------------------------ | --------------------------------- | ---------------------------------------- | | WriteTrack.listPersistedSessions() | Promise<PersistedSessionInfo[]> | List all persisted sessions in IndexedDB | | WriteTrack.deletePersistedSession(key, field?) | Promise<void> | Delete persisted session data |

Properties

| Property | Type | Description | | ----------- | --------------- | ------------------------------------------- | | wasmReady | boolean | Whether WASM module is loaded (getter) | | ready | Promise<void> | Resolves when persisted session is restored |

Server-Side Analysis

Analyze previously captured data without a DOM element:

import { analyzeEvents } from 'writetrack';

const data = JSON.parse(savedSessionJson); // from your API, database, etc.
const analysis = await analyzeEvents(data, {
  licenseKey: process.env.WRITETRACK_LICENSE_KEY,
});

Analysis Helpers

Convenience functions for extracting commonly needed values from a SessionAnalysis:

| Function | Returns | Description | | ------------------------------------- | ---------------------------------------------------------- | -------------------------------- | | getSessionDurationMs(analysis) | number | Session duration in milliseconds | | getTypingSpeed(analysis) | { cpm: number; wpm: number } | Characters and words per minute | | getContentOriginBreakdown(analysis) | { typed: number; pasted: number; autocompleted: number } | Content origin ratios (0-1 each) |

import { getTypingSpeed, getContentOriginBreakdown } from 'writetrack';

const analysis = await tracker.getAnalysis();
if (analysis) {
  const { wpm } = getTypingSpeed(analysis);
  const { typed, pasted } = getContentOriginBreakdown(analysis);
}

Testing Utilities

The writetrack/testing subpath export provides framework-agnostic factories for creating mock WriteTrack instances and SessionAnalysis objects in consumer tests:

import { createMockAnalysis, createMockTracker } from 'writetrack/testing';

const analysis = createMockAnalysis({ keydownCount: 500 });
const tracker = createMockTracker({ analysis });

| Function | Returns | Description | | -------------------------------- | ----------------- | -------------------------------------------------- | | createMockAnalysis(overrides?) | SessionAnalysis | Complete mock analysis with deep-partial overrides | | createMockTracker(options?) | MockWriteTrack | Mock tracker with configurable analysis and data |

Exported Types

Full type definitions are included in the package:

Functions: analyzeEvents, createSessionReport, formatIndicator, getHighResolutionTime, WriteTrackController

Types: WriteTrackOptions, PersistedSessionInfo, KeystrokeEvent, ClipboardEvent, SelectionEvent, UndoRedoEvent, CompositionEvent, ProgrammaticInsertionEvent, ModifierState, InputSource, WriteTrackDataSchema, DataQualityMetrics, SessionMetadata, SessionAnalysis, SessionReport, IndicatorOutput, ContentOriginAnalysis, TimingAuthenticityAnalysis, SessionContinuityAnalysis, PhysicalPlausibilityAnalysis, RevisionBehaviorAnalysis, TemporalPatternsAnalysis, WritingProcessAnalysis, WritingProcessSegment, PhaseTransition, WindowFeatures, AnalyzeEventsOptions, WriteTrackSink, WebhookOptions, DatadogOptions, DatadogClient, SegmentOptions, SegmentClient, OpenTelemetryOptions, OTelTracer, OTelSpan, BaseBindingOptions, OnTickData, ControllerState, ControllerOptions

Browser Support

| Browser | Version | | ------- | ------- | | Chrome | 90+ | | Firefox | 88+ | | Safari | 14+ | | Edge | 90+ |

Privacy

WriteTrack runs entirely client-side. No servers, no tracking, no external requests. Event data (including paste content and selected text) stays in the browser unless you explicitly export it via getData() or an output sink.

Documentation

Full documentation at writetrack.dev/docs.

License

See LICENSE for details.