Install IssueCapture in Angular 15+
Add bug reporting to your Angular app with environment-aware config, SSR support, and TypeScript. Works with standalone components, NgModules, Angular Router, and Angular Universal.
Quick Summary
Prerequisites
- Angular 15 or higher
- TypeScript 4.9+
- 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 your environment files
Angular uses environment.ts and environment.prod.ts for build-time config via fileReplacements
- The Angular CLI automatically swaps environment.ts → environment.prod.ts at compile-time via the fileReplacements rule in angular.json
- This is safer than runtime .env: the key is inlined into the bundle during build, not read from disk at runtime (no accidental leaks)
- You can use separate API keys for dev and production — useful when testing against different Jira instances
- Don't commit production keys to a public repo — load environment.prod.ts values via CI secrets or a CI step that overwrites the file before build
- The fileReplacements rule is set up automatically by `ng new` — no manual config needed
// src/environments/environment.ts (dev)
export const environment = {
production: false,
issueCaptureApiKey: 'ic_your_api_key_here',
};
// src/environments/environment.prod.ts (production)
export const environment = {
production: true,
issueCaptureApiKey: 'ic_your_production_api_key',
};
// angular.json (already configured by ng new)
{
"projects": {
"your-app": {
"architect": {
"build": {
"configurations": {
"production": {
"fileReplacements": [
{
"replace": "src/environments/environment.ts",
"with": "src/environments/environment.prod.ts"
}
]
}
}
}
}
}
}
}Initialize the widget from AppComponent (with cleanup)
Add the script after Angular bootstraps and clean up on destroy
- The widget is an ESM module — type="module" is required, not optional
- AppComponent runs once per app lifetime, so the widget initializes exactly once
- The SSR guard (`typeof document === "undefined"`) prevents the script from being injected in Angular Universal (server-side rendering)
- Implement `OnDestroy` and call `IssueCapture.destroy()` for proper cleanup (useful in dev hot-reload and SPAs where components unmount)
- If you use standalone components, the same `ngOnDestroy` pattern still works — import `OnDestroy` in the standalone providers
// src/app/app.component.ts
import { Component, OnInit, OnDestroy } from '@angular/core';
import { environment } from '../environments/environment';
@Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.css'],
})
export class AppComponent implements OnInit, OnDestroy {
ngOnInit(): void {
// SSR guard: skip widget on the server
if (typeof document === 'undefined') return;
const apiKey = environment.issueCaptureApiKey;
const s = document.createElement('script');
s.type = 'module';
// Inline ESM module — type="module" required because widget.js is ES export
s.textContent = `
import IssueCapture from 'https://issuecapture.com/widget.js';
IssueCapture.init({ apiKey: '${apiKey}' });
`;
document.body.appendChild(s);
}
ngOnDestroy(): void {
// Optional: clean up the widget when the app is destroyed
// (e.g. during hot-reload in dev, or SPAs that unmount the root component)
if (typeof window !== 'undefined' && window.IssueCapture) {
window.IssueCapture.destroy();
}
}
}Or use lazy-loaded route modules (advanced)
Initialize the widget only on specific routes if you want fine-grained control
- AppComponent (Step 3) is recommended — it ensures the widget is available app-wide
- Use lazy-loaded routes only if you want to defer the widget until a specific route loads (rare)
- Be aware: if you init the widget in multiple components, use `IssueCapture.destroy()` in ngOnDestroy to avoid stale listeners
- For most apps, Step 3 is simpler and safer
// src/app/some-feature.component.ts (in a lazy-loaded route)
import { Component, OnInit } from '@angular/core';
import { environment } from '../environments/environment';
@Component({
selector: 'app-some-feature',
template: '<p>Feature page</p>',
})
export class FeatureComponent implements OnInit {
ngOnInit(): void {
if (typeof document === 'undefined') return;
const apiKey = environment.issueCaptureApiKey;
const s = document.createElement('script');
s.type = 'module';
s.textContent = `
import IssueCapture from 'https://issuecapture.com/widget.js';
IssueCapture.init({ apiKey: '${apiKey}' });
`;
document.body.appendChild(s);
}
}Trigger the widget from your own button
Skip the floating button and open the modal from a component
- Optional-chain `window.IssueCapture?.open()` so it never throws if the script hasn't loaded yet
- You can use this button alongside the floating one, or set `trigger: '#your-id'` on init to disable the floating one
- aria-label keeps the button accessible even if it goes icon-only
// src/app/bug-report-button.component.ts
import { Component } from '@angular/core';
declare global {
interface Window {
IssueCapture?: {
open: () => void;
close: () => void;
updateConfig: (cfg: Record<string, unknown>) => void;
};
}
}
@Component({
selector: 'app-bug-report-button',
template: `
<button (click)="open()" aria-label="Report an issue">
Report an issue
</button>
`,
})
export class BugReportButtonComponent {
open(): void {
window.IssueCapture?.open();
}
}Pre-fill the logged-in user (optional)
When auth state changes, push name and email through
- Use `updateConfig` for runtime changes — `init` should only fire once per page
- If the user signs out, you can call `updateConfig({ user: { name: '', email: '' } })` to clear it
- Pre-fills are visible to the user before submit — they can still edit them
// src/app/app.component.ts
import { Component, OnInit } from '@angular/core';
import { AuthService } from './services/auth.service'; // your own auth service
@Component({ /* ... */ })
export class AppComponent implements OnInit {
constructor(private auth: AuthService) {}
ngOnInit(): void {
// Push user info whenever auth state changes.
// updateConfig is cheap — call it freely. Do NOT re-call init().
this.auth.user$.subscribe((user) => {
if (!user) return;
window.IssueCapture?.updateConfig({
user: { name: user.name, email: user.email },
});
});
}
}Verify it works
Quick sanity check end-to-end
- Start the dev server: `ng serve`
- Open `http://localhost:4200` — you should see the floating Report Issue button
- Open DevTools console and type `IssueCapture` — you should see the widget object
- Click the button, submit a test issue, and confirm it lands in Jira
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 (src/types/issuecapture.d.ts)
- Create `src/types/issuecapture.d.ts` declaring `IssueCapture` on the global Window interface
- Make sure the file is picked up by `tsconfig.app.json` `include`
- See the IssueCaptureAPI surface in the docs for the full method list
fileReplacements not working: environment.prod.ts is ignored in production
The most common Angular-specific configuration issue
- Verify `angular.json` contains the fileReplacements rule under `projects > [your-app] > architect > build > configurations > production`
- Build with `ng build --configuration=production` (the default in recent CLI versions, but double-check your npm scripts)
- Never import directly from `environment.prod.ts` — always import from `../environments/environment` (the replacement happens at compile-time)
- If you recently upgraded Angular, regenerate `angular.json` or merge the fileReplacements rule from a fresh scaffold
- Clear `.angular/cache` and rebuild: `rm -rf .angular && ng build --configuration=production`
Widget loads twice when navigating with Angular Router
- Inject the script in AppComponent (which mounts once), not in route-level components
- If you must run it from a routed component, guard with `if (window.IssueCapture) return` before injecting
- For per-route config, use `IssueCapture.updateConfig()` — never re-call `init()`
"Domain not allowed" error (often shows up as CORS)
- In the IssueCapture dashboard, open your widget and click the Domains tab
- Add `localhost:4200` (Angular default) for local dev
- Add your production domain — both www and non-www if you serve both
- Subdomains need to be listed individually
Widget not cleaning up on hot-reload (dev server memory leak)
- Implement `OnDestroy` in AppComponent and call `IssueCapture.destroy()` (see Step 3)
- Without cleanup, each dev rebuild leaves listeners and DOM nodes attached
- The cleanup is optional in production but recommended for a good dev experience
Ready to get started?
Free plan includes 10 issues/month. No card needed — connect Jira and you're done.