Vanilla JavaScript
WriteTrack works with any HTML page — no framework required. Attach it to a <textarea>, <input>, or contenteditable element and start capturing.
Installation
Section titled “Installation”npm i writetrackpnpm add writetrackyarn add writetrackbun add writetrackImport Map (no bundler)
Section titled “Import Map (no bundler)”If you’re loading WriteTrack without a bundler, add an import map so bare specifiers resolve correctly:
<script type="importmap"> { "imports": { "writetrack": "./node_modules/writetrack/dist/browser/index.js", "writetrack/viz": "./node_modules/writetrack/dist/browser/viz.js", "writetrack/pipes": "./node_modules/writetrack/dist/browser/pipes.js" } }</script>License
Section titled “License”On localhost, WriteTrack runs with full analysis enabled — no license key required. For production, register a free trial:
npx writetrack initThe CLI prompts for your email and domain, then saves the key to .env. Pass it when initializing:
const tracker = new WriteTrack({ target: textarea, license: 'your-license-key',});See Licensing for full details.
Basic Usage
Section titled “Basic Usage”Attach WriteTrack to any text input and call getData() on submit:
import { WriteTrack } from 'writetrack';
const textarea = document.querySelector<HTMLTextAreaElement>('#response-field')!;const tracker = new WriteTrack({ target: textarea });tracker.start();
document.getElementById('response-form')!.addEventListener('submit', (e) => { e.preventDefault(); const data = tracker.getData(); console.log('Session quality:', data.quality.qualityLevel); tracker.stop();});With Analysis
Section titled “With Analysis”Run the WASM analysis engine to assess typing authenticity:
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(formatIndicator(analysis.contentOrigin.indicator)); // → "Predominantly typed directly"
console.log(formatIndicator(analysis.timingAuthenticity.indicator)); // → "Timing variability is within normal range"}Or bundle raw data and analysis into a single payload with getSessionReport():
const report = await tracker.getSessionReport();// report.data — full session data (same as getData()), events at .session.events// report.analysis — authenticity analysis (same as getAnalysis())
await fetch('/api/submit', { method: 'POST', body: JSON.stringify(report),});With Output Sinks
Section titled “With Output Sinks”Use .pipe() to route session data to your backend automatically when getData() is called:
import { WriteTrack } from 'writetrack';import { webhook } from 'writetrack/pipes';
const tracker = new WriteTrack({ target: document.querySelector('#response-field')!,});
tracker.pipe(webhook({ url: '/api/writetrack' }));tracker.start();WriteTrack also ships sinks for Datadog, Segment, and OpenTelemetry. See Output Sinks for details.
Contenteditable Elements
Section titled “Contenteditable Elements”WriteTrack works with contenteditable elements the same way as <textarea> and <input>:
const editor = document.querySelector('[contenteditable]')!;const tracker = new WriteTrack({ target: editor });tracker.start();Three things to know:
-
Text extraction — WriteTrack reads
.valuefor inputs/textareas and.innerTextfor contenteditable elements. This happens automatically. -
Cursor position — For basic contenteditable elements, WriteTrack derives cursor position from the DOM Selection API. If your editor uses a custom document model, use
cursorPositionProviderto return the model’s offset. (If you’re using a supported editor like TipTap, CKEditor, Lexical, Slate, or TinyMCE, use the dedicated integration instead.) -
Rich text — Formatting changes (bold, italic, etc.) that don’t alter text content are invisible to WriteTrack.
Multiple Fields
Section titled “Multiple Fields”Track different fields independently:
const titleTracker = new WriteTrack({ target: document.querySelector('#title')!, contentId: 'title',});
const bodyTracker = new WriteTrack({ target: document.querySelector('#body')!, contentId: 'body',});
titleTracker.start();bodyTracker.start();Context Fields
Section titled “Context Fields”Tag sessions with identifiers that flow through to getData() output and all sinks:
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<string, unknown> | Arbitrary key-value pairs. Appears as metadata.custom. |
In the output from getData(), these fields appear in metadata:
{ "metadata": { "userId": "student-42", "contentId": "essay-prompt-3", "custom": { "assignmentId": "hw-5", "courseId": "CS101" } }}Note: the metadata option becomes custom in the output to avoid collision with system-generated metadata fields.
Type Reference
Section titled “Type Reference”If you’re using a bundler, import types directly:
import type { SessionAnalysis, IndicatorOutput } from 'writetrack';Without a bundler, you can still get IDE autocomplete via JSDoc:
/** @type {import('writetrack').WriteTrack} */const tracker = new WriteTrack({ target: textarea });
/** @type {import('writetrack').SessionAnalysis | null} */const analysis = await tracker.getAnalysis();For reference, here are the key shapes returned by getAnalysis() and getData():
// getAnalysis() → SessionAnalysis// Seven categories, each with .indicator and .metrics:// contentOrigin, timingAuthenticity, sessionContinuity,// physicalPlausibility, revisionBehavior, temporalPatterns, writingProcessanalysis.contentOrigin.indicator; // → { code: 'WT-100', params: { ... } }formatIndicator(indicator); // → 'Predominantly typed directly'
// getData() → WriteTrackDataSchemadata.version; // '2.0.0'data.metadata.sessionId; // 'wt_a1b2c3d4e5f6'data.session.events; // KeystrokeEvent[]data.session.clipboardEvents; // ClipboardEvent[]data.quality.qualityLevel; // 'good' | 'acceptable' | 'low'See API Reference for the complete type definitions.
Next Steps
Section titled “Next Steps”- Analysis — Deep dive on the seven analysis categories
- Session Persistence — Save and resume sessions across page reloads
- Scorecard — Render a full visual analysis report
- Output Sinks — Route data to webhooks, Datadog, Segment, or OpenTelemetry
- API Reference — Complete method and type reference