npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

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.

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-hootlens

Use

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 (default false): also check components whose names end in Button or Link.
  • 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, className or data-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