Skip to content

Vanilla JavaScript

WriteTrack works with any HTML page — no framework required. Attach it to a <textarea>, <input>, or contenteditable element and start capturing.

Terminal window
npm i writetrack

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>

On localhost, WriteTrack runs with full analysis enabled — no license key required. For production, register a free trial:

Terminal window
npx writetrack init

The 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.

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

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

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.

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:

  1. Text extraction — WriteTrack reads .value for inputs/textareas and .innerText for contenteditable elements. This happens automatically.

  2. Cursor position — For basic contenteditable elements, WriteTrack derives cursor position from the DOM Selection API. If your editor uses a custom document model, use cursorPositionProvider to return the model’s offset. (If you’re using a supported editor like TipTap, CKEditor, Lexical, Slate, or TinyMCE, use the dedicated integration instead.)

  3. Rich text — Formatting changes (bold, italic, etc.) that don’t alter text content are invisible to WriteTrack.

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();

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' },
});
FieldTypeDescription
userIdstringWho is typing. Appears in metadata.userId.
contentIdstringWhat they’re typing into. Appears in metadata.contentId.
metadataRecord<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.

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, writingProcess
analysis.contentOrigin.indicator; // → { code: 'WT-100', params: { ... } }
formatIndicator(indicator); // → 'Predominantly typed directly'
// getData() → WriteTrackDataSchema
data.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.

  • 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