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.js script 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
npm install @isnotai/text-monitor
yarn
yarn add @isnotai/text-monitor
pnpm
pnpm add @isnotai/text-monitor

Or use the CDN /auto.js entry (see Declarative Install below).

Programmatic Usage

Breaking change in v1.0. 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.

JavaScript
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:

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

HTML
<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:

HTML
<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:

  1. In your dashboard, open Settings → Self-hosted Script. You will see the CNAME targets your package needs.
  2. Create the CNAME records on your own subdomain (for example, verify.example.com). The dashboard verifies propagation once the records are live.
  3. Download your packaged bundle. It is built with your integration ID, region, and CNAMEd reporting hosts baked in.
  4. Host the file on your own CDN. Your install tag becomes a single script with no data-* attributes:
HTML
<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.

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

Vue 3
<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

CSP
script-src 'self' https://cdn.isnotai.com;
connect-src 'self' https://chl.isnot.ai wss://chl.isnot.ai;

EU

CSP
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 integrationId and region are 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.js script is blocked, add https://cdn.isnotai.com to script-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. handle is the value returned by await monitor.attach(...).
  • Create the monitor once at module scope. Do not call createMonitor inside render paths.
  • For dynamically-mounted editors that you do not explicitly tear down, pass observeRemoval: true to attach() so the SDK auto-closes the writing session when the element is removed.

Upgrading from v0.1.x

  • monitor.attach(el) is now const handle = await monitor.attach(el). Wrap call sites in an async function or use .then(handle => ...).
  • monitor.detach(el) is gone. Use await handle.detach({ reason }). Pass reason: 'submitted' on form submit, 'detach' on cleanup, or omit to default to 'detach'.
  • Top-level monitor is now frozen and exposes only attach. Code that called monitor.detach will throw TypeError until updated.
  • /auto.js consumers are not affected — the auto entry handles the lifecycle internally.