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

@metis-ai-research/feedback

v0.1.1

Published

Drop-in feedback widget that posts to Linear (or your adapter of choice).

Readme

@metis-ai-research/feedback

Drop-in customer feedback widget for React apps. Submits bug reports, general feedback, and feature requests directly to Linear (or any adapter you write).

  • Floating widget UI with type selector, title, description, optional screenshot
  • Server-side proxy so your Linear API key never touches the browser
  • Honeypot + IP rate limit baked in
  • Pluggable adapters — Linear ships by default; bring your own for GitHub Issues, Slack, Plain, etc.
  • Auth-aware: pass the current user and their context flows through to the ticket

Install

npm install @metis-ai-research/feedback

react and react-dom are peer dependencies. Tailwind CSS is required (the widget uses Tailwind classes).

If you're using Tailwind v3, add the package's path to content:

// tailwind.config.ts
content: [
  './app/**/*.{ts,tsx}',
  './node_modules/@metis-ai-research/feedback/dist/**/*.{js,cjs}',
]

Quick start (Next.js App Router)

1. Server route

// app/api/feedback/route.ts
import { createFeedbackHandler } from '@metis-ai-research/feedback/server'
import { linearAdapter } from '@metis-ai-research/feedback/adapters/linear'

export const POST = createFeedbackHandler({
  adapter: linearAdapter({
    apiKey: process.env.LINEAR_API_KEY!,
    teamId: process.env.LINEAR_TEAM_ID!,
    // Optional: route by feedback type
    labelMap: {
      bug: [process.env.LINEAR_LABEL_BUG!],
      feedback: [process.env.LINEAR_LABEL_FEEDBACK!],
      feature: [process.env.LINEAR_LABEL_FEATURE!],
    },
  }),
  // Defaults: 10 requests / 60s per IP. Pass `false` to disable.
  rateLimit: { maxPerWindow: 10, windowMs: 60_000 },
})

2. Client widget

// app/layout.tsx (or any client-rendered place)
import { FeedbackWidget } from '@metis-ai-research/feedback'

export default function RootLayout({ children }) {
  return (
    <html>
      <body>
        {children}
        <FeedbackWidget
          endpoint="/api/feedback"
          user={{ id: session?.user.id, email: session?.user.email, name: session?.user.name }}
          appVersion="1.0.0"
        />
      </body>
    </html>
  )
}

That's it. The widget appears as a floating button bottom-right. Click → modal → submit → ticket lands in Linear.

Note: client-supplied user fields are not authoritative. If you have auth, override them server-side — see Securing user context.


Getting your Linear values

You need three IDs to wire this up: an API key, a teamId, and (optionally) a projectId and label IDs.

API key

Create one at linear.app/settings/api → "Create personal API key". The key looks like lin_api_.... Treat it like a password — server-side env vars only, never in client code or git.

teamId is a UUID, not the team key

This is the #1 source of 400s. The teamId field expects the team's UUID (e.g. 9cfb482a-...), not the human-readable key like ENG you see in URLs (linear.app/your-org/team/ENG/...). Passing the key will fail.

List your team UUIDs:

curl -s https://api.linear.app/graphql \
  -H "Authorization: lin_api_YOUR_KEY" \
  -H "Content-Type: application/json" \
  -d '{"query":"{ teams { nodes { id key name } } }"}'

The id field in each node is what you want.

projectId (optional)

curl -s https://api.linear.app/graphql \
  -H "Authorization: lin_api_YOUR_KEY" \
  -H "Content-Type: application/json" \
  -d '{"query":"{ projects { nodes { id name } } }"}'

Label IDs (optional)

Labels are scoped to a team. Query labels for the team you'll be filing into:

curl -s https://api.linear.app/graphql \
  -H "Authorization: lin_api_YOUR_KEY" \
  -H "Content-Type: application/json" \
  -d '{"query":"{ team(id: \"YOUR_TEAM_UUID\") { labels { nodes { id name } } } }"}'

Use these IDs in labelMap. Labels from a different team will silently not apply (see Troubleshooting).

.env.example

LINEAR_API_KEY=
LINEAR_TEAM_ID=
LINEAR_PROJECT_ID=          # optional
LINEAR_LABEL_BUG=           # optional
LINEAR_LABEL_FEEDBACK=      # optional
LINEAR_LABEL_FEATURE=       # optional

Configuration

<FeedbackWidget /> props

| Prop | Type | Default | Notes | |---|---|---|---| | endpoint | string | required | Path to your server route (e.g. /api/feedback) | | user | { id?, email?, name? } | undefined | Auth-aware context — included on the ticket | | appVersion | string | undefined | Useful for correlating bugs to deploys | | metadata | Record<string, unknown> | undefined | Free-form per-app context | | position | 'bottom-right' \| 'bottom-left' \| 'inline' | 'bottom-right' | Where the trigger lives | | triggerLabel | string | 'Feedback' | Trigger button text | | defaultType | 'bug' \| 'feedback' \| 'feature' | 'feedback' | Pre-selected type | | onSuccess | (result) => void | undefined | Callback after successful submission |

