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

@madebywild/feedback

v0.2.1

Published

Headless, self-hosted marker.io equivalent — embeddable feedback widget that posts to Supabase and fans out to one or more configurable channels (Productive.io, etc.) via an adapter pattern.

Readme

@madebywild/feedback

wild-feedback cover

Embeddable feedback widget for browser apps. It captures an annotated screenshot plus runtime context, posts to a Supabase Edge Function, and fans out to one or more configured channels (Productive.io today; Linear, Slack, GitHub Issues, webhook, … via the same adapter pattern).

  • Package source: packages/feedback/src
  • Edge Function source: packages/feedback/supabase/functions/ingest-feedback
  • Channel adapters: packages/feedback/supabase/functions/ingest-feedback/channels
  • Schema/migrations: packages/feedback/supabase/migrations

Install

Consumers install the published package from npm:

pnpm add @madebywild/feedback

Inside this repository, use PNPM workspace commands while developing the package:

pnpm --filter @madebywild/feedback dev
pnpm --filter @madebywild/feedback build

Client usage

Using React? Skip ahead to React bindings for the @madebywild/feedback/react hooks.

import Feedback from "@madebywild/feedback";

Feedback.init({
  projectKey: "feedback_demo",
  endpoint: "https://<project-ref>.supabase.co/functions/v1/ingest-feedback",
  authToken: async () =>
    (await supabase.auth.getSession()).data.session?.access_token,
  trigger: { button: true, hotkey: "Shift+F" },
  position: "bottom-right",
  theme: "auto",
  reporter: { id: "user_123", name: "Avery", email: "[email protected]" },
  metadata: { release: "2026.04.27", page: "settings" },
  onSubmitted: (result) => console.log(result.id, result.status),
  onError: (error) => console.error(error.message),
});

Feedback.open();
Feedback.close();
Feedback.setReporter({ id: "user_123", name: "Avery" });
Feedback.shutdown();

Public API

default export Feedback (packages/feedback/src/index.ts):

  • init(config: FeedbackConfig)
    • Validates required projectKey + endpoint (must be a valid absolute URL)
    • Validates that metadata (if provided) is JSON-serializable
    • Installs console/network capture
    • Mounts/re-mounts the widget
  • open() opens capture/composer
  • close() closes composer
  • setReporter(reporter) mutates reporter used for next submit
  • setMetadata(metadata) replaces the per-submission metadata payload without re-mounting the widget. Use this on route changes — re-calling init resets capture buffers and churns event listeners. Validates JSON-serializability and throws on circular refs / BigInt.
  • shutdown() unmounts widget and restores patched console/fetch/xhr
  • clearStoredReporter() wipes the cached reporter name/email from sessionStorage and localStorage. Call on sign-out for shared-device flows.

FeedbackConfig

| Field | Required | Default | Notes | | ---------------- | -------- | ---------------- | -------------------------------------------------------------------------------------------------------------------------------- | | projectKey | yes | — | Must match widget_projects.project_key. | | endpoint | yes | — | Supabase Edge Function URL. | | authToken | no | undefined | Supabase access token (or async getter). Sent as Authorization: Bearer .... Required when backend auth_mode='authenticated'. | | trigger.button | no | true | Floating action button visibility. | | trigger.hotkey | no | "Shift+F" | Set false to disable keyboard trigger. | | position | no | "bottom-right" | bottom-right | bottom-left | top-right | top-left. | | theme | no | "auto" | light | dark | auto (prefers-color-scheme). | | reporter | no | undefined | { id?, name?, email? }. | | metadata | no | undefined | Arbitrary JSON persisted as custom_metadata. | | onSubmitted | no | undefined | Receives { id, status }. | | onError | no | undefined | Called on capture/submit errors. |

What is captured

  • Screenshot of document.documentElement (modern-screenshot), excluding:
    • Widget host (data-feedback-ignore on host)
    • Any node with data-feedback-ignore
  • Annotations: arrow, box, pen, text (prompt), blur
  • Console ring buffer: last 200 entries (log/info/warn/error/debug + uncaught/unhandled)
  • Network ring buffer: last 50 fetch/XMLHttpRequest calls
  • Metadata: URL, referrer, user-agent, language, viewport/screen, timezone, timestamp

