Text Monitor Package
The Text Monitor package captures typing behavior on text fields and editors: keystroke timing, pauses, paste events, and deletions. Output is a per-session composition classification (Imported, Single Drafted, Substituted, Quoted, Original). This page covers the standalone @isnotai/text-monitor package for sites that do not run the NotAI pixel. If the pixel is already installed, enable text monitoring there instead (see the pixel install guide).
Overview
The package ships in two shapes that share the same capture engine:
- npm package with programmatic control. Use this when editors mount and unmount dynamically, or when you want to attach and detach at specific lifecycle points.
- CDN
/auto.jsscript for a declarative drop-in install. Use this when a single script tag is enough and you do not need runtime control.
Both shapes produce the same output and use the same configuration keys. Pick whichever fits your stack.
When to Use This
Use the standalone package if:
- Your site does not run the NotAI pixel.
- You want text monitoring on a specific subset of pages or fields.
- You need programmatic control over attach and detach timing (for example, editors that mount and unmount during a single-page-app navigation).
Use the pixel-bundled path instead if:
- You are already running the NotAI pixel.
- You want a one-line script install with no runtime code.
- You want RCE auto-detection.
Do not load both the pixel and the standalone package on the same page.
Install
Install from npm:
npm install @isnotai/text-monitor
yarn add @isnotai/text-monitor
pnpm add @isnotai/text-monitor
Or use the CDN /auto.js entry (see Declarative Install below).
Programmatic Usage
monitor.attach() is now asynchronous and returns a per-attachment handle object. The top-level monitor.detach(element) has been removed — call handle.detach() on the value returned by attach() instead. The v1.0 surface is required for any integration that uses Canvas lookup-or-create, session resume, or beacon-on-unload. v0.1.x sites that already shipped with the synchronous attach/detach pair must update before pinning a newer version of @isnotai/text-monitor.
Create a monitor with your integration ID and region, then await attach() to start a writing session on a supported text field. Call handle.detach() when the field unmounts.
import { createMonitor } from '@isnotai/text-monitor';
const monitor = createMonitor({
integrationId: 'a7f3c2e1',
region: 'us'
});
// Open a writing session bound to this element.
// `attach` is async — it round-trips to the capture server to mint a session ID.
const handle = await monitor.attach(document.getElementById('essay'));
// `handle.writingSessionId` is the 32-hex server-issued ID. Persist it
// (hidden form field, URL, etc.) if you want to resume after a reload.
console.log(handle.writingSessionId);
// Later, when the field unmounts or the user submits:
await handle.detach({ reason: 'submitted' });
attach() rejects with an Error if the capture server is unreachable past its retry budget. Wrap in try/catch if you want to handle that case visibly; otherwise the rejection propagates. Attaching the same element twice returns the existing handle (idempotent).
Resuming a writing session
To re-attach to a previously-opened session (for example, after a page reload), pass opts.resume with the prior writing-session ID:
const prior = sessionStorage.getItem('wsid');
const handle = await monitor.attach(el, prior ? { resume: prior } : undefined);
if (handle.chainEvent) {
// Server issued a NEW id because the prior session was closed or tampered.
// handle.chainEvent.predecessorId points back to the old session.
}
Options
| Option | Type | Required | Description |
|---|---|---|---|
integrationId |
string | Yes | 8-character hex, from your dashboard. See Integration ID. |
region |
string | Yes | 'us' or 'eu'. Must match the region on your account. See Regions. |
correlationId |
string | No | Opaque caller-supplied tag (up to 256 chars) attached to every writing session this monitor opens. Useful for tying your own row IDs / submission IDs to the captured session. |
autoBeaconClose |
boolean | No | Default true. Auto-closes active writing sessions on pagehide / beforeunload so unload paths flush a final batch. |
extraSelectors |
string[] | No | CSS selectors for additional custom editors beyond the standard set. |
attach() options
| Option | Type | Description |
|---|---|---|
resume |
string | Prior writing-session ID (32-hex). When present, the server resumes the session or chains forward if it was closed. |
watchdogMs |
number | null | Idle-timeout override. Default 30 minutes; pass null to disable. Auto-closes with reason: 'watchdog' after this much inactivity on the field. |
observeRemoval |
boolean | Opt-in. When true, a MutationObserver auto-detaches if the attached element is removed from the DOM. |
Declarative Install (auto.js)
For a single-script install, drop the /auto.js script tag into your page. It reads its configuration from a data-config attribute and attaches to supported editors on load.
<script
src="https://cdn.isnotai.com/text-monitor/auto.js"
data-config='{"integrationId":"a7f3c2e1","region":"us"}'
async></script>
For custom editors not covered by the default selectors, pass extraSelectors:
<script
src="https://cdn.isnotai.com/text-monitor/auto.js"
data-config='{"integrationId":"a7f3c2e1","region":"us","extraSelectors":[".my-editor"]}'
async></script>
Runtime attach and detach are not available through /auto.js. If you need programmatic control, use the npm package.
Self-hosted Script
Some customers need the text monitor to be served from their own origin so it survives aggressive adblocker lists that target well-known third-party domains. For those cases we issue a pre-packaged bundle with your integration ID, region, and reporting endpoints baked in, which you host on a subdomain of your own site.
The setup is self-service from your dashboard:
- In your dashboard, open Settings → Self-hosted Script. You will see the CNAME targets your package needs.
- Create the CNAME records on your own subdomain (for example,
verify.example.com). The dashboard verifies propagation once the records are live. - Download your packaged bundle. It is built with your integration ID, region, and CNAMEd reporting hosts baked in.
- Host the file on your own CDN. Your install tag becomes a single script with no
data-*attributes:
<script src="//your-cdn.example.com/text-monitor.js" async></script>
The same packaged bundle works for programmatic control: import its createMonitor export from the local copy instead of the public npm package.
When you need to change configuration, return to the dashboard and download a fresh package. There is no runtime config for the self-hosted build; everything lives in the file you host.
Supported Editors
The package includes adapters for the following editor surfaces. Adapter selection is automatic: attach to any container and the package dispatches to the right adapter.
<textarea><input type="text">- Any element with
contenteditable="true" - Quill editor instances
- TinyMCE editor instances
If you use a different editor, pass its root container selector via extraSelectors. The package falls back to universal content diffing for anything that is not one of the specialized adapters above.
Integration ID
Every NotAI deployment is identified by an 8-character hexadecimal integration ID (for example a7f3c2e1). Copy yours from Integrations in your NotAI dashboard.
The integration ID is public by design: it identifies which account receives the session data. It does not authenticate the sender, and a leaked integration ID does not expose account data.
Regions
NotAI operates two data regions: US and EU. Pick the region that matches your account.
- US accounts: set
region: 'us'(the default). - EU accounts: set
region: 'eu'. Your site must also allow the EU reporting hosts in its Content Security Policy (see below).
A session sent to the wrong region will be rejected.
React Usage
Open the writing session inside a useEffect hook and store the returned handle in a ref so the cleanup function can close it. Use an active flag to ignore the resolved handle if the effect unmounts mid-flight.
import { useEffect, useRef } from 'react';
import { createMonitor } from '@isnotai/text-monitor';
const monitor = createMonitor({
integrationId: 'a7f3c2e1',
region: 'us'
});
function EssayForm() {
const ref = useRef(null);
const handleRef = useRef(null);
useEffect(() => {
if (!ref.current) return;
let active = true;
monitor.attach(ref.current).then((h) => {
if (!active) { h.detach({ reason: 'detach' }); return; }
handleRef.current = h;
});
return () => {
active = false;
handleRef.current?.detach({ reason: 'detach' });
handleRef.current = null;
};
}, []);
return <textarea ref={ref} />;
}
Create the monitor once at module scope, not on every render.
Vue Usage
Await attach in onMounted, store the returned handle in a non-reactive ref, and call handle.detach in onUnmounted.
<script setup>
import { ref, shallowRef, onMounted, onUnmounted } from 'vue';
import { createMonitor } from '@isnotai/text-monitor';
const monitor = createMonitor({
integrationId: 'a7f3c2e1',
region: 'us'
});
const textareaRef = ref(null);
const handle = shallowRef(null);
onMounted(async () => {
handle.value = await monitor.attach(textareaRef.value);
});
onUnmounted(() => handle.value?.detach({ reason: 'detach' }));
</script>
<template>
<textarea ref="textareaRef"></textarea>
</template>
Content Security Policy
Allow the CDN script host and the reporting hosts that match your region. Pick one regional block. Never combine.
US
script-src 'self' https://cdn.isnotai.com;
connect-src 'self' https://chl.isnot.ai wss://chl.isnot.ai;
EU
script-src 'self' https://cdn.isnotai.com;
connect-src 'self' https://chl-eu.isnot.ai wss://chl-eu.isnot.ai;
If you already have the CDN host in script-src for other assets, only the region-specific connect-src line is new.
Troubleshooting
Monitor is not capturing
- Confirm
integrationIdandregionare correct for your account. - Ensure your page's origin is on the authorized origins list for your integration. Add origins from Settings → Authorized Origins in your dashboard. Requests from origins outside the list are rejected.
- Verify your Content Security Policy allows the hosts for your region (see above).
- Check the element you passed to
attach()is one of the supported editor surfaces.
CSP violations in the browser console
- Confirm you are using the CSP block for your account's region, not the other one.
- If the
/auto.jsscript is blocked, addhttps://cdn.isnotai.comtoscript-src. - If the reporting connection is blocked, add the regional host to
connect-src.
Memory leaks in single-page apps
- Call
handle.detach()in every cleanup path where the attached element is removed from the DOM.handleis the value returned byawait monitor.attach(...). - Create the monitor once at module scope. Do not call
createMonitorinside render paths. - For dynamically-mounted editors that you do not explicitly tear down, pass
observeRemoval: truetoattach()so the SDK auto-closes the writing session when the element is removed.
Upgrading from v0.1.x
monitor.attach(el)is nowconst handle = await monitor.attach(el). Wrap call sites in anasyncfunction or use.then(handle => ...).monitor.detach(el)is gone. Useawait handle.detach({ reason }). Passreason: 'submitted'on form submit,'detach'on cleanup, or omit to default to'detach'.- Top-level
monitoris now frozen and exposes onlyattach. Code that calledmonitor.detachwill throwTypeErroruntil updated. /auto.jsconsumers are not affected — the auto entry handles the lifecycle internally.