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

@plany/feedback-widget

v1.4.0

Published

Embeddable feedback widget for Plany: a bottom-right bubble that files issues into your Plany project

Readme

@plany/feedback-widget

Embeddable feedback widget for Plany: a bottom-right bubble that files reports straight into your Plany project's Issues.

Server-proxied by design — the browser widget carries no credential:

browser widget ──POST──▶ your backend ──POST + server key──▶ Plany

Plany gives you ONE server key (Issues → Embed widget). It lives in your server env only — never in page HTML, never in client bundles. Because submissions pass through your backend, you can gate them to signed-in users and stamp the reporter's identity server-side, making it non-spoofable.

1 · Backend: proxy endpoint

npm i @plany/feedback-widget
// Next.js app/api/feedback/route.ts — any fetch-shaped handler works
// (Hono, Bun, Deno, Node 20+, edge runtimes).
import { proxyPlanyFeedback } from "@plany/feedback-widget";

export async function POST(req: Request) {
  // Optional: gate to signed-in users and attest their identity.
  // const session = await auth();
  // if (!session) return new Response(null, { status: 401 });
  return proxyPlanyFeedback(req, {
    token: process.env.PLANY_FEEDBACK_KEY!,
    api: "https://<deployment>.convex.site",
    // name: session.user.name,      // overrides whatever the browser sent —
    // contact: session.user.email,  // the identity is attested by YOUR server
    // reporterId: session.user.id,  // binds report history to the account
  });
}

2 · Frontend: mount the bubble

import { initPlanyFeedback } from "@plany/feedback-widget";

const widget = initPlanyFeedback({ endpoint: "/api/feedback" });
widget.setPosition({ bottom: 24, right: 24 }); // move an existing widget
widget.refreshContext(); // call after an SPA route change
widget.destroy(); // unmount (e.g. in a React effect cleanup)

Or as a script tag (equally credential-free):

<script async src="https://plany.bielcrystal.app/widget.js" data-endpoint="/api/feedback"></script>

Optional attributes / config:

  • data-lang="zh" / lang: "zh" — UI language (default: auto-detect)
  • data-name / name — prefill the reporter name and hide the field (for an attested identity, set it in proxyPlanyFeedback instead); name: false / data-name="off" hides the field and sends nothing
  • data-contact / contact — same semantics for the contact field
  • pageUrl: false — don't attach the current page URL (attached by default; the panel shows the exact URL that will be sent)
  • data-screenshot="auto|prompt|off" / screenshot: "auto" | "prompt" | false — screenshot mode (default auto); screenshots are optional
  • request — a fetch-compatible function used for all host endpoint requests, useful for adding session authorization
  • historyNamespace — isolates local report history within an endpoint, for example by signed-in user id
  • bottom / right — initial bubble offsets in CSS pixels (default 20); zIndex controls its stacking context
  • onError(err) — observe failed submits

The panel lets users attach up to 5 files of any type, ≤20MB total. PNG/JPEG/GIF/WebP attachments get thumbnail previews; every other format is stored and served as a download so browser-executable files never render inline.

Page screenshot (recommended) is optional and remains a removable, previewed attachment until the user hits send. In auto mode (default), the widget renders the visible viewport from the DOM when the form opens, without a browser prompt (cross-origin images and canvas/video content may be missing). prompt captures only after the user clicks the button and uses the browser's tab-share dialog (pixel-perfect, asks every time, hidden where unsupported); off removes the button. Call widget.refreshContext() after SPA route changes so the page hint and any automatic screenshot follow the current route.

My reports: anonymous status tracking

Every accepted report returns a per-issue claim token, which the widget keeps in localStorage. A "My reports" view in the panel lists past reports with a live status — received / in progress (with the assignee's display name) / fixed / closed — and a per-report timeline. A red dot on the bubble signals unseen updates (checked at most hourly).

This is capability auth, not accounts: presenting the token proves "I filed this report" and unlocks nothing else. Only a hash is stored server-side; clearing browser storage forgets the history. Status queries reuse the same proxy endpoint (a JSON POST with a claims array), so hosts already running proxyPlanyFeedback get all of this by just updating the package.

Bind history to your users' accounts. If your site has signed-in users, pass reporterId: session.user.id to proxyPlanyFeedback. Plany stores a salted hash of it per report — never the id itself — and the widget seeds "My reports" from it on any device, so a cleared cache or a new laptop no longer loses the history and replying keeps working. Your system stores nothing extra: the id is read from the session you already have, per request. Without reporterId the widget stays fully anonymous and per-device, exactly as before.

Two-way replies, live. Each report's timeline is a thread: team replies appear as messages, and the reporter writes back from the same view (2000 chars, 50 replies per report) — attachments included, both ways (≤5 files and ≤20MB total per message). While a thread is open on screen it's subscribed over Plany's WebSocket sync channel, so replies land the moment they're written — real chat pace. The subscription is authorized by a per-report thread key handed out through your proxy; no credential reaches the browser beyond that single-report, read-only capability. Internal comments never leave Plany — visibility is a per-message mode in the issue drawer (reply to reporter is the default on widget reports), mirroring the public-reply/private-note split of support tools.

Headless (CLIs, backends, scripts — Node 18+)

import { submitPlanyFeedback } from "@plany/feedback-widget";

await submitPlanyFeedback({
  token: process.env.PLANY_FEEDBACK_KEY!, // server-side only
  api: "https://<deployment>.convex.site",
  message: "Export crashed on empty CSV",
  name: "my-cli", // optional
  contact: "[email protected]", // optional
  files: [new File([buf], "crash.log", { type: "text/plain" })], // optional, Node 20+
});

Or without the package at all:

curl -X POST https://<deployment>.convex.site/embed/issues \
  -H 'Content-Type: application/json' \
  -d '{"token":"pesk_...","message":"something broke"}'

Notes

  • The server key authorizes filing feedback into one project, plus the reporter-scoped reads that power "My reports": status/thread lookups for reports proven by a claim token or by the reporterId the key holder attests. It can read nothing else — no titles you didn't file, no internal comments, no project data. Keep it server-side and rotate it anytime by toggling the widget off and on in Plany.
  • Submissions are rate-capped per project. Message ≤ 5000 chars.
  • Migrating from ≤0.3.x (token/api in the browser, getSignature): move the credential into a backend route with proxyPlanyFeedback and point the widget at it via endpoint. The old public-token mode and HMAC signature flow are gone — one key, server-side, is the whole story.