Vue.js Integration Guide

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

Time: ~10 minutes (incl. Jira OAuth)
Prerequisites: Vue 3+, Vite, Jira Cloud account
Method: Script tag, composition watch, or Pinia store
Works with: Vue 3 (Composition + Options), Vue Router, Pinia

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.

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 .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_here
3

Install 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>
4

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

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>
6

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>
7

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>
8

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)
})
9

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)
})
10

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.