linearAdapter options

| Option | Type | Notes | |---|---|---| | apiKey | string | Personal API key from Linear settings | | teamId | string | UUID of the Linear team that should receive issues | | projectId | string (optional) | Pin all issues to a specific project | | labelMap | Partial<Record<FeedbackType, string[]>> | Map types → Linear label IDs | | defaultPriority | 0 \| 1 \| 2 \| 3 \| 4 | 0 = none, 1 = urgent, 4 = low | | titlePrefix | Partial<Record<FeedbackType, string>> \| false | Override default [BUG] / [FEEDBACK] / [FEATURE] prefixes, or false to disable |

createFeedbackHandler options

| Option | Type | Notes | |---|---|---| | adapter | FeedbackAdapter | Required. The Linear adapter or a custom one | | rateLimit | { maxPerWindow, windowMs } \| false | Default { 10, 60_000 }. false disables. | | enrichPayload | (payload, request) => Promise<Partial<FeedbackPayload>> | Server-side hook to add user IDs, etc. from auth |

What gets sent to Linear

Only the fields below are forwarded to the adapter. Nothing else leaves your app.

| Field | Source | Notes | |---|---|---| | title | user input | Required, max 200 chars | | description | user input | Required, max 10,000 chars | | type | user selection | bug, feedback, or feature | | screenshotDataUrl | optional capture | Uploaded to Linear's CDN, embedded as a markdown image | | userContext.url | window.location.href | Page the user submitted from | | userContext.userAgent | navigator.userAgent | Browser/OS string | | userContext.viewport | window.innerWidth/Height | Useful for repro | | userContext.appVersion | appVersion prop | If you pass it | | userContext.userId | user.id prop or enrichPayload | Override server-side for trust | | userContext.userEmail | user.email prop or enrichPayload | Override server-side for trust | | userContext.userName | user.name prop or enrichPayload | Override server-side for trust | | metadata | metadata prop or enrichPayload | Free-form, rendered as a JSON block |

Securing user context

The user prop on <FeedbackWidget /> is sent from the browser, so a hostile or buggy client can lie about user.id, user.email, and user.name. If you have auth, do not trust those values — overwrite them server-side via enrichPayload. Returned partials are deep-merged onto the validated payload, so client-supplied fields (like appVersion) survive.

import { getServerSession } from 'next-auth'
import { authOptions } from '@/lib/auth'

enrichPayload: async () => {
  const session = await getServerSession(authOptions)
  if (!session?.user) return {}
  return { userContext: { userId: session.user.id, userEmail: session.user.email, userName: session.user.name } }
},

If enrichPayload throws, the handler logs and submits the un-enriched payload anyway — it never blocks user feedback on an auth lookup failure.


Custom adapters

Implement the FeedbackAdapter interface:

import type { FeedbackAdapter, FeedbackPayload, FeedbackResult } from '@metis-ai-research/feedback/server'

export function githubAdapter(opts: { token: string; repo: string }): FeedbackAdapter {
  return {
    name: 'github',
    async submit(payload: FeedbackPayload): Promise<FeedbackResult> {
      const res = await fetch(`https://api.github.com/repos/${opts.repo}/issues`, { /* ... */ })
      // ...
      return { ok: true, ticketUrl: '...' }
    }
  }
}

Adapters must not throw on transport errors — return { ok: false, error } instead. The handler maps both thrown errors and ok: false returns to a 502.


Troubleshooting

HTTP 500 with linearAdapter: apiKey is required (or teamId is required) in the server logsLINEAR_API_KEY or LINEAR_TEAM_ID is missing in the environment the route runs in. The adapter throws synchronously when constructed, so the route module fails to load and Next.js returns a 500. Set the env vars and redeploy.

HTTP 400 from Linear (logged as Linear API responded 400) — almost always a bad teamId. The most common cause is using the team key (ENG) instead of the UUID. Also check that projectId and any label IDs in labelMap are valid UUIDs.

Submission succeeds but the ticket has no labels — the label IDs in labelMap belong to a different team than teamId. Linear silently drops cross-team labels rather than erroring. Re-run the label query scoped to the right team UUID.


Production checklist

  • Separate API keys per environment. Create one Linear key for production and a different one for dev/staging, so you can revoke either without affecting the other.
  • Vercel: set env vars on Production scope only. Leave Preview and Development empty. The feedback route on those deploys will fail to load (500 with apiKey is required in the logs) instead of polluting your real Linear team with test tickets.
  • Redeploy after env var changes. Vercel (and most platforms) bind env vars at build time — existing builds keep their old values until you redeploy.

License

MIT © Metis AI Research Inc.