React bindings

A thin set of hooks at @madebywild/feedback/react wraps the imperative API. react and react-dom are optional peer dependencies (^18 || ^19).

import {
  useFeedback,
  useFeedbackReporter,
  useFeedbackMetadata,
  useFeedbackActions,
  clearStoredReporter,
} from "@madebywild/feedback/react";

function App() {
  useFeedback({
    projectKey: "feedback_demo",
    endpoint: import.meta.env.VITE_FEEDBACK_ENDPOINT,
    authToken: () => session?.access_token,
    onSubmitted: (r) => console.log(r.id, r.status),
  });
  useFeedbackReporter(
    user ? { id: user.id, name: user.name, email: user.email } : undefined,
  );
  useFeedbackMetadata({ route: pathname, release: __BUILD__ });
  return <Routes />;
}

function HelpMenu() {
  const { open } = useFeedbackActions();
  return <button onClick={open}>Report a bug</button>;
}

The hooks solve the four things React users get wrong on their own:

  1. StrictMode-safe: development double-invocation does not double-init or reset capture buffers.
  2. SSR-safe: no-op on the server; mounts on the first browser-side effect.
  3. Stable callbacks: authToken, onSubmitted, onError are read through refs so inline closures don't retrigger init.
  4. Reactive reporter / metadata: useFeedbackReporter and useFeedbackMetadata shallow-compare and call setReporter / setMetadata on change. Use them on route or auth changes instead of recalling useFeedback, which would reset the console and network ring buffers.

clearStoredReporter() (re-exported from the React entry; also Feedback.clearStoredReporter()) wipes the mbwf:reporter cache from sessionStorage and localStorage. The composer prefills name/email from this cache when config.reporter is missing those fields, so the cached values can leak across sign-outs on shared devices unless explicitly cleared.

See docs/react.md for the full reference, including reporter-cache precedence and the singleton-divergence pitfall when mixing entries.

Transport contract

Widget submits multipart/form-data:

  • project_key: string
  • payload: JSON string containing:
    • title (required)
    • description
    • reporter
    • metadata
    • customMetadata
    • consoleLog
    • networkLog
  • screenshot: PNG blob (screenshot.png)

fetch uses credentials: "omit" and includes Authorization only when authToken is provided.

Successful API response shape is { id: string, status: "received" | "synced" | "partial" | "failed" }:

  • received — submission stored, no channels were configured (or the channel lookup itself failed).
  • synced — every active channel for this project succeeded.
  • partial — at least one channel succeeded and at least one failed. Per-channel detail is in feedback_dispatches.
  • failed — every active channel failed.

Supabase backend setup

The npm package ships a complete Supabase workdir at supabase/:

  • supabase/config.toml — local/deploy config; ingest-feedback has verify_jwt = false because the function performs project/origin/auth-mode checks itself.
  • supabase/migrations/20260507091706_initial_wild_feedback.sql — the consolidated production schema.
  • supabase/functions/ingest-feedback — the Edge Function and channel adapters.

From a consuming app after pnpm add @madebywild/feedback, the recommended production path is to copy the packaged Supabase workdir into your app repo and manage it in version control:

FEEDBACK_PKG_DIR="$(pnpm root)/@madebywild/feedback"
mkdir -p supabase
cp -R "$FEEDBACK_PKG_DIR/supabase/." supabase/

supabase link --project-ref <project-ref> --password '<database-password>'
supabase db push --password '<database-password>'
supabase secrets set \
  --project-ref <project-ref> \
  PRODUCTIVE_API_TOKEN=<token> \
  PRODUCTIVE_ORGANIZATION_ID=<org-id>
supabase functions deploy ingest-feedback --project-ref <project-ref> --use-api

For one-off evaluation without copying files, run against the packaged workdir directly. Use a percent-encoded database URL because this flow does not link a local project:

FEEDBACK_PKG_DIR="$(pnpm root)/@madebywild/feedback"
DB_URL='postgresql://postgres:<percent-encoded-password>@db.<project-ref>.supabase.co:5432/postgres'

supabase db push --workdir "$FEEDBACK_PKG_DIR" --db-url "$DB_URL"
supabase functions deploy ingest-feedback \
  --workdir "$FEEDBACK_PKG_DIR" \
  --project-ref <project-ref> \
  --use-api

