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

feedback-tracker

v0.2.1

Published

A universal embeddable feedback widget with pluggable backend adapters.

Readme

feedback-tracker

A universal, embeddable feedback widget. One Web Component, three reference adapters, GDPR-friendly defaults. No framework, no build step, no vendor lock-in.

Live demo: https://feedback.api.mpeters.dev/ · CDN: https://feedback.api.mpeters.dev/feedback-tracker.js

<script type="module" src="https://feedback.api.mpeters.dev/feedback-tracker.js"></script>
<feedback-tracker
  adapter="pocketbase"
  endpoint="https://feedback.api.mpeters.dev"
  collection="feedback"
  collect-email
></feedback-tracker>

That's it. Floating button bottom-right, click → form, submit → the hosted PocketBase instance (or any backend you wire up via the adapters).

Why another one?

Existing widgets either lock you to one SaaS, require a backend you don't have, or aren't backend-agnostic. This one is a Web Component (works in any framework or none) with a pluggable adapter system so the backend is your choice. A couple are bundled out of the box; writing another is ~30 lines.

Install

Three options, pick one:

1. Use the hosted CDN (zero setup — production deployment at feedback.api.mpeters.dev):

<script type="module" src="https://feedback.api.mpeters.dev/feedback-tracker.js"></script>

2. Self-host the static file: copy src/ to any static host and load it as an ES module:

<script type="module" src="/path/to/feedback-tracker.js"></script>

3. Via npm (once published):

npm install feedback-tracker
import 'feedback-tracker';

Bundled adapters

| Adapter | Use when | Setup | |---|---|---| | webhook | You want to POST JSON anywhere — your own server, a Cloudflare Worker, a Discord/Slack webhook | endpoint="..." | | pocketbase | You want a self-hosted single-binary backend on your homeserver | endpoint, collection |

Webhook (universal)

<feedback-tracker
  adapter="webhook"
  endpoint="https://example.com/feedback"
></feedback-tracker>

POSTs JSON like:

{
  "type": "idea",
  "message": "...",
  "email": "[email protected]",
  "meta": {
    "url": "https://yoursite.com/page",
    "userAgent": "...",
    "locale": "en-US",
    "timestamp": "2026-04-12T15:14:22.000Z"
  }
}

Discord and Slack webhooks need a slightly different shape — pass data-format="discord" or data-format="slack" and the adapter formats the body for you.

PocketBase (self-host or hosted)

PocketBase is a single Go binary (~30 MB) with SQLite, an admin UI, file storage, and rule-based permissions. The hosted instance at https://feedback.api.mpeters.dev is a PocketBase deployment configured exactly like the steps below — see Self-hosting for how it's set up.

To run your own:

  1. Download and run PocketBase:
    ./pocketbase serve
  2. Open the admin UI at http://127.0.0.1:8090/_/ and create an admin account.
  3. Create a new collection called feedback with fields:
    • type (Plain text, required)
    • message (Plain text, required, max length 5000)
    • email (Email, optional)
    • url (URL)
    • userAgent (Plain text)
    • locale (Plain text)
    • screenshot (File, optional, max 5 MB, MIME types image/png, image/jpeg, image/webp) — required if you want to receive screenshots
  4. In the collection's API Rules tab, set the Create rule to an empty string (just save it blank). Leave List/View/Update/Delete rules unset so the public can write but not read.
  5. Embed the widget:
<feedback-tracker
  adapter="pocketbase"
  endpoint="https://pb.example.com"
  collection="feedback"
></feedback-tracker>

The PocketBase admin UI is your triage dashboard. Pair with Cloudflare Tunnel to expose it from a homeserver without opening ports — or follow the Caddy recipe below for a public droplet.

Configuration

