Skip to content

Commit 14b8d6b

Browse files
fix: bug-report endpoint /api collision (DEV-2114) + BugReportButton hydration (DEV-2017) (#2)
* fix(server): move bug-report endpoint off /api to avoid consumer /api proxy collision (DEV-2114) Consuming Nuxt apps very commonly proxy all `/api/**` requests to a separate backend via Nitro `routeRules`. That catch-all swallowed `/api/bug-report` and forwarded it to the backend (404), so bug.lt's own server handler never ran on built/deployed apps. Add a configurable `endpoint` module option defaulting to `/_bug-lt/report` (Nuxt-internal `_`-prefix style, outside `/api/`). Expose it via `runtimeConfig.public.bugLt.endpoint`; register the server handler with `options.endpoint`; and read the same value in `useBugReport` so server and client always agree. Document the option and rationale in the README. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(button): render BugReportButton client-only to stop hydration mismatch + RouterLink warning (DEV-2017) The floating button rendered a Nuxt UI `UButton` (which resolves to a NuxtLink with `to=undefined`) inside `<Teleport to="body">` during SSR. On the client this mismatched, logging `Failed to resolve component: RouterLink` plus hydration-node-mismatch warnings on every page. The button is a pure client-side UI overlay, so wrap the `<Teleport>`/`<UButton>` block in `<ClientOnly>` to skip SSR entirely. Behavior (position, color, screenshot capture, openModal) is unchanged. The modal needs no change — it is only ever mounted client-side via `useOverlay().create()` from a click, never in the SSR tree. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent f9757c9 commit 14b8d6b

7 files changed

Lines changed: 107 additions & 51 deletions

File tree

‎README.md‎

Lines changed: 37 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,11 @@ export default defineNuxtConfig({
4444
enabled: true, // false deaktiviert das komplette Modul
4545
ui: true, // false deaktiviert @nuxt/ui Installation
4646

47+
// Server-Endpunkt für den Bug-Report (Server-Handler + Client-`$fetch`).
48+
// Standard liegt bewusst NICHT unter `/api/`, damit ein `/api/**`-Proxy der
49+
// Consumer-App (Nitro `routeRules`) den Request nicht abfängt. Siehe Hinweis unten.
50+
endpoint: '/_bug-lt/report',
51+
4752
// Linear Integration
4853
linearApiKey: process.env.NUXT_LINEAR_API_KEY,
4954
linearTeamName: process.env.NUXT_LINEAR_TEAM_NAME || 'Entwicklung',
@@ -201,27 +206,38 @@ const {
201206

202207
## Konfigurationsoptionen
203208

204-
| Option | Typ | Standard | Beschreibung |
205-
|-------------------------|-----------|------------------|---------------------------------------|
206-
| `enabled` | `boolean` | `true` | Komplettes Modul aktivieren |
207-
| `ui` | `boolean` | `true` | @nuxt/ui Installation aktivieren |
208-
| `linearApiKey` | `string` | - | Linear API Key (erforderlich) |
209-
| `linearTeamName` | `string` | - | Linear Team Name oder Key |
210-
| `linearProjectName` | `string` | - | Linear Projekt Name (optional) |
211-
| `autoShow` | `boolean` | `true` | Automatische Anzeige des Buttons |
212-
| `position` | `string` | `'bottom-right'` | Button-Position |
213-
| `buttonColor` | `string` | `'#ef4444'` | Button-Farbe |
214-
| `enableScreenshot` | `boolean` | `true` | Screenshot-Funktionalität |
215-
| `enableBrowserInfo` | `boolean` | `true` | Browser-Info-Erfassung |
216-
| `enableConsoleLogs` | `boolean` | `true` | Console-Log-Erfassung |
217-
| `enableNetworkRequests` | `boolean` | `true` | Netzwerk-Request-Erfassung |
218-
| `enableUserJourney` | `boolean` | `true` | User Journey Tracking |
219-
| `enableErrorBoundary` | `boolean` | `true` | Error Boundary Komponente |
220-
| `autoOpenOnError` | `boolean` | `false` | Modal bei JS-Fehlern öffnen |
221-
| `theme` | `string` | `'auto'` | Theme ('light', 'dark', 'auto') |
222-
| `maxConsoleLogs` | `number` | `50` | Maximale Anzahl Console-Logs |
223-
| `maxNetworkRequests` | `number` | `50` | Maximale Anzahl Network-Requests |
224-
| `userJourney` | `object` | siehe unten | User Journey Konfiguration |
209+
| Option | Typ | Standard | Beschreibung |
210+
|-------------------------|-----------|---------------------|-------------------------------------------------------|
211+
| `enabled` | `boolean` | `true` | Komplettes Modul aktivieren |
212+
| `ui` | `boolean` | `true` | @nuxt/ui Installation aktivieren |
213+
| `endpoint` | `string` | `'/_bug-lt/report'` | Server-Route für den Bug-Report (siehe Hinweis unten) |
214+
| `linearApiKey` | `string` | - | Linear API Key (erforderlich) |
215+
| `linearTeamName` | `string` | - | Linear Team Name oder Key |
216+
| `linearProjectName` | `string` | - | Linear Projekt Name (optional) |
217+
| `autoShow` | `boolean` | `true` | Automatische Anzeige des Buttons |
218+
| `position` | `string` | `'bottom-right'` | Button-Position |
219+
| `buttonColor` | `string` | `'#ef4444'` | Button-Farbe |
220+
| `enableScreenshot` | `boolean` | `true` | Screenshot-Funktionalität |
221+
| `enableBrowserInfo` | `boolean` | `true` | Browser-Info-Erfassung |
222+
| `enableConsoleLogs` | `boolean` | `true` | Console-Log-Erfassung |
223+
| `enableNetworkRequests` | `boolean` | `true` | Netzwerk-Request-Erfassung |
224+
| `enableUserJourney` | `boolean` | `true` | User Journey Tracking |
225+
| `enableErrorBoundary` | `boolean` | `true` | Error Boundary Komponente |
226+
| `autoOpenOnError` | `boolean` | `false` | Modal bei JS-Fehlern öffnen |
227+
| `theme` | `string` | `'auto'` | Theme ('light', 'dark', 'auto') |
228+
| `maxConsoleLogs` | `number` | `50` | Maximale Anzahl Console-Logs |
229+
| `maxNetworkRequests` | `number` | `50` | Maximale Anzahl Network-Requests |
230+
| `userJourney` | `object` | siehe unten | User Journey Konfiguration |
231+
232+
> [!IMPORTANT]
233+
> **Warum liegt der Endpunkt nicht unter `/api/`?**
234+
> Viele Nuxt-Apps leiten *alle* `/api/**`-Requests per Nitro-`routeRules`-Proxy an
235+
> ein separates Backend weiter. Ein solcher Catch-all würde `/api/bug-report`
236+
> verschlucken und an das Backend weiterreichen (→ **404**), sodass der Handler
237+
> dieses Moduls nie ausgeführt wird. Deshalb liegt der Standard-`endpoint` bewusst
238+
> außerhalb von `/api/` (`/_bug-lt/report`, im Nuxt-internen `_`-Präfix-Stil). Server
239+
> und Client lesen denselben Wert aus `runtimeConfig.public.bugLt.endpoint` — du
240+
> kannst ihn frei überschreiben, solange er nicht von einem Proxy abgefangen wird.
225241
226242
### User Journey Optionen
227243

‎src/module.ts‎

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,18 @@ export interface ModuleOptions {
2222
*/
2323
ui?: boolean
2424

25+
/**
26+
* Server route the bug report is submitted to (server handler + client `$fetch`).
27+
*
28+
* Defaults to a non-`/api` path on purpose: consuming apps very commonly proxy
29+
* every `/api/...` request to a separate backend (via Nitro `routeRules`). Such a
30+
* catch-all would swallow `/api/bug-report` and forward it to the backend (404),
31+
* so this module's own handler would never run. Keep the default outside `/api`
32+
* (Nuxt-internal `_`-prefix style) unless you have a dedicated rule for it.
33+
* @defaultValue `'/_bug-lt/report'`
34+
*/
35+
endpoint?: string
36+
2537
// Linear Integration
2638
linearApiKey?: string
2739
linearTeamName?: string
@@ -83,6 +95,8 @@ export default defineNuxtModule<ModuleOptions>({
8395
defaults: {
8496
enabled: true,
8597
ui: true,
98+
// Off `/api/` so consumer `/api/**` proxies (Nitro routeRules) don't swallow it
99+
endpoint: '/_bug-lt/report',
86100
autoShow: true,
87101
position: 'bottom-right',
88102
buttonColor: '#ef4444',
@@ -137,6 +151,7 @@ export default defineNuxtModule<ModuleOptions>({
137151
nuxt.options.runtimeConfig.public.bugLt = {
138152
enabled: options.enabled,
139153
ui: options.ui,
154+
endpoint: options.endpoint,
140155
autoShow: options.autoShow,
141156
position: options.position,
142157
buttonColor: options.buttonColor,
@@ -201,9 +216,11 @@ export default defineNuxtModule<ModuleOptions>({
201216
from: resolver.resolve('./runtime/composables/useBugReport'),
202217
})
203218

204-
// Add server API endpoints
219+
// Add server API endpoint.
220+
// Route comes from `options.endpoint` (default `/_bug-lt/report`) so it sits
221+
// outside `/api/**` and isn't swallowed by consumer `/api/**` proxies.
205222
addServerHandler({
206-
route: '/api/bug-report',
223+
route: options.endpoint,
207224
handler: resolver.resolve('./runtime/server/api/bug-report.post'),
208225
method: 'post',
209226
})

‎src/runtime.d.ts‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ declare module '@nuxt/schema' {
1515
bugLt: {
1616
enabled?: boolean
1717
ui?: boolean
18+
endpoint?: string
1819
autoShow?: boolean
1920
position?: 'bottom-right' | 'bottom-left' | 'top-right' | 'top-left'
2021
buttonColor?: string

‎src/runtime/components/BugReportButton.vue‎

Lines changed: 34 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -71,30 +71,39 @@ const buttonClasses = computed(() => {
7171
</script>
7272

7373
<template>
74-
<Teleport to="body">
75-
<UButton
76-
v-if="config.autoShow"
77-
:class="buttonClasses"
78-
:style="`background-color: ${config.buttonColor}; color: white;`"
79-
size="lg"
80-
variant="solid"
81-
:loading="capturingScreenshot"
82-
@click="handleOpenModal"
83-
>
84-
<img
85-
v-if="!config.buttonIcon && !config.buttonText"
86-
:src="iconPng"
87-
class="w-7 h-7 p-1"
88-
alt="Bug Report"
74+
<!--
75+
Client-only on purpose: this floating button is a pure client-side UI overlay.
76+
Nuxt UI's UButton resolves to a NuxtLink with `to=undefined`, which renders an
77+
`<a>` during SSR but mismatches on hydration -> "Failed to resolve component:
78+
RouterLink" + hydration-mismatch warnings on every page. Rendering it only on
79+
the client avoids the SSR/hydration mismatch entirely.
80+
-->
81+
<ClientOnly>
82+
<Teleport to="body">
83+
<UButton
84+
v-if="config.autoShow"
85+
:class="buttonClasses"
86+
:style="`background-color: ${config.buttonColor}; color: white;`"
87+
size="lg"
88+
variant="solid"
89+
:loading="capturingScreenshot"
90+
@click="handleOpenModal"
8991
>
90-
<UIcon
91-
v-else-if="config.buttonIcon"
92-
:name="config.buttonIcon"
93-
class="w-6 h-6"
94-
/>
95-
<div v-if="config.buttonText">
96-
{{ capturingScreenshot ? 'Screenshot...' : config.buttonText }}
97-
</div>
98-
</UButton>
99-
</Teleport>
92+
<img
93+
v-if="!config.buttonIcon && !config.buttonText"
94+
:src="iconPng"
95+
class="w-7 h-7 p-1"
96+
alt="Bug Report"
97+
>
98+
<UIcon
99+
v-else-if="config.buttonIcon"
100+
:name="config.buttonIcon"
101+
class="w-6 h-6"
102+
/>
103+
<div v-if="config.buttonText">
104+
{{ capturingScreenshot ? 'Screenshot...' : config.buttonText }}
105+
</div>
106+
</UButton>
107+
</Teleport>
108+
</ClientOnly>
100109
</template>

‎src/runtime/composables/useBugReport.ts‎

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,13 @@ import { ref } from 'vue'
22
import type { BugReportData, ErrorInfo, UseBugReportReturn, UserInteractionEvent } from '../types'
33
import { useOverlay } from '#ui/composables/useOverlay'
44
import { useToast } from '#ui/composables/useToast'
5+
import { useRuntimeConfig } from '#imports'
56
import BugReportModal from '../components/BugReportModal.vue'
67
import { getUserInteractions } from '../utils/userInteractions'
78

9+
// Fallback must match the module's default `endpoint` option (src/module.ts).
10+
const DEFAULT_ENDPOINT = '/_bug-lt/report'
11+
812
// Global state for bug reporting
913
const isSubmitting = ref<boolean>(false)
1014
const error = ref<string | null>(null)
@@ -34,7 +38,10 @@ export const useBugReport = (): UseBugReportReturn => {
3438
error.value = null
3539

3640
try {
37-
const response: any = await $fetch('/api/bug-report', {
41+
// Resolve the configured endpoint; must stay in sync with the server handler
42+
// route registered in src/module.ts (default: off `/api/` to dodge proxies).
43+
const endpoint = useRuntimeConfig().public.bugLt?.endpoint || DEFAULT_ENDPOINT
44+
const response: any = await $fetch(endpoint, {
3845
method: 'POST',
3946
body: data,
4047
})

‎src/runtime/types/index.ts‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -165,6 +165,7 @@ export interface BugReportConfig {
165165
linearApiKey?: string
166166
linearTeamName?: string
167167
linearProjectName?: string
168+
endpoint?: string
168169
autoShow?: boolean
169170
position?: BugReportPosition
170171
buttonColor?: string

‎test/module.test.ts‎

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ describe('Bug LT Module', () => {
3030
expect(module.defaults).toEqual({
3131
enabled: true,
3232
ui: true,
33+
endpoint: '/_bug-lt/report',
3334
autoShow: true,
3435
position: 'bottom-right',
3536
buttonColor: '#ef4444',
@@ -123,12 +124,16 @@ describe('Bug LT Module', () => {
123124
},
124125
}
125126

126-
const mockOptions: ModuleOptions = {}
127+
// setup() is called directly here, bypassing defineNuxtModule's default merge,
128+
// so pass the endpoint explicitly to mirror the resolved default option.
129+
const mockOptions: ModuleOptions = { endpoint: '/_bug-lt/report' }
127130

128131
await module.setup(mockOptions, mockNuxt)
129132

133+
// Route must come from options.endpoint (off `/api/` to dodge consumer proxies),
134+
// not the old hardcoded `/api/bug-report`.
130135
expect(addServerHandler).toHaveBeenCalledWith({
131-
route: '/api/bug-report',
136+
route: '/_bug-lt/report',
132137
handler: './runtime/server/api/bug-report.post',
133138
method: 'post',
134139
})

0 commit comments

Comments
 (0)