Run migrations before sending traffic. The schema creates private service-role-only tables, the feedback-screenshots storage bucket, the consume_rate_limit RPC, and RLS deny policies for public client roles.

Required project data

Each client app needs one widget_projects row and at least one active widget_project_channels row. project_key must match Feedback.init({ projectKey }); allowed_origins should contain exact browser origins such as https://app.example.com. An empty origin array disables origin enforcement and should be reserved for development.

insert into public.widget_projects
  (project_key, name, allowed_origins, auth_mode)
values
  (
    'feedback_demo',
    'Feedback demo',
    array['https://app.example.com'],
    'both'
  )
returning id;

insert into public.widget_project_channels
  (widget_project_id, kind, config)
values
  (
    '<widget-project-id>',
    'productive',
    jsonb_build_object(
      'projectId', '<productive project id>',
      'taskListId', '<productive task list id>',
      'assigneeId', null,
      'titlePrefix', '[Feedback] '
    )
  );

auth_mode is anonymous, authenticated, or both. In authenticated mode, configure authToken on the browser widget so the Edge Function can validate the reporter with Supabase Auth.

Channels

Every submission for a given widget_projects.id is fanned out to the active rows in widget_project_channels for that project. Each row is one configured instance of a ChannelAdapter. Channels run concurrently (Promise.allSettled); each writes one feedback_dispatches row and the submission's status rolls them up:

| Outcome across channels | feedback_submissions.status | | ----------------------------------------- | ----------------------------- | | no active channels | received | | all channels synced | synced | | at least one synced + at least one failed | partial | | all channels failed | failed |

feedback_submissions.sync_error carries a short summary ("1/3 channels failed"); per-channel detail is in feedback_dispatches.sync_error.

Built-in channels

productive

Config (widget_project_channels.config):

{
  "projectId": "<productive project id>",
  "taskListId": "<productive task list id>",
  "assigneeId": "<optional productive person id>",
  "titlePrefix": "[Feedback] "
}

Required env on the Edge Function: PRODUCTIVE_API_TOKEN, PRODUCTIVE_ORGANIZATION_ID.

Adding a new channel kind

The adapter contract lives in supabase/functions/ingest-feedback/channels/types.ts:

export interface ChannelAdapter<TConfig = unknown> {
  readonly kind: string;
  parseConfig(raw: unknown): TConfig; // throw on invalid
  dispatch(
    input: ChannelDispatchInput,
    config: TConfig,
  ): Promise<DispatchResult>;
}

To add (e.g.) a Linear channel:

  1. Create channels/linear/index.ts exporting an adapter with kind: "linear".
  2. Register it in channels/registry.ts.
  3. Insert a widget_project_channels row with kind = 'linear' and an appropriate config jsonb.
  4. Set any required env vars (e.g. LINEAR_API_KEY) on the Edge Function.

Adapters MUST NOT throw — they return DispatchFailure on error so the orchestrator can record the failure without taking down the rest of the fan-out. The orchestrator catches throws as a defensive guard, but this should be treated as a contract violation.

Edge Function deployment + config

Function entrypoint: packages/feedback/supabase/functions/ingest-feedback/index.ts

Secrets/runtime env

Used by code:

  • SUPABASE_URL (runtime; expected in function environment)
  • SUPABASE_SERVICE_ROLE_KEY (runtime; expected in function environment)
  • SUPABASE_ANON_KEY (runtime; required for bearer token validation in auth_mode='authenticated')
  • PRODUCTIVE_API_TOKEN (required)
  • PRODUCTIVE_ORGANIZATION_ID (required)

If Productive secrets are missing/invalid, submissions are still audited and returned as status: "failed" with sync_error persisted.

Deploy notes

  • Prefer the packaged-workdir or copied-workdir CLI flow in "Supabase backend setup" above.
  • supabase/config.toml sets [functions.ingest-feedback].verify_jwt = false; do not rely on the default JWT gateway check for this function.
  • You can deploy via the Supabase dashboard using the same function source and secret values.
  • Ensure browser access mode matches your auth plan. The widget always sends credentials: "omit" and only sends JWT auth when authToken is configured.
  • Ensure migrations above are applied before traffic.

