Skip to content

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:

MetricWritten in the formPasted in
Effort ratio1.01×0.06×
Pasted0%95%
Largest single paste0%95% of the final text
Corrections613
Active writing time21s15s

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.

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

Install the SDK:

Terminal window
npm i writetrack

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

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.

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.

The scorecard renders a whole session with a plain-language summary, a typed/pasted bar and seven expandable categories:

Terminal window
npm i @observablehq/plot
import { 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.

Analysis is available on localhost during development. To register a production domain:

Terminal window
npx writetrack init

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