| Attribute | Description | |---|---| | adapter | Which adapter to use (webhook, pocketbase, or any registered) | | accent | CSS color for the button and accents (default #6366f1) | | label | Trigger button text (default Feedback) | | collect-email | Show an email field | | position | bottom-right (default) or bottom-left | | hidden-trigger | Hide the floating button (control programmatically) |

Adapter-specific attributes (endpoint, project, etc.) are passed through to the adapter's config object. Anything starting with data- is also passed through.

Programmatic control

import { FeedbackTracker, registerAdapter } from 'feedback-tracker';

// Register a custom adapter
registerAdapter('mybackend', {
  async submit(payload, config) {
    const res = await fetch(config.endpoint, {
      method: 'POST',
      body: JSON.stringify(payload),
    });
    return res.ok ? { ok: true } : { ok: false, error: await res.text() };
  },
});
<feedback-tracker adapter="mybackend" endpoint="..."></feedback-tracker>

Events

The widget dispatches feedback-sent and feedback-error custom events that bubble through the shadow root:

document.querySelector('feedback-tracker').addEventListener('feedback-sent', (e) => {
  console.log('sent', e.detail.payload, e.detail.result);
});

Spam protection

The widget ships with a honeypot field (catches naive bots for free). For real spam protection, put a Cloudflare Turnstile verifier in front of your endpoint — it's free, unlimited, and invisible. The webhook adapter is the natural place to add it: send the Turnstile token in the payload and verify server-side before storing.

React / Next.js

A thin React wrapper ships at feedback-tracker/react. It registers the Web Component on mount (SSR-safe — registration happens in useEffect) and forwards camelCase props as kebab-case attributes:

'use client';
import { FeedbackTracker } from 'feedback-tracker/react';

export default function Page() {
  return (
    <FeedbackTracker
      adapter="pocketbase"
      endpoint="https://feedback.api.mpeters.dev"
      collection="feedback"
      collectEmail
      accent="#6366f1"
      label="Send feedback"
      onSent={(d) => console.log('sent', d.payload, d.result)}
      onError={(d) => console.warn('failed', d.error)}
    />
  );
}

TypeScript types ship with the package (src/react/index.d.ts). All adapter-specific props (endpoint, collection, accessKey) and widget options (accent, label, position, collectEmail, hiddenTrigger, noScreenshot, dataFormat) are typed. onSent / onError receive the same detail payload as the underlying feedback-sent / feedback-error custom events.

To register a custom adapter from React code:

import { registerAdapter } from 'feedback-tracker/react';

await registerAdapter('mybackend', {
  async submit(payload, config) { /* ... */ },
});

Demo

Live: https://feedback.api.mpeters.dev/ — a Next.js static export using the React wrapper, deployed to the same droplet that hosts the widget files. Click the floating button in the bottom-right to submit to the hosted PocketBase instance.

Local development:

cd demo-next
npm install
npm run dev
# open http://localhost:3000

Build the static export:

npm run demo:build      # → demo-next/out/
npm run demo:preview    # → http://localhost:3000 (built output)

Edit demo-next/app/page.tsx to change adapters, endpoints, or layout. The demo imports the React wrapper from the workspace via a file:.. dependency, so changes to src/react/index.jsx are picked up after npm install.

Self-hosting (Caddy + PocketBase)

This is the exact recipe used to deploy the hosted instance at https://feedback.api.mpeters.dev on a $4/mo DigitalOcean droplet (Ubuntu, 1 vCPU, 512 MB). Replace the domain with your own.

1. DNS. Point an A record at your droplet's IP:

feedback.example.com    A    YOUR.DROPLET.IP    60

2. Install Caddy (terminates TLS, serves static files, reverse-proxies to PocketBase):

apt install -y debian-keyring debian-archive-keyring apt-transport-https curl gnupg
curl -fsSL https://dl.cloudsmith.io/public/caddy/stable/gpg.key \
  | gpg --dearmor -o /usr/share/keyrings/caddy-stable-archive-keyring.gpg
curl -fsSL https://dl.cloudsmith.io/public/caddy/stable/debian.deb.txt \
  > /etc/apt/sources.list.d/caddy-stable.list
apt update && apt install -y caddy

3. Install PocketBase as a systemd service:

PB_VER=$(curl -fsSL https://api.github.com/repos/pocketbase/pocketbase/releases/latest \
  | grep tag_name | head -1 | sed 's/.*"v\([^"]*\)".*/\1/')
curl -fsSL -o /tmp/pb.zip \
  "https://github.com/pocketbase/pocketbase/releases/download/v${PB_VER}/pocketbase_${PB_VER}_linux_amd64.zip"
apt install -y unzip
unzip -o /tmp/pb.zip pocketbase -d /usr/local/bin/
chmod +x /usr/local/bin/pocketbase
useradd --system --home /var/lib/pocketbase --shell /usr/sbin/nologin pocketbase
mkdir -p /var/lib/pocketbase && chown -R pocketbase:pocketbase /var/lib/pocketbase

cat > /etc/systemd/system/pocketbase.service <<'EOF'
[Unit]
Description=PocketBase
After=network.target

[Service]
Type=simple
User=pocketbase
Group=pocketbase
ExecStart=/usr/local/bin/pocketbase serve --http=127.0.0.1:8090 --dir=/var/lib/pocketbase/pb_data --publicDir=/var/lib/pocketbase/pb_public
Restart=always
RestartSec=5
LimitNOFILE=4096

[Install]
WantedBy=multi-user.target
EOF

systemctl daemon-reload
systemctl enable --now pocketbase

4. Deploy the widget files:

mkdir -p /var/www/feedback-tracker
# from your dev machine:
rsync -az src/ [email protected]:/var/www/feedback-tracker/
rsync -az README.md LICENSE package.json [email protected]:/var/www/feedback-tracker/
rsync -az demo/index.html [email protected]:/var/www/feedback-tracker/index.html

5. Configure Caddy (/etc/caddy/Caddyfile):

feedback.example.com {
    encode gzip zstd

    @cors_preflight method OPTIONS
    handle @cors_preflight {
        header Access-Control-Allow-Origin "*"
        header Access-Control-Allow-Methods "GET, POST, OPTIONS"
        header Access-Control-Allow-Headers "Content-Type, Authorization"
        header Access-Control-Max-Age "86400"
        respond 204
    }

    handle /api/* {
        reverse_proxy 127.0.0.1:8090
        header Access-Control-Allow-Origin "*"
    }

    handle /_/* {
        reverse_proxy 127.0.0.1:8090
    }

    handle {
        root * /var/www/feedback-tracker
        header {
            Access-Control-Allow-Origin "*"
            Cache-Control "public, max-age=300"
            ?X-Content-Type-Options "nosniff"
        }
        @js path *.js
        header @js Content-Type "application/javascript; charset=utf-8"
        file_server
    }
}
systemctl reload caddy

Caddy provisions a Let's Encrypt cert automatically on first request.

6. Bootstrap the PocketBase superuser & feedback collection:

sudo -u pocketbase /usr/local/bin/pocketbase superuser create \
  [email protected] 'GENERATE-A-STRONG-PASSWORD' \
  --dir=/var/lib/pocketbase/pb_data

Then log into https://feedback.example.com/_/ and create the feedback collection per the PocketBase adapter section above. Or do it via API:

TOKEN=$(curl -sS -X POST https://feedback.example.com/api/collections/_superusers/auth-with-password \
  -H "Content-Type: application/json" \
  -d '{"identity":"[email protected]","password":"YOUR_PASSWORD"}' | jq -r .token)

curl -sS -X POST https://feedback.example.com/api/collections \
  -H "Authorization: $TOKEN" -H "Content-Type: application/json" \
  -d '{
    "name": "feedback",
    "type": "base",
    "fields": [
      {"name": "type",       "type": "text",  "required": true,  "max": 50},
      {"name": "message",    "type": "text",  "required": true,  "max": 5000},
      {"name": "email",      "type": "email", "required": false},
      {"name": "url",        "type": "url",   "required": false},
      {"name": "userAgent",  "type": "text",  "required": false, "max": 500},
      {"name": "locale",     "type": "text",  "required": false, "max": 20},
      {"name": "screenshot", "type": "file",  "required": false, "maxSelect": 1, "maxSize": 5242880,
        "mimeTypes": ["image/png","image/jpeg","image/webp"]}
    ],
    "createRule": "",
    "listRule": null, "viewRule": null, "updateRule": null, "deleteRule": null
  }'

7. Embed the widget elsewhere:

<script type="module" src="https://feedback.example.com/feedback-tracker.js"></script>
<feedback-tracker
  adapter="pocketbase"
  endpoint="https://feedback.example.com"
  collection="feedback"
  collect-email
></feedback-tracker>

Updating after code changes:

rsync -az src/ [email protected]:/var/www/feedback-tracker/

No reload needed — Caddy serves new files immediately (the Cache-Control: max-age=300 means embedders may take up to 5 minutes to pick up changes).

Resource use (observed on the live droplet): Caddy ~17 MB RAM, PocketBase ~10 MB RAM. Comfortably fits a 512 MB droplet.

Adapter interface

To write your own adapter:

export const myAdapter = {
  async submit(payload, config) {
    // payload = { type, message, email?, meta }
    // config  = attributes from the <feedback-tracker> tag
    // return  = { ok: true, id?: string } | { ok: false, error: string }
  },
};

Then register it before the element is connected:

import { registerAdapter } from 'feedback-tracker';
import { myAdapter } from './my-adapter.js';
registerAdapter('mine', myAdapter);

Roadmap

  • Built-in Cloudflare Turnstile support
  • More adapters: Supabase, GitHub Issues (via Worker proxy), Brevo (via Worker proxy)
  • i18n
  • Custom field schemas

Recently shipped: Lucide icon set, screenshot capture + redaction, hosted PocketBase deployment, React wrapper, Next.js static-export demo.

License

MIT