Operational behavior (audit/status semantics)

For accepted requests (valid project key/origin/auth mode):

  1. Insert feedback_submissions row immediately with status = 'received'.
  2. Upload screenshot to feedback-screenshots (<widget_project_id>/<submission_id>.png).
  3. Build a channel-agnostic markdown description, including a 30-day signed screenshot URL when available.
  4. Load active rows from widget_project_channels for this project; fan out via the channel orchestrator (concurrent).
  5. For each adapter result, write one feedback_dispatches row (status synced or failed, with external_id / external_url on success and sync_error on failure).
  6. Update the submission row with the rolled-up status (received | synced | partial | failed) and a short sync_error summary.
  7. Return 200 with { id, status }.

This guarantees an audit record even when downstream sync fails, and per-channel detail is queryable from feedback_dispatches.

Error contract

The widget treats persistence to Supabase as the single point of success. Once a submission is durably stored, the user is shown the success toast regardless of what happens to downstream side effects. This is intentional: the user has done their job; integration plumbing should never punish them for it.

Concretely:

| Stage | Failure → user-visible? | Failure → onError? | Failure → result.status | | ----------------------------------------------------------------------------- | ------------------------------------- | -------------------- | ---------------------------------------------------------------------------- | | Network / fetch reject | yes (error banner) | yes | n/a | | Edge function returns non-2xx (validation, rate limit, auth, insert_failed) | yes (error banner) | yes | n/a | | Supabase row insert fails | yes (returned as 500 insert_failed) | yes | n/a | | Storage screenshot upload | no | no | rolled-up status; sync_error prefixed screenshot: | | Signed URL generation | no | no | rolled-up status | | Channel dispatch (any adapter — task create, webhook POST, etc.) | no | no | "partial" or "failed", with per-channel feedback_dispatches.sync_error | | Channel attachment / follow-up step | no | no | "partial" or "failed"; Productive prefixes attachment: | | Adapter throws (contract violation) | no | no | "partial" or "failed"; orchestrator records as a failed dispatch | | Any other unexpected error after row insert | no | no | "failed", with sync_error |

Where the contract is enforced:

  • packages/feedback/supabase/functions/ingest-feedback/index.ts — the whole post-insert side-effect block is wrapped in a defensive try/catch. Even an unanticipated runtime error after the audit row is created will return 200 { id, status: "failed" } with the failure reason persisted in feedback_submissions.sync_error.
  • packages/feedback/src/transport/submit.ts — only throws on non-2xx or a malformed response body. status: "failed" from a 200 response is not an error.
  • packages/feedback/src/widget/Composer.tsx — calls onSubmitted(result) on every 2xx and shows the success toast; result.status is forwarded to the integrator but never rendered as a user-facing error.

Operating implications:

  • Channel outages, schema drift, attachment-size limits, and similar third-party issues do not degrade the reporter UX. They produce status: 'partial' or 'failed' rows in feedback_submissions and per-channel feedback_dispatches rows for triage.
  • onError is only called for failures that prevent durable storage. Wire it to your error monitoring; do not treat it as a sync-health signal — query feedback_dispatches (filtered by status='failed') for that.
  • New integrations land as channel adapters (see "Adding a new channel kind" above) rather than ad-hoc additions to the post-insert block. The orchestrator already provides the silent-failure guarantee for any adapter registered in channels/registry.ts.

Rate limiting, CORS, auth mode

From ingest-feedback/index.ts:

  • Rate limit: 30 requests per 5 minutes per (widget_project_id, client_ip) bucket via consume_rate_limit. If the RPC fails, ingestion now fails closed with 503 rate_limit_unavailable.
  • CORS:
    • Preflight: wildcard (*) response.
    • POST: project-specific check against widget_projects.allowed_origins.
    • If allowed_origins is empty, origin enforcement is skipped.
  • Auth mode enforcement (widget_projects.auth_mode):
    • authenticated: requires a valid Authorization: Bearer <Supabase access token>. reporter.id from payload is ignored; verified auth user id is used.
    • anonymous: rejects when payload.reporter.id is present.
    • both: accepts anonymous submissions, but uses verified auth user id when a valid bearer token is sent.

