Install IssueCapture in Next.js
Add bug reporting to your Next.js app. Works with App Router, Pages Router, Turbopack, and Next.js 13/14/15+.
Quick Summary
Prerequisites
- Next.js 13 or higher
- Jira Cloud account (Software or JSM)
- IssueCapture account (free — includes 10 issues/month)
Step-by-Step Installation
Follow these steps to get up and running in minutes.
Get Your API Key
Sign up for IssueCapture and grab your widget API key
- Go to issuecapture.com/signup and create a free account
- Connect your Jira instance via OAuth (about 30 seconds)
- Open the Widgets page and create a new widget
- Click the "Keys" tab on your widget and copy the API key (starts with "ic_")
Add the API key to .env.local
Store the key in env so it doesn't end up in the repo
- Create .env.local in your project root (if it doesn't exist)
- Use NEXT_PUBLIC_ so the value is inlined for the browser bundle
- Make sure .env.local is in .gitignore (Next.js scaffolds this by default)
- Restart `next dev` after adding or changing env vars
# .env.local
NEXT_PUBLIC_ISSUECAPTURE_API_KEY=ic_your_api_key_hereInstall in app/layout.tsx (App Router)
Drop the widget into your root layout so it loads on every page
- The widget is an ES module — it has to be loaded with `<script type="module">`. Next.js's `<Script>` component doesn't support type="module", so use a raw `<script>` with dangerouslySetInnerHTML
- Place it at the end of `<body>`, after children, so it never blocks first paint
- The default `trigger: "auto"` and styling come from the dashboard — you only need apiKey here
- Works with App Router, Pages Router (in `_app.tsx`), Next.js 13/14/15+, and Turbopack
// app/layout.tsx
export default function RootLayout({
children,
}: {
children: React.ReactNode
}) {
const apiKey = process.env.NEXT_PUBLIC_ISSUECAPTURE_API_KEY
return (
<html lang="en">
<body>
{children}
{/* IssueCapture Widget — ESM module, runs after hydration */}
<script
type="module"
dangerouslySetInnerHTML={{
__html: `
import IssueCapture from 'https://issuecapture.com/widget.js';
IssueCapture.init({ apiKey: '${apiKey}' });
`,
}}
/>
</body>
</html>
)
}Option: use your own button
Skip the floating button and trigger the widget from an element you already render
- `trigger` accepts any CSS selector — a single element or a class that matches many
- When you set `trigger` to a selector, the floating button is not injected
- Clicking any matched element opens the widget modal
// app/layout.tsx
export default function RootLayout({
children,
}: {
children: React.ReactNode
}) {
const apiKey = process.env.NEXT_PUBLIC_ISSUECAPTURE_API_KEY
return (
<html lang="en">
<body>
{children}
{/* Your existing feedback button somewhere in the layout */}
<button id="my-feedback-btn">Report an Issue</button>
<script
type="module"
dangerouslySetInnerHTML={{
__html: `
import IssueCapture from 'https://issuecapture.com/widget.js';
IssueCapture.init({
apiKey: '${apiKey}',
trigger: '#my-feedback-btn',
});
`,
}}
/>
</body>
</html>
)
}Programmatic open from a Client Component
Open the widget from your own React code (e.g. an error boundary)
- `window.IssueCapture` is defined as soon as the widget script runs — guard with `?.` in case the script hasn't loaded yet
- The same component pattern works in React 18 and React 19
- For a full TypeScript declaration of every method (init, on, updateConfig, etc.) see the troubleshooting section
// components/BugReportButton.tsx
'use client'
declare global {
interface Window {
IssueCapture?: {
open: () => void
close: () => void
isOpen: () => boolean
}
}
}
export default function BugReportButton() {
const open = () => window.IssueCapture?.open()
return (
<button onClick={open} className="px-4 py-2 bg-blue-600 text-white rounded">
Report Issue
</button>
)
}Pre-fill the logged-in user (optional)
Save your customers the typing — pass their name/email through
- The values are inlined at render time — works in a server-rendered layout
- If the user signs in client-side later, call `IssueCapture.updateConfig({ user: { name, email } })` instead of re-initialising
- Don't call `IssueCapture.init()` more than once per page — use `updateConfig` for changes
// app/layout.tsx
import { auth } from '@/lib/auth' // your auth helper
export default async function RootLayout({
children,
}: {
children: React.ReactNode
}) {
const apiKey = process.env.NEXT_PUBLIC_ISSUECAPTURE_API_KEY
const session = await auth()
const userJson = JSON.stringify({
name: session?.user?.name ?? '',
email: session?.user?.email ?? '',
})
return (
<html lang="en">
<body>
{children}
<script
type="module"
dangerouslySetInnerHTML={{
__html: `
import IssueCapture from 'https://issuecapture.com/widget.js';
IssueCapture.init({
apiKey: '${apiKey}',
user: ${userJson},
});
`,
}}
/>
</body>
</html>
)
}Verify it loaded
Quick sanity check before you ship it
- Start the dev server: `next dev`
- Open the page — you should see the floating Report Issue button
- Open DevTools console and type `IssueCapture` — you should see the widget object (confirms the script loaded even if an ad blocker is hiding the button)
- Click the button, fill in a test issue, and submit
- Open Jira and confirm the issue landed in the configured project
Troubleshooting
Common integration issues and how to solve them.
TypeScript: Property 'IssueCapture' does not exist on type 'Window'
Add a global declaration to your project (e.g. types/issuecapture.d.ts)
- Create `types/issuecapture.d.ts` and declare `IssueCapture` on the global `Window` interface
- Reference it in `tsconfig.json` under `include` (Next.js picks up `types/**` by default)
- See the IssueCaptureAPI surface in the docs for the full method list (init, open, close, on, updateConfig, destroy)
Widget script doesn't load (no `IssueCapture` global)
- Make sure you used `<script type="module">` — the widget is ESM. A plain `<Script src=...>` will not execute it correctly
- Check the browser Network tab for a 200 on `widget.js`
- Verify `NEXT_PUBLIC_ISSUECAPTURE_API_KEY` is exposed at build time — log `process.env.NEXT_PUBLIC_ISSUECAPTURE_API_KEY` in your layout to confirm
- Restart `next dev` if you just added the env var
- Disable ad blockers when testing — some block any script that mentions "widget"
"Domain not allowed" error in the console
- In the IssueCapture dashboard, open your widget and click the Domains tab
- Add `localhost:3000` for local dev (or whichever port you use)
- Add your production and preview deployment domains (see the Vercel guide for wildcard tips)
- Domains match on the browser's Origin header — subdomains need to be listed individually
Pages Router (legacy `_app.tsx`)
- The same `<script type="module">` pattern works — put it in `_app.tsx` inside the root component's JSX
- Use `useEffect` if you need to render the script tag only on the client
- App Router is the recommended setup for new projects
Ready to get started?
Free plan includes 10 issues/month. No card needed — connect Jira and you're done.