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

jat-feedback

v1.5.0

Published

Embeddable feedback widget for bug reports and feature requests. Captures screenshots, console logs, and user context as a web component.

Readme

jat-feedback

Embeddable feedback widget for bug reports and feature requests. Ships as a web component (<jat-feedback>) that captures screenshots, console logs, and user context.

Reports are stored in your Supabase database. The JAT ingest daemon polls for new rows and creates tasks automatically.

User clicks "Report Bug" → Widget captures context → POST /api/feedback/report
  → Supabase feedback_reports table → JAT ingest daemon → JAT task created

Install

npm install jat-feedback

The package includes the widget bundle, Supabase migration, and edge function — all three are needed for the full pipeline.

Widget Attributes

| Attribute | Required | Default | Description | |-----------|----------|---------|-------------| | endpoint | Yes | '' | Base URL for the report API (usually location.origin) | | project | Yes | '' | Project identifier (e.g., "my-app") | | position | No | 'bottom-right' | Widget position: bottom-right, bottom-left, top-right, top-left | | theme | No | 'dark' | Color theme: light, dark, auto | | buttoncolor | No | '#3b82f6' | Hex color for the floating button | | user-id | No | '' | Authenticated user's ID | | user-email | No | '' | Authenticated user's email | | user-name | No | '' | Authenticated user's display name | | user-role | No | '' | User's role (e.g., admin, user) | | org-id | No | '' | Organization/tenant ID | | org-name | No | '' | Organization name |

What the Widget Captures

Each report includes:

  • Title and description (user-provided)
  • Type: bug, enhancement, or other
  • Priority: low, medium, high, critical
  • Screenshots: Full-page captures as base64 data URLs
  • Console logs: Recent console output (errors, warnings, logs)
  • Selected elements: DOM elements the user highlighted
  • Page URL and user agent
  • User context: All user-* and org-* attributes

Programmatic Control

Open the widget from a button in your app:

window.dispatchEvent(new CustomEvent('jat-feedback:open'));

Integration Guide: SvelteKit + Supabase

Full setup for any SvelteKit app with Supabase. Creates the feedback pipeline from widget to JAT task.

Prerequisites

  • SvelteKit app with Supabase auth
  • supabaseServiceRole available on locals (server-side)
  • JAT installed with ingest daemon configured

Step 1: Add Widget to app.html

The widget is a web component bundled at dist/jat-feedback.js. Use vite-plugin-static-copy to copy it into your build output, then load it from your own server.

vite.config.ts:

import { sveltekit } from '@sveltejs/vite-plugin-svelte';
import { viteStaticCopy } from 'vite-plugin-static-copy';

export default {
  plugins: [
    sveltekit(),
    viteStaticCopy({
      targets: [
        {
          src: 'node_modules/jat-feedback/dist/jat-feedback.js',
          dest: '.'
        }
      ]
    })
  ]
};

src/app.html:

<!-- before closing </body> tag -->
<script src="/jat-feedback.js"></script>
<jat-feedback project="YOUR_PROJECT"></jat-feedback>
<script>
  (function() {
    var el = document.querySelector('jat-feedback');
    if (el) el.setAttribute('endpoint', location.origin);
  })();
</script>

Step 2: Create API Endpoint

Create src/routes/api/feedback/report/+server.ts:

import { json } from '@sveltejs/kit';
import type { RequestHandler } from './$types';

const CORS_HEADERS = {
  'Access-Control-Allow-Origin': '*',
  'Access-Control-Allow-Methods': 'GET, POST, OPTIONS',
  'Access-Control-Allow-Headers': 'Content-Type',
  'Access-Control-Max-Age': '86400'
};

export const OPTIONS: RequestHandler = async () => {
  return new Response(null, { status: 204, headers: CORS_HEADERS });
};

export const GET: RequestHandler = async () => {
  return json(
    { status: 'ok', service: 'feedback-report', timestamp: new Date().toISOString() },
    { headers: CORS_HEADERS }
  );
};

