Angular Integration Guide

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

Time: ~10 minutes (incl. Jira OAuth)
Prerequisites: Angular 15+, Jira Cloud account
Method: AppComponent inject or index.html tag
Works with: Angular 15+, Router, NgRx, Universal

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.

1

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_")
2

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"
                }
              ]
            }
          }
        }
      }
    }
  }
}
3

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();
    }
  }
}
4

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);
  }
}
5

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();
  }
}
6

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 },
      });
    });
  }
}
7

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.