diff --git a/src/runtime/utils/linearApi.ts b/src/runtime/utils/linearApi.ts index 12b4169..42a64d9 100644 --- a/src/runtime/utils/linearApi.ts +++ b/src/runtime/utils/linearApi.ts @@ -466,7 +466,8 @@ export const formatBugReportForLinear = (bugReport: BugReportData): Partial { const time = formatTimestamp(event.timestamp) const type = formatEventType(event.type) - const target = event.target.length > 40 ? event.target.substring(0, 37) + '...' : event.target + const rawTarget = event.target ?? '' + const target = rawTarget.length > 40 ? rawTarget.substring(0, 37) + '...' : rawTarget const details = formatDetails(event) return `| ${time} | ${type} | ${target} | ${details} |` }).join('\n')}` @@ -499,14 +500,15 @@ ${logsContent} let networkContent = `| Method | URL | Status | |--------|-----|--------| ${requests.map((req) => { - const url = req.url.length > 60 ? req.url.substring(0, 57) + '...' : req.url + const rawUrl = req.url ?? '' + const url = rawUrl.length > 60 ? rawUrl.substring(0, 57) + '...' : rawUrl const status = req.status === 0 ? 'ERR' : req.status return `| ${req.method} | ${url} | ${status} |` }).join('\n')}` if (failedRequests.length > 0) { networkContent += `\n\n**Fehlerhafte Requests:**\n${failedRequests.map((req) => { - return `- ${req.method} ${req.url.substring(0, 60)} → ${req.status} ${req.statusText}` + return `- ${req.method} ${(req.url ?? '').substring(0, 60)} → ${req.status} ${req.statusText}` }).join('\n')}` } diff --git a/src/runtime/utils/networkRequests.ts b/src/runtime/utils/networkRequests.ts index dc21b0a..2b200fb 100644 --- a/src/runtime/utils/networkRequests.ts +++ b/src/runtime/utils/networkRequests.ts @@ -85,8 +85,17 @@ export const initializeNetworkMonitoring = (): void => { // Intercept fetch window.fetch = async (...args: Parameters): Promise => { const startTime = Date.now() - const url = typeof args[0] === 'string' ? args[0] : args[0].url - const options = typeof args[0] === 'string' ? args[1] : args[0] + // fetch() accepts string | URL | Request as first arg. + // A URL exposes .href (not .url), so mirror the XHR path and resolve all cases. + const first = args[0] + const url + = typeof first === 'string' + ? first + : first instanceof URL + ? first.href + : first?.url ?? '' + // Options come from args[1] for string/URL inputs; a Request carries them itself. + const options = first instanceof Request ? first : args[1] if (!shouldCaptureUrl(url)) { return originalFetch(...args) diff --git a/test/utils/linearApi.test.ts b/test/utils/linearApi.test.ts index 77ae7cd..33f2cc9 100644 --- a/test/utils/linearApi.test.ts +++ b/test/utils/linearApi.test.ts @@ -255,4 +255,36 @@ describe('formatBugReportForLinear', () => { expect(result.description).toContain('## Bug Report') expect(result.description).toContain('**Beschreibung:**\nSimple description') }) + + // Regression: network requests captured via fetch(new URL(...)) end up with + // url: undefined and previously crashed formatBugReportForLinear (issue #3). + it('should not throw when a network request has no url', () => { + const bugReport = { + title: 'repro', + type: 'improvement', + description: 'repro', + networkRequests: [ + { method: 'GET', status: 200, statusText: 'OK', timestamp: '2026-01-01T00:00:00Z', type: 'fetch' }, + ], + } as unknown as BugReportData + + expect(() => formatBugReportForLinear(bugReport)).not.toThrow() + const result = formatBugReportForLinear(bugReport) + expect(result.description).toContain('| GET |') + }) + + // Regression: user interactions without a target (e.g. external/legacy posts) + // previously crashed on event.target.length (issue #3). + it('should not throw when a user interaction has no target', () => { + const bugReport = { + title: 'repro', + type: 'improvement', + description: 'repro', + userInteractions: [ + { type: 'click', timestamp: '2026-01-01T00:00:00Z' }, + ], + } as unknown as BugReportData + + expect(() => formatBugReportForLinear(bugReport)).not.toThrow() + }) }) diff --git a/test/utils/networkRequests.test.ts b/test/utils/networkRequests.test.ts new file mode 100644 index 0000000..73c877a --- /dev/null +++ b/test/utils/networkRequests.test.ts @@ -0,0 +1,57 @@ +import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest' + +// The module captures window.fetch at import time, so the fetch mock must be in +// place before the (dynamic) import. Each test imports a fresh module instance. +describe('network request capture', () => { + let fetchMock: ReturnType + + beforeEach(() => { + vi.resetModules() + fetchMock = vi.fn().mockResolvedValue( + new Response('{}', { status: 200, statusText: 'OK' }), + ) + window.fetch = fetchMock as unknown as typeof fetch + }) + + afterEach(async () => { + const mod = await import('../../src/runtime/utils/networkRequests') + mod.resetNetworkMonitoring() + }) + + // Regression for issue #3: fetch(new URL(...)) must record the resolved href, + // not url: undefined (a URL exposes .href, not .url). + it('records the href when fetch is called with a URL object', async () => { + const { initializeNetworkMonitoring, getNetworkRequests } = await import('../../src/runtime/utils/networkRequests') + initializeNetworkMonitoring() + + await window.fetch(new URL('https://example.com/api/health')) + + const requests = getNetworkRequests() + expect(requests).toHaveLength(1) + expect(requests[0]?.url).toBe('https://example.com/api/health') + expect(requests[0]?.url).toBeDefined() + }) + + it('still records a plain string url', async () => { + const { initializeNetworkMonitoring, getNetworkRequests } = await import('../../src/runtime/utils/networkRequests') + initializeNetworkMonitoring() + + await window.fetch('https://example.com/api/data') + + const requests = getNetworkRequests() + expect(requests).toHaveLength(1) + expect(requests[0]?.url).toBe('https://example.com/api/data') + }) + + it('records the url and method from a Request object', async () => { + const { initializeNetworkMonitoring, getNetworkRequests } = await import('../../src/runtime/utils/networkRequests') + initializeNetworkMonitoring() + + await window.fetch(new Request('https://example.com/api/post', { method: 'POST' })) + + const requests = getNetworkRequests() + expect(requests).toHaveLength(1) + expect(requests[0]?.url).toBe('https://example.com/api/post') + expect(requests[0]?.method).toBe('POST') + }) +})