Install IssueCapture in Vue.js
Add bug reporting to Vue 3 with Composition API or Options API. Includes pre-fill watchers with immediate mode, Pinia store integration, Vue Router navigation guards, and Composition API gotchas.
Quick Summary
Prerequisites
- Vue 3 with Composition API
- Vite (recommended) or another ESM-compatible bundler
- 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
Vite reads VITE_-prefixed vars via import.meta.env
- Vite only exposes variables that start with VITE_ to the browser bundle
- Access at runtime via `import.meta.env.VITE_ISSUECAPTURE_API_KEY`
- Add .env to .gitignore — don't commit the key
- Restart `vite` after adding env vars
# .env (Vite — recommended for Vue 3)
VITE_ISSUECAPTURE_API_KEY=ic_your_api_key_hereInstall via index.html (simplest)
Hardcode the script tag — fine if you can paste the key directly
- The widget is an ESM module — type="module" is required
- Vite does NOT replace tokens in index.html, so paste the key directly here
- For a runtime key from .env (recommended for repo-friendly setups), use Step 4 instead
- Place the snippet at the end of <body> so it never blocks first paint
<!-- index.html -->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>Your Vue App</title>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.ts"></script>
<!-- IssueCapture Widget (ESM module) -->
<script type="module">
import IssueCapture from 'https://issuecapture.com/widget.js';
IssueCapture.init({ apiKey: 'ic_your_api_key_here' });
</script>
</body>
</html>Or load from main.ts so .env supplies the key
Inject the script after the Vue app mounts — keeps the key out of HTML
- `import.meta.env.VITE_ISSUECAPTURE_API_KEY` is replaced at build time with the value from .env
- Adding a `<script type="module">` programmatically with inline textContent DOES execute when appended — this is the supported pattern
- Putting this in main.ts keeps the widget at app level (no component teardown to worry about)
// src/main.ts
import { createApp } from 'vue'
import App from './App.vue'
createApp(App).mount('#app')
// IssueCapture — runs once on app boot
const apiKey = import.meta.env.VITE_ISSUECAPTURE_API_KEY
if (apiKey) {
const s = document.createElement('script')
s.type = 'module'
// Inline ESM — runs on append and gets the runtime value of apiKey
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 Vue component
- Optional-chain `window.IssueCapture?.open()` so it never throws if the script hasn't loaded yet
- You can use this alongside the floating button, or set `trigger: '#your-id'` on init to disable the floating one
- aria-label keeps the button accessible even if you go icon-only
<!-- src/components/BugReportButton.vue -->
<script setup lang="ts">
declare global {
interface Window {
IssueCapture?: {
open: () => void
close: () => void
updateConfig: (cfg: Record<string, unknown>) => void
}
}
}
const open = () => window.IssueCapture?.open()
</script>
<template>
<button
@click="open"
class="bug-report-btn"
aria-label="Report an issue"
>
Report an issue
</button>
</template>Pre-fill the logged-in user (optional)
When the user object changes, push their name and email through
- Use updateConfig for runtime changes — init should only fire once per page
- `immediate: true` runs the watcher once on setup so currently-logged-in users get pre-filled immediately
- Pre-fills are visible to the user before they submit — they can still edit them
<!-- src/App.vue -->
<script setup lang="ts">
import { watch } from 'vue'
import { useAuth } from './composables/useAuth' // your own auth composable
const { user } = useAuth()
// Whenever user changes, tell the widget who's reporting.
// updateConfig is cheap — call it freely. Do NOT re-call init().
watch(user, (next) => {
if (!next) return
window.IssueCapture?.updateConfig({
user: { name: next.name, email: next.email },
})
}, { immediate: true })
</script>
<template>
<div id="app"><!-- your app --></div>
</template>Options API alternative: defineComponent + setup()
If you prefer Options API or need legacy Vue class syntax
- Use `defineComponent` with `setup()` for Options API + Composition functions
- The setup() hook runs before the template renders — watchers initialize immediately
- `immediate: true` ensures the widget pre-fills on first render (before user action)
- Composition API `<script setup>` (step 6) is the modern shorthand — this is the equivalent verbose form
<!-- src/App.vue (Options API) -->
<script lang="ts">
import { defineComponent, watch } from 'vue'
import { useAuth } from './composables/useAuth'
export default defineComponent({
name: 'App',
setup() {
const { user } = useAuth()
watch(user, (next) => {
if (!next) return
window.IssueCapture?.updateConfig({
user: { name: next.name, email: next.email },
})
}, { immediate: true })
return { user }
},
})
</script>
<template>
<div id="app"><!-- your app --></div>
</template>Pinia store: sync widget events with app state (optional)
React to widget submission events or error capture in your Pinia store
- Pinia stores are reactive by default — perfect for tracking widget state globally
- The `on()` method allows you to listen for widget events: submit, error, open, close
- This is optional — only use if you need to track widget activity in your app state
- Call IssueCapture.on() once, ideally in main.ts or a top-level component setup hook
// src/stores/issueCapture.ts (Pinia)
import { defineStore } from 'pinia'
import { ref } from 'vue'
export const useIssueCaptureStore = defineStore('issueCapture', () => {
const submissions = ref(0)
const errors = ref<string[]>([])
const recordSubmission = () => {
submissions.value += 1
}
const recordError = (error: string) => {
errors.value.push(error)
}
return { submissions, errors, recordSubmission, recordError }
})
// In App.vue (or main.ts), wire up the widget events
import { useIssueCaptureStore } from './stores/issueCapture'
const store = useIssueCaptureStore()
// After IssueCapture.init(), listen for events
IssueCapture.on('submit', (data) => {
store.recordSubmission()
console.log('Issue submitted:', data)
})
IssueCapture.on('error', (error) => {
store.recordError(error.message)
})Vue Router: reinit/destroy on navigation (SPA best practice)
If you load different widgets or config per route, use Vue Router guards
- Use router.afterEach for post-navigation hooks (after transition completes)
- Call updateConfig to switch widget settings per route — this is cheap and preserves state
- Only call init() once per app (in main.ts) — use updateConfig for routing changes
- This pattern works for both Composition API and Options API setups
// src/router/index.ts (Vue Router with composition)
import { createRouter, createWebHistory } from 'vue-router'
export const router = createRouter({
history: createWebHistory(),
routes: [
{ path: '/support', component: () => import('../pages/Support.vue') },
{ path: '/billing', component: () => import('../pages/Billing.vue') },
],
})
router.afterEach((to) => {
// On route change, optionally reinit the widget with route-specific config
const configByRoute: Record<string, unknown> = {
'/support': {
apiKey: 'ic_support_widget',
defaultValues: { component: 'Support' },
},
'/billing': {
apiKey: 'ic_billing_widget',
defaultValues: { component: 'Billing', priority: 'High' },
},
}
const config = configByRoute[to.path] || { apiKey: 'ic_default' }
window.IssueCapture?.updateConfig(config)
})Verify it works
Quick sanity check end-to-end
- Start the dev server: `npm run dev` or `pnpm dev`
- Open your app — 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 that path is covered by `tsconfig.json` `include`
- See the IssueCaptureAPI surface in the docs for the full method list
Environment variables aren't reaching the browser
- Vite only exposes variables prefixed with `VITE_`
- Read them via `import.meta.env.VITE_YOUR_VAR` — `process.env` is unavailable in browser code
- Restart `vite` after editing .env
- Ensure `.env` lives next to `vite.config.ts` in the project root
Widget loads twice when navigating with Vue Router
- Initialise the widget once in main.ts (not in a route-level component)
- If you must run it from a component, guard with `if (window.IssueCapture) return` before injecting
- For per-route tweaks, 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:5173` (Vite default) for local dev
- Add your production domain — both www and non-www if you serve both
- Subdomains need to be listed individually
Composition API: watch() runs AFTER first render (user shows blank fields)
By default, watchers in Composition API run after render. Use `immediate: true` for first-render sync.
- Always use `{ immediate: true }` when pre-filling the widget on mount (step 6 shows this)
- Without immediate: true, the watcher runs AFTER first render, so the first page load shows empty user fields
- Test in DevTools: open the modal on first load — if fields are blank, add `{ immediate: true }` to your watch()
- This applies to both setup() hooks and `<script setup>`
Options API: 'immediate' in watch() options not working
In Options API `watch`, check that you're using the full watcher config object, not the shorthand.
- Use the full form: `watch: { user: { handler(next) { }, immediate: true } }`
- The shorthand `watch: { user() {} }` does NOT support options — use setup() instead for cleaner code
- If migrating from Vue 2, note that Options API watchers in Vue 3 have slightly different syntax — check the Vue 3 docs
Ready to get started?
Free plan includes 10 issues/month. No card needed — connect Jira and you're done.