Quick Start
WriteTrack records how text enters a field and produces metrics you can store, query and display. These two submissions contain similar text, but one was written in the form and the other was pasted:
| Metric | Written in the form | Pasted in |
|---|---|---|
| Effort ratio | 1.01× | 0.06× |
| Pasted | 0% | 95% |
| Largest single paste | 0% | 95% of the final text |
| Corrections | 6 | 13 |
| Active writing time | 21s | 15s |
These metrics describe the capture session. They do not determine authorship; the pasted submission might come from a careful writer who drafts elsewhere.
The examples below run on localhost without a license key or account.
Pick your starting point
Section titled “Pick your starting point”- A new project — see the numbers in about two minutes.
- Existing app — add capture to an existing form or editor and store the result.
- Existing sessions — analyse stored data, on a server or in a review page.
A new project
Section titled “A new project”Install the SDK:
npm i writetrackpnpm add writetrackyarn add writetrackbun add writetrackPoint it at a text field, type into it, and read the numbers:
<textarea id="essay" rows="10"></textarea><button id="done">I'm finished</button>
<script type="module"> import { WriteTrack, summarizeAnalysis } from 'writetrack';
const tracker = new WriteTrack({ target: document.querySelector('#essay') }); tracker.start();
document.querySelector('#done').addEventListener('click', async () => { tracker.stop(); const { analysis } = await tracker.getSessionReport();
if (!analysis) { console.error( 'Analysis unavailable — check the preceding console warning.' ); return; }
console.log(summarizeAnalysis(analysis)); // { effortRatio: 1.01, typedPct: 100, pastedPct: 0, largestPastePct: null, // corrections: 6, tabAways: 0, activeWritingMs: 21385, ... } });</script>Type a sentence and click the button. The result should show an effort ratio
near 1 and pasted near 0%. Reload the page, paste a paragraph into the
field, and click again. The second result should show a lower effort ratio and a
higher pasted percentage. Reloading starts a new capture session after the
button stops the previous one.
getSessionReport() returns { data, analysis }: data contains the capture
and analysis contains the computed metrics. Both are plain JSON.
Existing app
Section titled “Existing app”The usual shape: capture in the form, send the report with the submission, review it later.
1. Capture. Attach the tracker to the field you already have. Rich-text editors use dedicated integrations for TipTap, CKEditor, ProseMirror, Quill, Lexical, Slate and TinyMCE. React and Vue have hooks; Next.js has its own guide.
import { WriteTrack } from 'writetrack';
const tracker = new WriteTrack({ target: document.querySelector<HTMLTextAreaElement>('#statement')!, userId: 'candidate-42', // optional, flows through to the output contentId: 'essay-q3',});tracker.start();2. Send it with the submission. One extra field on the request you already make:
form.addEventListener('submit', async (event) => { event.preventDefault(); tracker.stop();
await fetch('/api/submissions', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ text: textarea.value, report: await tracker.getSessionReport(), }), });});3. Store it, and pull out the columns you want to sort by. The report is JSON, so any database that handles JSON will do:
ALTER TABLE submissions ADD COLUMN report JSON NOT NULL;ALTER TABLE submissions ADD COLUMN effort_ratio REAL;ALTER TABLE submissions ADD COLUMN pasted_pct REAL;import { summarizeAnalysis } from 'writetrack';
if (!report.analysis) { throw new Error('Cannot summarize a report without analysis');}const summary = summarizeAnalysis(report.analysis);
await db.submissions.update(id, { report, effort_ratio: summary.effortRatio, pasted_pct: summary.pastedPct, largest_paste_pct: summary.largestPastePct, corrections: summary.corrections, active_writing_ms: summary.activeWritingMs,});summarizeAnalysis() performs arithmetic over the analysis and runs in the
browser or on a server. The scorecard’s badges use the same function, keeping
stored columns consistent with the UI.
Capture and analysis run in the user’s browser. Your application controls where the report is sent.
Existing sessions
Section titled “Existing sessions”For reports already stored in your database, createSessionReport() returns a
report you can render or re-analyse:
import { createSessionReport } from 'writetrack';
const stored = await db.getReport(submissionId); // { data, analysis }const report = await createSessionReport(stored);A stored { data, analysis } report is accepted as supplied.
createSessionReport() preserves the existing analysis without re-running,
authenticating or verifying it. If a report crossed an untrusted boundary, use
verifyAnalysisSignatureAsync()
before relying on its signed fields. Preserving the existing analysis keeps the
original session record available for later review.
To analyse raw captured data server-side instead, use
analyzeEvents() with a license key.
Results for reviewers
Section titled “Results for reviewers”The scorecard renders a whole session with a plain-language summary, a typed/pasted bar and seven expandable categories:
npm i @observablehq/plotpnpm add @observablehq/plotyarn add @observablehq/plotbun add @observablehq/plotimport { WtScorecard } from 'writetrack/viz';
WtScorecard.register();document.querySelector<WtScorecard>('wt-scorecard')!.setData(report);<wt-scorecard></wt-scorecard>Observable Plot is a peer dependency for charts. Individual charts are also available and accept raw session data directly: see Visualization.
Going to production
Section titled “Going to production”Analysis is available on localhost during development. To register a
production domain:
npx writetrack initThat registers a free 28-day trial and writes the key to .env. Treat the key
as public client configuration bound to a domain. Expose it through your
framework’s public/client environment-variable mechanism, then pass the value
into the browser code:
function createTracker(target: HTMLElement, licenseKey: string) { return new WriteTrack({ target, license: licenseKey });}License state controls analysis. start() and getData() remain available in
every license state. See Licensing for domain binding and
expiry.
Next steps
Section titled “Next steps”- Analysis — what the seven categories measure
- Session Report — the
{ data, analysis }shape, and how to store it - Data Schema — everything capture records
- Editor integrations — TipTap, CKEditor, Lexical, TinyMCE and more
- API Reference — full method and type reference