Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 5 additions & 3 deletions src/runtime/utils/linearApi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -466,7 +466,8 @@ export const formatBugReportForLinear = (bugReport: BugReportData): Partial<Line
${interactions.map((event) => {
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')}`
Expand Down Expand Up @@ -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')}`
}

Expand Down
13 changes: 11 additions & 2 deletions src/runtime/utils/networkRequests.ts
Original file line number Diff line number Diff line change
Expand Up @@ -85,8 +85,17 @@ export const initializeNetworkMonitoring = (): void => {
// Intercept fetch
window.fetch = async (...args: Parameters<typeof fetch>): Promise<Response> => {
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 ?? ''
Comment on lines +90 to +96
// 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)
Expand Down
32 changes: 32 additions & 0 deletions test/utils/linearApi.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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()
})
})
57 changes: 57 additions & 0 deletions test/utils/networkRequests.test.ts
Original file line number Diff line number Diff line change
@@ -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<typeof vi.fn>

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