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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

### Changed

(plugin-network-instrumentation) Manually parse URLs to improve React Native compatibility [#2674](https://github.com/bugsnag/bugsnag-js/pull/2674)
Update bugsnag-android to [v6.22.0](https//github.com/bugsnag/bugsnag-android/releases/tag/v6.22.0) [#2656](https://github.com/bugsnag/bugsnag-js/pull/2656)

### Fixed
Expand Down
1 change: 1 addition & 0 deletions packages/node/test/notifier.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ describe('node notifier', () => {
beforeAll(() => {
jest.spyOn(console, 'debug').mockImplementation(() => {})
jest.spyOn(console, 'warn').mockImplementation(() => {})
jest.spyOn(console, 'log').mockImplementation(() => {})
})

beforeEach(() => {
Expand Down
4 changes: 4 additions & 0 deletions packages/plugin-aws-lambda/test/serverless-express.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,10 @@ let sentEvents
let sentSessions

describe('serverless express', function () {
beforeAll(() => {
jest.spyOn(console, 'debug').mockImplementation(() => {})
})

beforeEach(function () {
sentEvents = []
sentSessions = []
Expand Down
25 changes: 23 additions & 2 deletions packages/plugin-network-instrumentation/lib/extract-domain.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,29 @@
*/
module.exports = function (url) {
try {
const urlObj = new URL(url)
return urlObj.host
const isAbsolute = /^https?:\/\//i.test(url)
if (!isAbsolute) {
return 'unknown'
}

const urlWithoutProtocol = url.replace(/^https?:\/\//i, '')

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

return urlWithoutProtocol.substring(0, endIndex)
} catch (e) {
return 'unknown'
}
Expand Down
22 changes: 17 additions & 5 deletions packages/plugin-network-instrumentation/lib/parse-query-params.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,24 @@
*/
module.exports = function (url) {
try {
const urlObj = new URL(url)
const params = {}
urlObj.searchParams.forEach((value, key) => {
params[key] = value
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)
}

// convert query string to object without using UrlSearchParams
const queryStringObject = {}
const pairs = queryString.split('&').filter(pair => pair.length > 0)
pairs.forEach(pair => {
const [key, value] = pair.split('=')
queryStringObject[decodeURIComponent(key)] = decodeURIComponent(value || '')
})
return params

return queryStringObject
} catch (e) {
return {}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,38 +1,23 @@
const parseQueryParams = require('./parse-query-params')
const redactValues = require('./redact-values')

function isAbsoluteURL (url) {
try {
// eslint-disable-next-line no-new
new URL(url)
return true
} catch (e) {
return false
}
}

module.exports = function (url, redactedKeys) {
const isAbsolute = isAbsoluteURL(url)
const base = isAbsolute ? undefined : 'http://localhost'

// Parse the URL - use a base only for relative URLs
const urlObj = new URL(url, base)
const params = new URLSearchParams(urlObj.search)

// Convert URLSearchParams to object without using Object.fromEntries()
const paramsObject = {}
params.forEach((value, key) => {
paramsObject[key] = value
})

const paramsObject = parseQueryParams(url)
const redactedParams = redactValues(paramsObject, redactedKeys)
urlObj.search = new URLSearchParams(redactedParams).toString()
const redactedQueryString = Object.entries(redactedParams).map(([key, value]) => `${key}=${value}`).join('&')
Copy link

Copilot AI Feb 4, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Query parameter values should be URI-encoded when constructing the query string. Without encoding, special characters (like '&', '=', or spaces) in parameter values will break the URL format. Use encodeURIComponent(value) when building the query string.

Suggested change
const redactedQueryString = Object.entries(redactedParams).map(([key, value]) => `${key}=${value}`).join('&')
const redactedQueryString = Object.entries(redactedParams).map(([key, value]) => `${key}=${encodeURIComponent(value)}`).join('&')

Copilot uses AI. Check for mistakes.
Copy link

Copilot AI Feb 4, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Query parameter keys should also be URI-encoded when constructing the query string. Keys may contain special characters that need to be encoded. Use encodeURIComponent(key) alongside encoding the value.

Suggested change
const redactedQueryString = Object.entries(redactedParams).map(([key, value]) => `${key}=${value}`).join('&')
const redactedQueryString = Object.entries(redactedParams)
.map(([key, value]) => `${encodeURIComponent(key)}=${encodeURIComponent(value)}`)
.join('&')

Copilot uses AI. Check for mistakes.

// Return appropriate format based on original URL type
if (isAbsolute) {
return decodeURI(urlObj.toString())
const queryStart = url.indexOf('?')
const hashStart = url.indexOf('#')
const hash = hashStart !== -1 ? url.substring(hashStart) : ''
let result = queryStart !== -1 ? url.substring(0, queryStart) : url

// Build the result URL manually
if (redactedQueryString && redactedQueryString.length > 0) {
result += '?' + redactedQueryString
}
if (hash) {
result += hash
}

// For relative URLs, return only the path + search + hash components
const relativePart = urlObj.pathname + urlObj.search + urlObj.hash
return decodeURI(relativePart)
return result
}
8 changes: 4 additions & 4 deletions test/browser/features/http_errors.feature
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ Feature: HTTP Errors
And the exception "errorClass" equals "HTTPError"
And the error payload field "events.0.exceptions.0.message" equals the stored value "expected.exception.message"
And the event "severity" equals "error"
And the event "unhandled" is true
And the event "unhandled" is false
Comment thread
lemnik marked this conversation as resolved.
And the event "severityReason.type" equals "httpError"
And the error payload field "events.0.context" equals the stored value "expected.context"

Expand Down Expand Up @@ -46,7 +46,7 @@ Feature: HTTP Errors
And the exception "errorClass" equals "HTTPError"
And the error payload field "events.0.exceptions.0.message" equals the stored value "expected.exception.message"
And the event "severity" equals "error"
And the event "unhandled" is true
And the event "unhandled" is false
And the event "severityReason.type" equals "httpError"
And the error payload field "events.0.context" equals the stored value "expected.context"

Expand Down Expand Up @@ -78,7 +78,7 @@ Feature: HTTP Errors
And the exception "errorClass" equals "HTTPError"
And the error payload field "events.0.exceptions.0.message" equals the stored value "expected.exception.message"
And the event "severity" equals "error"
And the event "unhandled" is true
And the event "unhandled" is false
And the event "severityReason.type" equals "httpError"
And the error payload field "events.0.context" equals the stored value "expected.context"

Expand Down Expand Up @@ -109,7 +109,7 @@ Feature: HTTP Errors
And the exception "errorClass" equals "HTTPError"
And the error payload field "events.0.exceptions.0.message" equals the stored value "expected.exception.message"
And the event "severity" equals "error"
And the event "unhandled" is true
And the event "unhandled" is false
And the event "severityReason.type" equals "httpError"
And the error payload field "events.0.context" equals the stored value "expected.context"

Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[
{
"message": "^401: .*\/reflect\/[?]status=401$",
"message": "^401: .*\/reflect[?]status=401$",
"errorClass": "HTTPError",
"type": "reactnativejs",
"stacktrace": "IGNORE"
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[
{
"message": "^500: .*\/reflect\/[?]status=500$",
"message": "^500: .*\/reflect[?]status=500$",
"errorClass": "HTTPError",
"type": "reactnativejs",
"stacktrace": "IGNORE"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,7 @@ export class NetworkRequestScenario extends Scenario {
}

run () {
const url = new URL(this.reflectEndpoint)
url.searchParams.append('status', this.statusCode)
fetch(url).catch((err) => {
fetch(`${this.reflectEndpoint}?status=${this.statusCode}`).catch((err) => {
Bugsnag.notify(err)
})
}
Expand Down