eslint-plugin-hootlens
v0.1.0
Published
ESLint rules that teach coding agents and developers the Hoot Lens conventions: stable ids, page versions, private regions, dynamic content and consent.
Maintainers
Readme
eslint-plugin-hootlens
ESLint rules for the Hoot Lens conventions. They catch the mistakes that quietly split heatmaps, leak page text into recordings or record a consent choice the visitor never made, and each message says why, so a developer or a coding agent that runs lint learns the convention from the output.
The plugin has no runtime dependencies. It needs eslint 8.57 or later (flat config in ESLint 9 is the primary target) and works with JSX and TSX through any parser that understands them, such as typescript-eslint.
Install
npm install --save-dev eslint-plugin-hootlensUse
Flat config (eslint.config.js):
import hootlens from 'eslint-plugin-hootlens';
export default [
hootlens.configs.recommended,
// Optional: tune a rule.
{
rules: {
'hootlens/stable-ids': ['warn', {require: 'all'}],
'hootlens/private-fields': ['warn', {preset: 'full'}],
},
},
];With TypeScript, add typescript-eslint as usual; the recommended config turns on JSX parsing and does not set a parser.
Legacy .eslintrc:
{
"extends": ["plugin:hootlens/legacy-recommended"]
}Agents see these messages
Coding agents run eslint as part of their loop and read what it prints. Every message in this plugin explains the effect on Hoot Lens data and links to the matching section of docs/TRACKER.md, so the rule text doubles as short documentation the agent can act on without being told the convention in a prompt. Pair it with @hootlens/types so the window.hootlens command queue is type-checked too.
Rules
recommended enables all six. A fix column of "suggestion" means the editor offers a fix you accept; nothing is changed automatically, because every fix here changes what Hoot Lens records.
| Rule | Default | What it catches | Fix |
| --- | --- | --- | --- |
| hootlens/stable-ids | warn | Buttons, links with href, input type=submit\|button and role=button\|link elements without a stable kebab-case data-hoot-id, and ids that look generated (long digit runs, hashes, UUIDs, list indexes). | none |
| hootlens/no-page-version-effect | error | hootlens.push(['pageVersion', ...]) or ['variant', ...] inside useEffect, useLayoutEffect, componentDidMount or a component's render. | none |
| hootlens/private-fields | warn, quiet unless preset: "full" | A form or region holding password, payment, ID or one-time-code fields that is not inside a data-hoot-private element, when the project uses the full preset. | none |
| hootlens/dynamic-regions | warn | Random (Math.random), clock (new Date(), Date.now()) and carousel, slider or ticker components that render rotating content without data-hoot-dynamic. | suggestion |
| hootlens/consent-false-only-on-refusal | warn | ['consent', false] pushed outside a handler that looks like a visitor refusing or a toggle changing. | none |
| hootlens/no-tracker-in-private-routes | warn | The tag or init in files under app/, admin/, account/, dashboard/ or portal/ route folders. | none |
hootlens/stable-ids
Hoot Lens names a clicked element by its data-hoot-id, then by an id with no long digit run, then by an aria-label, and only then by a position path such as div:nth-of-type(3) > a. The position path changes with the layout, so one control becomes several in the heatmap. The tracker uses a data-hoot-id only when it is 1 to 80 letters, digits, hyphens or underscores; the rule asks for lowercase kebab-case within that.
Options:
require:"goals"(default) checks submit controls and controls whose literal text reads like a conversion step (buy, sign up, subscribe, book, donate, contact, start, download and similar)."all"checks every interactive element.checkComponents(defaultfalse): also check components whose names end inButtonorLink.components: extra component names to treat as interactive.ignoreComponents: component names that are never checked.ignorePatterns: regular expressions. An element is skipped when one matches its tag,id,classNameordata-testid.
An element with a {...spread} is skipped, because the spread may carry the id.
hootlens/no-page-version-effect
Each new page version label starts a new segment. A label set from an effect or from render fires on every mount or update, so cohorts split by render timing. Mark the section that changed with data-hoot-region and data-hoot-version, or set the label once before the page renders. Calls inside event handlers (onClick, handleX, onX) are allowed, and so are calls in named helper functions, since they may only run from a handler.
Option: commands (default ["pageVersion", "variant"]).
hootlens/private-fields
The tracker always empties password, payment (autocomplete cc-*) and one-time-code fields, and masks every other form value unless the project allowlists it, so the typed value is not recorded in any preset. What differs is the text around the field. Under the full preset the labels, helper text, order summaries and saved details near it are recorded as written, so the region should be data-hoot-private. Under balanced and strict that text is redacted or replaced, so the rule does nothing.
Option: preset ("full", "balanced" or "strict", default "balanced", which keeps the rule quiet). Set it to the preset your project uses in Settings, Privacy.
The rule matches fields by type="password", by autoComplete tokens (cc-*, current-password, new-password, one-time-code) and by name, id, data-testid or aria-label patterns for card numbers, security codes, social security and tax IDs, and one-time codes. It looks only at the JSX in the file it is linting, so a field rendered by a child component is not seen.
hootlens/dynamic-regions
The tracker fingerprints a page's structure and text to detect a new page version. Content that differs on every load makes the same page look edited. The tracker already ignores <time>, live regions and elements whose id or class names a carousel, swiper, slider, marquee, ticker or similar. This rule covers what it cannot see: random picks in render, the current time written inline, and carousel components whose markup carries other class names.
Options: components (extra widget component names) and ignoreComponents.
hootlens/consent-false-only-on-refusal
['consent', false] is the visitor's choice: it is stored and applied on every later load, discards unsent data and holds until ['consent', true]. The rule accepts it inside handlers named like a refusal (decline, reject, optOut, withdraw, revoke, deny, toggle and similar), in onChange, onCheckedChange and onToggle handlers, in change or input listeners, and in click handlers on controls whose text or id says so. To pause capture for a moment, push ['stop'] and later ['start'].
Option: names: extra handler name fragments that count as a refusal.
hootlens/no-tracker-in-private-routes
Reports the script tag (hootlens.com/t.js), ['init', ...], and init or start imported from @hootlens/tracker in files whose project-relative path has a private route folder: app, admin, account, dashboard or portal. The first app folder in a path is treated as the framework's router root (Next.js app/, Angular src/app/), so src/app/layout.tsx is not reported but src/app/(app)/layout.tsx and src/app/dashboard/layout.tsx are.
Options: segments (folder names that mark a private route) and packages (import sources whose init and start install the tracker).
This is a path heuristic. If a route is public, rename the folder, configure segments, or turn the rule off for that file.
Limits
The rules read one file at a time. They do not follow imports, resolve variables across files or know what your components render, so they miss a violation that spans files and can flag code that is fine in context. Treat them as a prompt to check, and use // eslint-disable-next-line hootlens/<rule> with a reason when the code is right.
License
MIT