export const POST: RequestHandler = async ({ request, locals }) => {
  try {
    const body = await request.json();

    if (!body.title || typeof body.title !== 'string' || body.title.trim() === '') {
      return json({ ok: false, error: 'Title is required' }, { status: 400, headers: CORS_HEADERS });
    }

    const supabase = locals.supabaseServiceRole;

    // Upload screenshots to Storage
    const screenshotPaths: string[] = [];
    if (body.screenshots && Array.isArray(body.screenshots)) {
      for (let i = 0; i < body.screenshots.length; i++) {
        const dataUrl = body.screenshots[i];
        if (typeof dataUrl !== 'string' || !dataUrl.startsWith('data:')) continue;

        const [header, base64] = dataUrl.split(',');
        if (!base64) continue;

        const mime = header.match(/:(.*?);/)?.[1] || 'image/png';
        const ext = mime === 'image/png' ? 'png' : 'jpg';
        const filename = `${Date.now()}-${crypto.randomUUID().slice(0, 8)}.${ext}`;
        const filePath = `reports/${filename}`;

        const binaryStr = atob(base64);
        const buffer = new Uint8Array(binaryStr.length);
        for (let j = 0; j < binaryStr.length; j++) {
          buffer[j] = binaryStr.charCodeAt(j);
        }

        const { error: uploadError } = await supabase.storage
          .from('feedback-screenshots')
          .upload(filePath, buffer, { contentType: mime, upsert: false });

        if (uploadError) {
          console.warn(`[feedback] Screenshot upload failed (${i}):`, uploadError.message);
        } else {
          screenshotPaths.push(filePath);
        }
      }
    }

    const reporter = body.metadata?.reporter || {};

    const { data: row, error: insertError } = await supabase
      .from('feedback_reports')
      .insert({
        title: body.title.trim(),
        description: body.description?.trim() || '',
        type: body.type || 'bug',
        priority: body.priority || 'medium',
        page_url: body.page_url || null,
        user_agent: body.user_agent || null,
        reporter_user_id: reporter.userId || null,
        reporter_email: reporter.email || null,
        reporter_name: reporter.name || null,
        reporter_role: reporter.role || null,
        console_logs: body.console_logs || null,
        selected_elements: body.selected_elements || null,
        screenshot_paths: screenshotPaths.length > 0 ? screenshotPaths : null,
        metadata: body.metadata || null
      })
      .select('id')
      .single();

    if (insertError) {
      console.error('[feedback] Insert failed:', insertError);
      return json({ ok: false, error: insertError.message }, { status: 500, headers: CORS_HEADERS });
    }

    return json(
      { ok: true, id: row.id, message: `Report saved (${row.id})` },
      { status: 201, headers: CORS_HEADERS }
    );
  } catch (err) {
    console.error('[feedback] Error:', err);
    return json(
      { ok: false, error: err instanceof Error ? err.message : 'Failed to submit report' },
      { status: 500, headers: CORS_HEADERS }
    );
  }
};

Step 3: Run the Supabase Migration

The feedback_reports table schema is included in this package. Copy it into your migrations folder and push:

# Copy the migration (rename to match your timestamp convention)
cp node_modules/jat-feedback/supabase/migrations/1.0.0_feedback_reports.sql \
   supabase/migrations/$(date +%Y%m%d%H%M%S)_feedback_reports.sql

# Push to Supabase
supabase db push

When upgrading jat-feedback: check node_modules/jat-feedback/supabase/migrations/ for new versioned files (e.g. 1.1.0_*.sql) and copy+apply any you haven't run yet.

Step 4: Wire User Context

In your authenticated layout, set widget attributes after login so reports include user identity.

