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
69 changes: 68 additions & 1 deletion packages/core/src/browser/xhrObservable.spec.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import type { Configuration } from '../domain/configuration'
import { withXhr, mockXhr } from '../../test'
import type { Subscription } from '../tools/observable'
import { noop } from '../tools/utils/functionUtils'
import type { XhrCompleteContext, XhrContext } from './xhrObservable'
import { initXhrObservable } from './xhrObservable'
import { initXhrObservable, resetXhrObservable } from './xhrObservable'

describe('xhr observable', () => {
let requestsTrackingSubscription: Subscription
Expand Down Expand Up @@ -402,4 +403,70 @@ describe('xhr observable', () => {
},
})
})

describe('with conflicting allowUntrustedEvents policies across callers', () => {
// Reproduces the bug where the early bufferedData call to
// initXhrObservable({ allowUntrustedEvents: true }) creates the singleton
// and a later initXhrObservable(customerConfig) silently reuses it,
// discarding the customer's stricter policy.
let policySubscription: Subscription
let policyContexts: XhrContext[]

beforeEach(() => {
requestsTrackingSubscription.unsubscribe()
resetXhrObservable()
policyContexts = []
})

afterEach(() => {
policySubscription.unsubscribe()
resetXhrObservable()
// The outer afterEach calls requestsTrackingSubscription.unsubscribe(); we already
// unsubscribed it in beforeEach, so give it a no-op stub.
requestsTrackingSubscription = { unsubscribe: noop }
})

it('does not emit completion for an untrusted loadend when the customer disallows it', (done) => {
// First caller (bufferedData) opts into untrusted events.
initXhrObservable({ allowUntrustedEvents: true })
// Second caller (customer config) opts out.
policySubscription = initXhrObservable({ allowUntrustedEvents: false }).subscribe((context) => {
policyContexts.push(context)
})

withXhr({
setup(xhr) {
xhr.open('GET', '/ok')
xhr.send()
// Untrusted Event (constructor-built, no __ddIsTrusted marker)
xhr.dispatchEvent(new Event('loadend'))
},
onComplete() {
const completions = policyContexts.filter((context) => context.state === 'complete')
expect(completions).toEqual([])
done()
},
})
})

it('emits completion for an untrusted loadend when every caller allows it', (done) => {
initXhrObservable({ allowUntrustedEvents: true })
policySubscription = initXhrObservable({ allowUntrustedEvents: true }).subscribe((context) => {
policyContexts.push(context)
})

withXhr({
setup(xhr) {
xhr.open('GET', '/ok')
xhr.send()
xhr.dispatchEvent(new Event('loadend'))
},
onComplete() {
const completions = policyContexts.filter((context) => context.state === 'complete')
expect(completions.length).toBe(1)
done()
},
})
})
})
})
20 changes: 14 additions & 6 deletions packages/core/src/browser/xhrObservable.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,14 +34,22 @@ export type XhrContext = XhrOpenContext | XhrStartContext | XhrCompleteContext
let xhrObservable: Observable<XhrContext> | undefined
const xhrContexts = new WeakMap<XMLHttpRequest, XhrContext>()

export function initXhrObservable(configuration: { allowUntrustedEvents?: boolean | undefined }) {
// The singleton XHR observable applies the latest caller's allowUntrustedEvents
// policy so that the customer's configuration overrides the early call from
// bufferedData (which always opts in before the customer config is parsed).
let allowUntrustedEvents: boolean | undefined

export function initXhrObservable(configuration: { allowUntrustedEvents?: boolean | undefined } = {}) {
if (configuration.allowUntrustedEvents !== undefined) {
allowUntrustedEvents = configuration.allowUntrustedEvents
}
Comment on lines +43 to +45
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Preserve opt-out when a later caller opts in

When any later caller passes { allowUntrustedEvents: true }, this assignment overwrites a prior customer opt-out, so XHRs sent after that point capture true in the loadend listener and synthetic loadend events are accepted. This can happen, for example, if RUM has already initialized with the default false and another SDK/public API starts buffering data later (startBufferingData() calls initXhrObservable({ allowUntrustedEvents: true })); the stricter policy is then lost until another false call happens, contrary to the singleton needing to honor all subscribers' policies.

Useful? React with πŸ‘Β / πŸ‘Ž.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

it's fine, it's not expected to have the SDK initialize more that once

if (!xhrObservable) {
xhrObservable = createXhrObservable(configuration)
xhrObservable = createXhrObservable()
}
return xhrObservable
}

function createXhrObservable(configuration: { allowUntrustedEvents?: boolean | undefined }) {
function createXhrObservable() {
if (!('XMLHttpRequest' in globalObject)) {
return new Observable<XhrContext>()
}
Expand All @@ -53,7 +61,7 @@ function createXhrObservable(configuration: { allowUntrustedEvents?: boolean | u
XMLHttpRequest.prototype,
'send',
(call) => {
sendXhr(call, configuration, observable)
sendXhr(call, observable)
},
{ computeHandlingStack: true }
)
Expand All @@ -78,7 +86,6 @@ function openXhr({ target: xhr, parameters: [method, url] }: InstrumentedMethodC

function sendXhr(
{ target: xhr, parameters: [body], handlingStack }: InstrumentedMethodCall<XMLHttpRequest, 'send'>,
configuration: { allowUntrustedEvents?: boolean | undefined },
observable: Observable<XhrContext>
) {
const context = xhrContexts.get(xhr)
Expand Down Expand Up @@ -123,7 +130,7 @@ function sendXhr(
observable.notify({ ...completeContext, state: 'complete' })
}

const { stop: unsubscribeLoadEndListener } = addEventListener(configuration, xhr, 'loadend', onEnd)
const { stop: unsubscribeLoadEndListener } = addEventListener({ allowUntrustedEvents }, xhr, 'loadend', onEnd)

observable.notify(startContext)
}
Expand All @@ -142,4 +149,5 @@ function abortXhr({ target: xhr }: InstrumentedMethodCall<XMLHttpRequest, 'abort
*/
export function resetXhrObservable() {
xhrObservable = undefined
allowUntrustedEvents = undefined
}
Loading