inner-lens
v0.4.9
Published
Self-Debugging QA Agent - Universal bug reporting widget with AI-powered analysis for any frontend framework
Maintainers
Readme
Installation
npm install inner-lens
# or
yarn add inner-lens
# or
pnpm add inner-lens
# Optional: With Session Replay (see below)
npm install inner-lens [email protected]
# or
yarn add inner-lens [email protected]
# or
pnpm add inner-lens [email protected]Quick Start
npx inner-lens initOr manually:
React / Next.js:
- Configure build to inject git branch:
// next.config.js
const { getGitBranch } = require('inner-lens/build');
module.exports = {
env: {
NEXT_PUBLIC_GIT_BRANCH: getGitBranch(),
},
};- Add the widget:
import { InnerLensWidget } from 'inner-lens/react';
export default function App() {
return (
<>
<YourApp />
<InnerLensWidget
mode="hosted"
repository="your-org/your-repo"
branch={process.env.NEXT_PUBLIC_GIT_BRANCH}
/>
</>
);
}Vue 3 (Vite):
- Configure build:
// vite.config.js
import { defineConfig } from 'vite';
import vue from '@vitejs/plugin-vue';
import { getGitBranch } from 'inner-lens/build';
export default defineConfig({
plugins: [vue()],
define: {
'import.meta.env.VITE_GIT_BRANCH': JSON.stringify(getGitBranch()),
},
});- Add the widget:
<script setup>
import { InnerLensWidget } from 'inner-lens/vue';
</script>
<template>
<YourApp />
<InnerLensWidget
mode="hosted"
repository="your-org/your-repo"
:branch="import.meta.env.VITE_GIT_BRANCH"
/>
</template>Vanilla JS (Vite):
- Configure build (same as Vue)
- Add the widget:
import { InnerLens } from 'inner-lens/vanilla';
const widget = new InnerLens({
mode: 'hosted',
repository: 'your-org/your-repo',
branch: import.meta.env.VITE_GIT_BRANCH,
});
widget.mount();Note: The
branchprop tells the AI analysis engine which code version to analyze. If you only deploy frommain, you can omit it (defaults tomain). ThegetGitBranch()utility auto-detects branch from CI/CD environment variables (Vercel, Netlify, AWS Amplify, Cloudflare Pages, Render, Railway, GitHub Actions, Heroku).
Why inner-lens?
Bug reports like "it doesn't work" waste hours of debugging time.
| Without inner-lens | With inner-lens | |-------------------|-----------------| | "The button doesn't work" | Console logs, network errors, DOM state, session replay | | Hours of back-and-forth | One-click bug reports with full context | | Manual log collection | Automatic capture with PII masking | | Guessing what happened | AI-powered root cause analysis |
How It Works
User clicks "Report Bug"
↓
Widget captures context (logs, actions, performance, DOM)
↓
GitHub Issue created with full context
↓
AI analyzes code & identifies root cause
↓
Analysis posted as comment with fix suggestionsHosted vs Self-Hosted
| | Hosted (Recommended) | Self-Hosted |
|---|:---:|:---:|
| Setup Time | 2 minutes | 10 minutes |
| Backend Required | No | Yes |
| Issue Author | inner-lens-app[bot] | Your GitHub account |
| Rate Limit | 10 req/min/IP + 100 req/day per repo | None |
Hosted Mode
- Install GitHub App
- Add widget with
mode="hosted"andrepositoryprop
<InnerLensWidget mode="hosted" repository="owner/repo" />Self-Hosted Mode
- Create GitHub Token
- Add backend handler:
// Next.js App Router
import { createFetchHandler } from 'inner-lens/server';
export const POST = createFetchHandler({
githubToken: process.env.GITHUB_TOKEN!,
repository: 'owner/repo',
});- Add widget with
mode="self-hosted"andendpointprop:
<InnerLensWidget
mode="self-hosted"
endpoint="/api/inner-lens/report"
repository="owner/repo"
/>For external API servers (Cloudflare Workers, separate domain, etc.), use fullUrl:
<InnerLensWidget
mode="self-hosted"
fullUrl="https://api.example.com/inner-lens/report"
repository="owner/repo"
/>// Express
import { createExpressHandler } from 'inner-lens/server';
app.post('/api/report', createExpressHandler({ githubToken, repository }));
// Fastify
import { createFastifyHandler } from 'inner-lens/server';
fastify.post('/api/report', createFastifyHandler({ githubToken, repository }));
// Hono / Bun / Deno
import { createFetchHandler } from 'inner-lens/server';
app.post('/api/report', (c) => createFetchHandler({ githubToken, repository })(c.req.raw));
// Koa
import { createKoaHandler } from 'inner-lens/server';
router.post('/api/report', createKoaHandler({ githubToken, repository }));
// Node.js HTTP
import { createNodeHandler } from 'inner-lens/server';
const handler = createNodeHandler({ githubToken, repository });AI Analysis
Enable AI-powered bug analysis with GitHub Actions.
# .github/workflows/inner-lens.yml
name: inner-lens Analysis
on:
issues:
types: [opened]
jobs:
analyze:
if: contains(github.event.issue.labels.*.name, 'inner-lens')
uses: jhlee0409/inner-lens/.github/workflows/analysis-engine.yml@v1
with:
provider: 'anthropic' # or 'openai', 'google'
secrets:
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}| Provider | Model (example) | Secret | Notes |
|----------|-----------------|--------|-------|
| Anthropic | claude-sonnet-4-5-20250929 | ANTHROPIC_API_KEY | model is optional; omit to use provider default |
| OpenAI | gpt-4o | OPENAI_API_KEY | model is optional; omit to use provider default |
| Google | gemini-2.5-flash | GOOGLE_GENERATIVE_AI_API_KEY | model is optional; omit to use provider default |
Workflow Options
| Option | Required | Type | Default | Description |
|--------|:--------:|------|---------|-------------|
| provider | No | string | anthropic | AI provider (anthropic, openai, google) |
| model | No | string | '' | Optional model name (e.g., claude-sonnet-4-20250514); empty string uses provider default |
| language | No | string | en | Analysis output language (en, ko, ja, zh, es, de, fr, pt) |
| max_files | No | number | 25 | Maximum files to analyze (5-50) |
Secrets (required based on provider):
| Secret | Required |
|--------|----------|
| ANTHROPIC_API_KEY | When provider: 'anthropic' |
| OPENAI_API_KEY | When provider: 'openai' |
| GOOGLE_GENERATIVE_AI_API_KEY | When provider: 'google' |
jobs:
analyze:
if: contains(github.event.issue.labels.*.name, 'inner-lens')
uses: jhlee0409/inner-lens/.github/workflows/analysis-engine.yml@v1
with:
provider: 'anthropic'
model: 'claude-sonnet-4-20250514'
language: 'ko'
max_files: 30
secrets:
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}Session Replay
Record DOM changes to visually replay what the user experienced.
Why use it? See exactly what the user saw — clicks, scrolls, and UI changes — without asking them to reproduce the issue.
When to use it? Complex UI bugs that are hard to reproduce from logs alone.
Note: Adds ~500KB to your bundle. Only enable if needed.
npm install [email protected]<InnerLensWidget mode="hosted" repository="owner/repo" captureSessionReplay={true} />Configuration
Migration from v0.4.7 or earlier: The
modeprop is now required.
- If using hosted API: add
mode="hosted"- If using custom endpoint: add
mode="self-hosted"
| Option | Type | Default | Description |
|--------|------|---------|-------------|
| mode | 'hosted' \| 'self-hosted' | required | API mode (see below) |
| repository | string | - | GitHub repo (owner/repo) |
| endpoint | string | - | Relative backend URL (self-hosted only) |
| fullUrl | string | - | Absolute backend URL (self-hosted only) |
| branch | string | - | Git branch for AI analysis |
| language | string | en | UI language (en, ko, ja, zh, es) |
| position | string | bottom-right | Button position |
| buttonColor | string | #6366f1 | Button color |
| buttonSize | sm\|md\|lg | lg | Trigger button size |
| styles | { buttonColor?, buttonPosition?, buttonSize? } | - | Advanced style config (overrides position/color/size) |
| hidden | boolean | false | Hide widget |
| disabled | boolean | false | Disable widget |
| captureSessionReplay | boolean | false | Enable DOM recording |
| reporter | object | - | User info { name, email?, id? } |
| Option | Type | Default |
|--------|------|---------|
| labels | string[] | ['inner-lens'] |
| captureConsoleLogs | boolean | true |
| maxLogEntries | number | 50 |
| maskSensitiveData | boolean | true |
| captureUserActions | boolean | true |
| captureNavigation | boolean | true |
| capturePerformance | boolean | true |
| captureSessionReplay | boolean | false |
| styles | { buttonColor?, buttonPosition?, buttonSize? } | - |
| buttonSize | sm\|md\|lg | lg |
| buttonText | string | i18n |
| dialogTitle | string | i18n |
| dialogDescription | string | i18n |
| submitText | string | i18n |
| cancelText | string | i18n |
| successMessage | string | i18n |
| trigger | ReactNode | - |
| onOpen | () => void | - |
| onClose | () => void | - |
| onSuccess | (url) => void | - |
| onError | (error) => void | - |
Security & Privacy
Data Flow
┌─────────────────────────────────────────────────────────────────────────┐
│ Your Application (Browser) │
│ ┌─────────────┐ ┌──────────────┐ ┌─────────────────────────────┐ │
│ │ User clicks │───►│ Widget │───►│ Client-side Masking │ │
│ │ Report Bug │ │ captures │ │ (30 patterns applied) │ │
│ └─────────────┘ │ context │ │ • Emails → [EMAIL_REDACTED] │ │
│ └──────────────┘ │ • API keys → [*_REDACTED] │ │
│ │ • Tokens → [TOKEN_REDACTED] │ │
│ └──────────────┬──────────────┘ │
└────────────────────────────────────────────────────────┼────────────────┘
│
▼
┌───────────────────────────────────────────┐
│ inner-lens API (Pass-through) │
│ • No data storage │
│ • No logging of report content │
│ • Rate limiting only │
└───────────────────────┬───────────────────┘
│
▼
┌───────────────────────────────────────────┐
│ GitHub Issues API │
│ • Issue created in YOUR repository │
│ • Data stored only in GitHub │
│ • Access controlled by your repo perms │
└───────────────────────────────────────────┘What We Collect
| Data Type | Purpose | Privacy Notes | |-----------|---------|---------------| | Console logs | Debug errors & warnings | Auto-masked for sensitive content | | User actions | Understand user journey | Generic selectors only (no personal text) | | Navigation history | Track page flow | URLs only (query params masked) | | Browser & OS info | Environment debugging | Summarized (e.g., "Chrome 120", "Windows 10") | | Performance metrics | Identify slow operations | Timing data only (LCP, FCP, etc.) | | DOM snapshot (opt-in) | Visual debugging | Session replay disabled by default |
What We DON'T Collect
- ❌ IP addresses — Not logged or stored
- ❌ Cookies — Never accessed
- ❌ localStorage/sessionStorage — Never read
- ❌ Geolocation — Not requested
- ❌ Device fingerprints — No unique identifiers
- ❌ Form field values — Input content excluded
- ❌ Full User Agent string — Only browser/OS summary
Automatic Masking (30+ Patterns)
Sensitive data is masked client-side, before transmission:
| Category | Replacement |
|----------|-------------|
| Email, Phone, SSN | [EMAIL_REDACTED], [PHONE_REDACTED], [SSN_REDACTED] |
| Credit cards | [CARD_REDACTED] |
| Auth tokens, JWTs | [TOKEN_REDACTED], [JWT_REDACTED] |
| API keys (AWS, OpenAI, Anthropic, Google, Stripe, GitHub) | [*_KEY_REDACTED] |
| Database URLs, Private keys | [DATABASE_URL_REDACTED], [PRIVATE_KEY_REDACTED] |
| Discord webhooks, Slack tokens | [DISCORD_WEBHOOK_REDACTED], [SLACK_TOKEN_REDACTED] |
| NPM, SendGrid, Twilio | [NPM_TOKEN_REDACTED], [SENDGRID_KEY_REDACTED], [TWILIO_REDACTED] |
Key Security Principles
- Client-side first — All masking happens in the browser before data leaves
- No data retention — Hosted API is a pass-through; no logs stored
- User-initiated only — Reports sent only when user clicks submit
- Minimal collection — Only debugging-relevant data captured
- Transparent storage — Data goes to YOUR GitHub repo, nowhere else
API Reference
Client
| Package | Exports |
|---------|---------|
| inner-lens/react | InnerLensWidget, useInnerLens |
| inner-lens/vue | InnerLensWidget, useInnerLens |
| inner-lens/vanilla | InnerLens |
Server
| Export | Frameworks |
|--------|------------|
| createFetchHandler | Next.js, Hono, Bun, Deno, Cloudflare |
| createExpressHandler | Express |
| createFastifyHandler | Fastify |
| createKoaHandler | Koa |
| createNodeHandler | Node.js HTTP |
| handleBugReport | Any |
Utilities (Vue bundle re-exports)
- Masking:
maskSensitiveData,maskSensitiveObject,validateMasking - Log capture:
initLogCapture,getCapturedLogs,clearCapturedLogs,addCustomLog,restoreConsole(available frominner-lens/vue)
FAQ
The widget uses browser APIs, so it must run as a client component.
// App Router: Add 'use client' directive
'use client';
import { InnerLensWidget } from 'inner-lens/react';
function BugReportWidget() {
return <InnerLensWidget mode="hosted" repository="owner/repo" />;
}
// Pages Router: Use dynamic import
import dynamic from 'next/dynamic';
const InnerLensWidget = dynamic(
() => import('inner-lens/react').then(m => m.InnerLensWidget),
{ ssr: false }
);Sensitive data (emails, API keys, tokens, etc.) is automatically masked before leaving the browser. In Hosted mode, we don't store any data — it goes directly to GitHub.
- Check if
hidden={true}prop is set - Verify import path is
'inner-lens/react'(or/vue,/vanilla) - Check browser console (F12) for error messages
inner-lens targets ES2022 and works with modern browsers:
- Chrome 94+
- Firefox 93+
- Safari 15.4+
- Edge 94+
Node.js 20+ is required for server-side features.
Contributing
Contributions welcome! See CONTRIBUTING.md.