SvelteKit example (in your auth layout's onMount):

onMount(() => {
  const user = session?.user;
  if (user) {
    const el = document.querySelector('jat-feedback');
    if (el) {
      if (user.id) el.setAttribute('user-id', user.id);
      if (user.email) el.setAttribute('user-email', user.email);
      // Adjust based on where your app stores display name:
      if (user.user_metadata?.full_name) el.setAttribute('user-name', user.user_metadata.full_name);
      if (user.user_metadata?.role) el.setAttribute('user-role', user.user_metadata.role);
    }
  }
});

Optional: Add a "Report Bug" button anywhere in your UI:

<button onclick={() => window.dispatchEvent(new CustomEvent('jat-feedback:open'))}>
  Report Bug
</button>

Step 5: Configure JAT Ingest Daemon

This connects the Supabase table to JAT so new reports automatically become tasks.

5a. Store Supabase credentials:

If you've already configured Supabase project secrets in IDE Settings → Project Secrets, you can skip this step — the ingest daemon will auto-resolve supabase_url and supabase_service_role_key from project secrets.

Otherwise, store the service role key manually:

jat-secret --set YOUR_PROJECT-supabase-service-role "eyJhbGci..." \
  --desc "Supabase service role key for YOUR_PROJECT feedback ingest"

5b. Add source to ~/.config/jat/integrations.json:

Add this entry to the sources array.

Minimal config (when project secrets are configured in IDE Settings):

{
  "id": "YOUR_PROJECT-feedback",
  "type": "supabase",
  "enabled": true,
  "project": "YOUR_PROJECT",
  "pollInterval": 120,
  "taskDefaults": {
    "type": "bug",
    "priority": 2,
    "labels": ["widget", "feedback"]
  },
  "table": "feedback_reports",
  "statusColumn": "status",
  "statusNew": "submitted",
  "taskIdColumn": "jat_task_id",
  "titleColumn": "title",
  "descriptionTemplate": "**Reporter:** {reporter_name} ({reporter_email}) — {reporter_role}\n**Page:** {page_url}\n**Browser:** {user_agent}\n\n{description}",
  "authorColumn": "reporter_email",
  "timestampColumn": "created_at",
  "attachmentColumn": "screenshot_paths",
  "storageBucket": "feedback-screenshots"
}

Note: projectUrl and secretName are omitted above. The adapter resolves them automatically from IDE Settings → Project Secrets (supabase_url and supabase_service_role_key).

Full config (with explicit projectUrl and secretName):

{
  "id": "YOUR_PROJECT-feedback",
  "type": "supabase",
  "enabled": true,
  "project": "YOUR_PROJECT",
  "pollInterval": 120,
  "taskDefaults": {
    "type": "bug",
    "priority": 2,
    "labels": ["widget", "feedback"]
  },
  "projectUrl": "https://YOUR_SUPABASE_PROJECT_ID.supabase.co",
  "secretName": "YOUR_PROJECT-supabase-service-role",
  "table": "feedback_reports",
  "statusColumn": "status",
  "statusNew": "submitted",
  "taskIdColumn": "jat_task_id",
  "titleColumn": "title",
  "descriptionTemplate": "**Reporter:** {reporter_name} ({reporter_email}) — {reporter_role}\n**Page:** {page_url}\n**Browser:** {user_agent}\n\n{description}",
  "authorColumn": "reporter_email",
  "timestampColumn": "created_at",
  "attachmentColumn": "screenshot_paths",
  "storageBucket": "feedback-screenshots"
}

Optional fields you can add:

{
  "automation": {
    "action": "delay",
    "command": "/jat:start",
    "delay": 5,
    "delayUnit": "minutes"
  },
  "callback": {
    "url": "https://YOUR_SUPABASE_PROJECT_ID.supabase.co/functions/v1/jat-webhook",
    "events": ["status_changed", "task_closed"],
    "statusMapping": {
      "open": "submitted",
      "in_progress": "in_progress",
      "closed": "completed"
    },
    "referenceTable": "feedback_reports",
    "referenceIdFrom": "item_id"
  },
  "actions": [
    {
      "id": "sync_status",
      "label": "Sync Status",
      "description": "Push current task status to Supabase",
      "type": "callback",
      "event": "status_changed",
      "icon": "refresh"
    },
    {
      "id": "open_record",
      "label": "View in Supabase",
      "description": "Open the original feedback report in Supabase dashboard",
      "type": "link",
      "urlTemplate": "{projectUrl}/project/default/editor/feedback_reports?filter=id%3Deq.{referenceId}",
      "icon": "external-link"
    }
  ]
}
  • automation: Auto-spawn an agent when a report is ingested
  • callback: Push JAT task status changes back to Supabase — requires deploying the jat-webhook edge function (see Step 6)
  • actions: Adds "Sync Status" and "View in Supabase" buttons in the JAT IDE task panel

The ingest daemon picks up config changes automatically (no restart needed).

Step 6: Deploy the JAT Webhook Edge Function (for callbacks)

The jat-webhook Supabase Edge Function receives status-change callbacks from JAT and updates your feedback_reports rows. It's included in this package — copy it into your project and deploy it.

# Copy the function into your project
mkdir -p supabase/functions/jat-webhook
cp node_modules/jat-feedback/supabase/functions/jat-webhook/index.ts \
   supabase/functions/jat-webhook/index.ts

# Deploy to Supabase
supabase functions deploy jat-webhook

The function uses SUPABASE_URL and SUPABASE_SERVICE_ROLE_KEY — both are injected automatically by Supabase, no extra configuration needed.

Skip this step if you don't need bidirectional status sync (i.e., you only want JAT to ingest reports, not push status back to Supabase).

Step 7: Verify

# Check the API endpoint is working
curl http://localhost:5173/api/feedback/report
# → {"status":"ok","service":"feedback-report","timestamp":"..."}

# Check ingest daemon sees the source
jat ingest status
# → Should list your project's feedback source

# Submit a test report via the widget, then check:
# 1. Row appears in feedback_reports table
# 2. JAT task gets created after next poll cycle (pollInterval seconds)

Pipeline Overview

┌──────────────┐     ┌──────────────┐     ┌──────────────┐     ┌──────────────┐
│  jat-feedback │────►│  Your App    │────►│   Supabase   │────►│  JAT Ingest  │
│  (widget)     │     │  /api/report │     │  feedback_   │     │  Daemon      │
│              │     │              │     │  reports     │     │              │
└──────────────┘     └──────────────┘     └──────┬───────┘     └──────┬───────┘
                                                 ▲                    │
                                                 │                    ▼
                                          ┌──────┴───────┐   ┌──────────────┐
                                          │  jat-webhook  │◄──│  JAT Task    │
                                          │  (edge fn)    │   │  status      │
                                          │  status sync  │   │  changes     │
                                          └──────────────┘   └──────────────┘

Column Reference

The feedback_reports table columns used by the pipeline:

| Column | Type | Purpose | |--------|------|---------| | status | TEXT | Lifecycle: submittedin_progresscompletedaccepted | rejected | | jat_task_id | TEXT | JAT task ID written back after ingest (e.g., myapp-abc) | | rejection_reason | TEXT | User-provided reason when rejecting a completed report | | dev_notes | TEXT | Developer notes pushed back via callback |

Upgrading

npm never auto-updates — you need to explicitly upgrade and then handle each type of change manually.

# 1. Update the package
npm install jat-feedback@latest

Then handle whatever changed in the release notes:

Widget JS changes (UI, behavior, new attributes):

# Nothing extra — the updated bundle is copied to your build output automatically
# Just redeploy your app

Schema changes (new columns, indexes):

# Check for new migration files
ls node_modules/jat-feedback/supabase/migrations/

# Copy any new ones into your project and run them
cp node_modules/jat-feedback/supabase/migrations/1.1.0_*.sql \
   supabase/migrations/$(date +%Y%m%d%H%M%S)_feedback_1_1_0.sql

supabase db push

Edge function changes (webhook behavior):

# Re-copy and redeploy
cp node_modules/jat-feedback/supabase/functions/jat-webhook/index.ts \
   supabase/functions/jat-webhook/index.ts

supabase functions deploy jat-webhook

Versioning

This package follows semver. The ^ range in consuming projects ("jat-feedback": "^1.1.0") means:

  • Patch and minor (1.1.x, 1.2.0) — auto-accepted by npm install
  • Major (2.0.0) — requires manual version bump in package.json

What triggers a major version

| Change | Version | |--------|---------| | New nullable column (additive) | patch/minor | | New widget attribute (optional) | minor | | Removing or renaming a column | major | | Changing a column's type | major | | Renaming status values (e.g. submittednew) | major | | Required integrations.json config field added/renamed | major |

Rule for additive schema changes

Any column added in a 1.x release must be nullable with no required default. This ensures consuming projects don't break even if they haven't run the migration yet — the insert just omits the column and it lands as NULL.

If a new column is required (non-nullable, no default), that's a breaking change and belongs in a major version.

License

MIT