Client integration lifecycle

  • Call Feedback.init once when your app shell mounts. In React StrictMode, guard the effect so development double-invocation does not reinitialize capture buffers.
  • On route changes, call Feedback.setMetadata to update per-submission context without tearing down global console/network capture or re-mounting the widget.
  • Call Feedback.shutdown() only when the app shell is truly unmounting; it restores patched globals.

Troubleshooting

| Symptom | Likely cause | Where to check | | -------------------------------------------------- | ------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------- | | unknown_project_key (404) | Missing/inactive widget_projects row or wrong projectKey | widget_projects.project_key, is_active | | origin_not_allowed (403) | Request origin not in allowed_origins | widget_projects.allowed_origins | | authenticated_required (401) | auth_mode='authenticated' without a valid bearer token | Authorization header + auth_mode | | anonymous_only (400) | auth_mode='anonymous' with reporter.id | Payload reporter + auth_mode | | rate_limited (429) | Bucket exceeded 30/5min | rate_limits table + traffic source | | rate_limit_unavailable (503) | Rate-limit RPC errored | Edge logs + consume_rate_limit function | | auth_unavailable (500) | Missing SUPABASE_ANON_KEY in authenticated mode | Function env secrets | | insert_failed (500) | DB write failed | Edge logs + table schema/migrations | | Client receives status: failed / partial (200) | One or more channel dispatches failed | feedback_dispatches.sync_error (per channel), feedback_submissions.sync_error summary, Edge logs | | Screenshot missing in destination | Storage upload or attachment flow failed | screenshot_path, feedback_dispatches.sync_error, channel API logs | | status: received despite a configured channel | No active row in widget_project_channels for the project, or widget_project_channels lookup errored | widget_project_channels (is_active, widget_project_id), feedback_submissions.sync_error |

Tests

The package ships three test tiers (Vitest):

| Tier | Command | What it covers | Needs Docker? | | ----------- | ----------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------- | | Unit | pnpm test | Pure logic in src/, the orchestrator, the Productive API client + adapter, the description template, capture utilities, transport submit. All against in-process mocks. | No | | Integration | pnpm test:integration | Migrations applied to a real Postgres (Testcontainers): schema + constraints, the unique partial index on widget_project_channels, idempotent backfill blocks for 0009/0010, the consume_rate_limit RPC under burst + window rollover, and the orchestrator persisting to the real feedback_dispatches table. | Yes | | E2E | pnpm test:e2e | A realistic submission flow: real Postgres + an in-process Productive HTTP mock running the full 4-step attach dance. Drives the actual orchestrator + Productive adapter through happy path, createTask 422, attachment-link 502, multi-channel partial rollup, and missing-env failure. | Yes | | All three | pnpm test:all | The above, in order. | Yes |

pnpm typecheck runs both tsconfig.json (production sources) and tsconfig.test.json (which adds the tests/ directory and the Node-compatible Edge Function modules to the typecheck graph).

What's intentionally not covered

The thin Deno HTTP shell (supabase/functions/ingest-feedback/index.ts — CORS, multipart parsing, auth header extraction, rate-limit RPC wiring) is transport code that wires already-tested pieces. To exercise it, run supabase start && supabase functions serve locally and POST a multipart submission directly. There's no automated tier for it; if the shell ever grows logic, add a Deno-runtime test under supabase/functions/tests/.

Adding a new channel adapter — what to test

For each new adapter:

  • Unit: cover parseConfig (every required field + defaults) and dispatch (success path, every failure path returning a DispatchFailure). Mock the API client at the module boundary.
  • E2E: spin up an in-process mock of the destination's API, register the real adapter in the orchestrator, and assert feedback_dispatches.external_id / external_url + the submission rollup status.

The orchestrator's tests already cover fan-out, partial rollup, and adapter-throws-defensively semantics — adapters don't need to re-test those.

Known limitations / follow-ups

  • No automatic retry worker for failed channel dispatches; triage is via feedback_dispatches.status='failed'.
  • No PII redaction/masking in console, network, or custom metadata payloads.
  • Console/network buffers are process-local ring buffers and are not cleared per submission.
  • No session replay or two-way sync from any channel.