Skip to content
Draft
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
3 changes: 3 additions & 0 deletions .buildkite/package_manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
"packages/plugin-interaction-breadcrumbs",
"packages/plugin-navigation-breadcrumbs",
"packages/plugin-network-breadcrumbs",
"packages/plugin-network-instrumentation",
"packages/plugin-simple-throttle",
"packages/plugin-strip-query-string",
"packages/plugin-window-onerror",
Expand Down Expand Up @@ -97,6 +98,7 @@
"packages/delivery-react-native",
"packages/plugin-console-breadcrumbs",
"packages/plugin-network-breadcrumbs",
"packages/plugin-network-instrumentation",
"packages/plugin-react",
"packages/plugin-react-native-client-sync",
"packages/plugin-react-native-event-sync",
Expand All @@ -121,6 +123,7 @@
"packages/delivery-react-native",
"packages/plugin-console-breadcrumbs",
"packages/plugin-network-breadcrumbs",
"packages/plugin-network-instrumentation",
"packages/plugin-react",
"packages/plugin-react-native-client-sync",
"packages/plugin-react-native-event-sync",
Expand Down
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,14 @@

## [Unreleased]

### Added

- (delivery-react-native) Handle request and response parameters [#2667](https://github.com/bugsnag/bugsnag-js/pull/2667)

### Changed

- (plugin-network-instrumentation) Manually parse URLs to improve React Native compatibility [#2674](https://github.com/bugsnag/bugsnag-js/pull/2674)
- (plugin-network-instrumentation) Refactor URL parsing to improve performance [#2667](https://github.com/bugsnag/bugsnag-js/pull/2667)

### Fixed

Expand Down
2 changes: 2 additions & 0 deletions packages/delivery-react-native/delivery.js
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,8 @@ module.exports = (client, NativeClient) => ({
breadcrumbs: derecursify(event.breadcrumbs),
context: event.context,
user: event._user,
request: event.request,
response: event.response,
metadata: derecursify(event._metadata),
groupingHash: event.groupingHash,
groupingDiscriminator: event._groupingDiscriminator,
Expand Down
6 changes: 6 additions & 0 deletions packages/delivery-react-native/test/delivery.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ type NativeClientEvent = Pick<EventWithInternals,
| 'unhandled'
| 'app'
| 'device'
| 'request'
| 'response'
| 'threads'
| 'breadcrumbs'
| 'context'
Expand Down Expand Up @@ -89,6 +91,8 @@ describe('delivery: react native', () => {
expect(sent[0].context).toBe('test screen')
expect(sent[0].user).toEqual({ id: '123', email: undefined, name: undefined })
expect(sent[0].metadata).toEqual({})
expect(sent[0].request).toEqual({})
expect(sent[0].response).toEqual({})
expect(sent[0].groupingHash).toEqual('ER_GRP_098')
expect(sent[0].apiKey).toBe('abcdef123456abcdef123456abcdef123456')
expect(sent[0].correlation).toEqual({ traceId: 'trace-id', spanId: 'span-id' })
Expand Down Expand Up @@ -286,6 +290,8 @@ describe('delivery: react native', () => {
expect(sent[0].context).toBe('test screen')
expect(sent[0].user).toEqual({ id: '123', email: undefined, name: undefined })
expect(sent[0].metadata).toEqual({})
expect(sent[0].request).toEqual({})
expect(sent[0].response).toEqual({})
expect(sent[0].groupingHash).toEqual('ER_GRP_098')
expect(sent[0].apiKey).toBe('abcdef123456abcdef123456abcdef123456')
done()
Expand Down
34 changes: 0 additions & 34 deletions packages/plugin-network-instrumentation/lib/extract-domain.js

This file was deleted.

29 changes: 0 additions & 29 deletions packages/plugin-network-instrumentation/lib/parse-query-params.js

This file was deleted.

19 changes: 19 additions & 0 deletions packages/plugin-network-instrumentation/lib/parse-query-string.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
/**
* Parse a query string into an object
* @param {string} queryString - Query string (e.g., "key=value&foo=bar")
* @returns {Object<string, string>} Parsed query parameters as key-value pairs
*/
module.exports = function (queryString) {
const params = {}
if (!queryString) {
return params
}

const pairs = queryString.split('&').filter(pair => pair.length > 0)
pairs.forEach(pair => {
const [key, value] = pair.split('=')
params[decodeURIComponent(key)] = decodeURIComponent(value || '')
})

return params
}
64 changes: 64 additions & 0 deletions packages/plugin-network-instrumentation/lib/parse-url.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
/**
* Parse a URL in a single pass to extract domain, clean URL, and query string
* @param {string} url - URL string
* @returns {{ domain: string, cleanUrl: string, queryString: string }} Object with domain, cleanUrl, and queryString
*/
module.exports = function (url) {
try {
const isAbsolute = /^https?:\/\//i.test(url)

// Extract query string from the full URL
const queryStart = url.indexOf('?')
const hashStart = url.indexOf('#')

let queryString = ''
if (queryStart !== -1) {
const queryEnd = hashStart !== -1 && hashStart > queryStart ? hashStart : url.length
queryString = url.substring(queryStart + 1, queryEnd)
}

// Extract domain
let domain = 'unknown'
if (isAbsolute) {
const urlWithoutProtocol = url.replace(/^https?:\/\//i, '')

// Find the earliest occurrence of '/', '?', or '#' to determine the domain boundary
const slashIndex = urlWithoutProtocol.indexOf('/')
const domainQueryIndex = urlWithoutProtocol.indexOf('?')
const domainHashIndex = urlWithoutProtocol.indexOf('#')
let endIndex = urlWithoutProtocol.length
if (slashIndex !== -1 && slashIndex < endIndex) {
endIndex = slashIndex
}
if (domainQueryIndex !== -1 && domainQueryIndex < endIndex) {
endIndex = domainQueryIndex
}
if (domainHashIndex !== -1 && domainHashIndex < endIndex) {
endIndex = domainHashIndex
}

domain = urlWithoutProtocol.substring(0, endIndex)
}

// Strip query string while preserving hash
const hash = hashStart !== -1 ? url.substring(hashStart) : ''
let urlWithoutHash = queryStart !== -1 ? url.substring(0, queryStart) : url
if (hashStart !== -1 && queryStart === -1) {
// If there's a hash but no query string, remove the hash first
urlWithoutHash = url.substring(0, hashStart)
}
const cleanUrl = urlWithoutHash + hash

return {
domain,
cleanUrl,
queryString
}
} catch (e) {
return {
domain: 'unknown',
cleanUrl: url,
queryString: ''
}
}
}

This file was deleted.

32 changes: 14 additions & 18 deletions packages/plugin-network-instrumentation/network-instrumentation.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,9 @@
* A plugin to automatically capture and report HTTP errors
*/

const extractDomain = require('./lib/extract-domain')
const parseQueryParams = require('./lib/parse-query-params')
const redactQueryParameters = require('./lib/redact-query-parameters')
const parseUrl = require('./lib/parse-url')
const parseQueryString = require('./lib/parse-query-string')
const redactValues = require('./lib/redact-values')
const shouldCaptureStatusCode = require('./lib/should-capture-status-code')
const truncate = require('./lib/truncate')

Expand Down Expand Up @@ -84,22 +84,25 @@ module.exports = (config = {}, global = window) => {

try {
// Extract request information
const url = startContext.url
const requestParams = parseQueryParams(url)
const originalUrl = startContext.url
const { domain, cleanUrl, queryString } = parseUrl(originalUrl)
const url = cleanUrl
const method = startContext.method
const domain = extractDomain(url)

// Parse query string into object
const requestParams = parseQueryString(queryString)

// Create request and response objects for callback
const requestObj = {
url: startContext.url,
httpMethod: startContext.method,
url,
httpMethod: method,
headers: startContext.headers,
params: requestParams
params: redactValues(requestParams, client._config.redactedKeys),
bodyLength: startContext.body ? startContext.body.length : undefined
}
const responseObj = {
statusCode: endContext.status,
headers: endContext.headers,
body: endContext.body,
bodyLength: endContext.body ? endContext.body.length : undefined
}

Expand All @@ -116,22 +119,15 @@ module.exports = (config = {}, global = window) => {
// Truncate request body
if (maxRequestSize > 0 && startContext.body) {
requestObj.body = truncate(startContext.body, maxRequestSize)
requestObj.bodyLength = startContext.body.length
}

// Truncate response body - XHR only
if (maxResponseSize > 0 && endContext.body) {
responseObj.body = truncate(endContext.body, maxResponseSize)
responseObj.bodyLength = endContext.body.length
}

// Strip query parameters from URL
if (requestObj.url !== '[REDACTED]') {
requestObj.url = redactQueryParameters(requestObj.url, client._config.redactedKeys)
}

// Create error and notify
const error = new Error(`${responseObj.statusCode}: ${requestObj.url}`)
const error = new Error(`${responseObj.statusCode}: ${url}`)
error.name = 'HTTPError'

const handledState = {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -210,32 +210,5 @@ describe('plugin-network-instrumentation', () => {
// since they don't have status codes, but let's verify the behavior
expect(notifyCallbacks.length).toBe(0)
})

it('should redact specified query parameters in XHR URLs', async () => {
const notifyCallbacks: Event[] = []

plugin = createPlugin({
httpErrorCodes: { min: 400, max: 499 }
})

const client = new Client({ apiKey: 'api_key', plugins: [plugin], redactedKeys: ['token', 'userId'] })
client._setDelivery(createMockDelivery(notifyCallbacks))

const xhr = new XMLHttpRequest() as any
xhr.status = 403
xhr.statusText = 'Forbidden'
xhr.response = 'Forbidden'

xhr.open('GET', 'https://api.example.com/data?userId=42')
xhr.send()

await new Promise(resolve => setTimeout(resolve, 20))

expect(notifyCallbacks.length).toBe(1)
const event = notifyCallbacks[0].toJSON()

// Verify that sensitive query parameters are redacted
expect(event.request.url).toBe('https://api.example.com/data?userId=[REDACTED]')
})
})
})
Loading
Loading