Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
afdc517
feat: add Salesforce POC with view tracking and e2e tests
BeltranBulbarellaDD Apr 22, 2026
36c6bfb
Remove view tracking and related code from Salesforce POC
BeltranBulbarellaDD Apr 22, 2026
98b9440
First pass: Preview the SDK from crashing by patching globalThis, rea…
BeltranBulbarellaDD Apr 22, 2026
730973c
Add cookie and location guards.
BeltranBulbarellaDD Apr 22, 2026
6fffdb7
Add first pass of e2e test setup for Salesforce POC.
BeltranBulbarellaDD Apr 23, 2026
794398d
Ignore CSP violation events when the environment rejects the event li…
BeltranBulbarellaDD Apr 24, 2026
4b9b229
Add view tracking by polling window.location.pathname.
BeltranBulbarellaDD Apr 24, 2026
dc44af1
Add e2e proxy for Salesforce RUM intake. Do not forward events to Dat…
BeltranBulbarellaDD Apr 24, 2026
f77d99a
Fix CI, Format and typecheck
BeltranBulbarellaDD Apr 24, 2026
d76ffe2
Add Salesforce POC tests for Dreamhouse Aura
BeltranBulbarellaDD Apr 28, 2026
4e8305c
a
BeltranBulbarellaDD Apr 28, 2026
5d2a05f
a
BeltranBulbarellaDD Apr 28, 2026
97cadad
Revert "Add Salesforce POC tests for Dreamhouse Aura"
BeltranBulbarellaDD Apr 28, 2026
a080cc3
Move salesforce package to its own package
BeltranBulbarellaDD Apr 29, 2026
e6a9854
Rely on self to access the sandbox global object when available
BeltranBulbarellaDD Apr 29, 2026
4935c49
Use regular rum package for salesforce
BeltranBulbarellaDD Apr 29, 2026
2a75938
Add polling for window.performance for view loading time.
BeltranBulbarellaDD May 5, 2026
7679084
Add Salesforce resource plugin and polling.
BeltranBulbarellaDD May 5, 2026
54b3c6c
gracefully degrade when CSS.escape or event.composedPath() are unavai…
BeltranBulbarellaDD May 12, 2026
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 package.json
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
"test:e2e:init": "yarn build && yarn build:apps && yarn playwright install chromium --with-deps",
"test:e2e": "playwright test --config test/e2e/playwright.local.config.ts --project chromium",
"test:e2e:bs": "node --env-file-if-exists=.env ./scripts/test/bs-wrapper.ts playwright test --config test/e2e/playwright.bs.config.ts",
"test:e2e:salesforce": "playwright test --config test/e2e/playwright.salesforce.config.ts",
"test:e2e:ci": "yarn test:e2e:init && yarn test:e2e",
"test:e2e:ci:bs": "yarn build && yarn build:apps && yarn test:e2e:bs",
"test:compat:tsc": "node scripts/check-typescript-compatibility.ts",
Expand Down
25 changes: 25 additions & 0 deletions packages/core/src/browser/addEventListener.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,31 @@ describe('addEventListener', () => {
expect(customEventTarget.removeEventListener).toHaveBeenCalled()
})

it('does not break stop() when removeEventListener is missing', () => {
const addEventListenerSpy = jasmine.createSpy()
const customEventTarget = {
addEventListener: addEventListenerSpy,
} as unknown as HTMLElement

const { stop } = addEventListener({ allowUntrustedEvents: false }, customEventTarget, 'change', noop)

expect(addEventListenerSpy).toHaveBeenCalled()
expect(stop).not.toThrow()
})

it('skips registration when addEventListener is missing', () => {
const listener = jasmine.createSpy()
const removeEventListenerSpy = jasmine.createSpy()
const customEventTarget = {
removeEventListener: removeEventListenerSpy,
} as unknown as HTMLElement

const { stop } = addEventListener({ allowUntrustedEvents: false }, customEventTarget, 'change', listener)

expect(stop).not.toThrow()
expect(removeEventListenerSpy).not.toHaveBeenCalled()
})

describe('Untrusted event', () => {
beforeEach(() => {
configuration = { allowUntrustedEvents: false } as Configuration
Expand Down
11 changes: 11 additions & 0 deletions packages/core/src/browser/addEventListener.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { monitor } from '../tools/monitor'
import { getZoneJsOriginalValue } from '../tools/getZoneJsOriginalValue'
import { noop } from '../tools/utils/functionUtils'
import type { CookieStore, CookieStoreEventMap, VisualViewport, VisualViewportEventMap } from './browser.types'

export type TrustableEvent<E extends Event = Event> = E & { __ddIsTrusted?: boolean }
Expand Down Expand Up @@ -132,10 +133,20 @@ export function addEventListeners<Target extends EventTarget, EventName extends
window.EventTarget && eventTarget instanceof EventTarget ? window.EventTarget.prototype : eventTarget

const add = getZoneJsOriginalValue(listenerTarget, 'addEventListener')
if (typeof add !== 'function') {
return {
stop: noop,
}
}

eventNames.forEach((eventName) => add.call(eventTarget, eventName, listenerWithMonitor, options))

function stop() {
const remove = getZoneJsOriginalValue(listenerTarget, 'removeEventListener')
if (typeof remove !== 'function') {
return
}

eventNames.forEach((eventName) => remove.call(eventTarget, eventName, listenerWithMonitor, options))
}

Expand Down
14 changes: 14 additions & 0 deletions packages/core/src/domain/report/reportObservable.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,4 +68,18 @@ describe('report observable', () => {
csp: { disposition: 'enforce' },
})
})

it(`should ignore ${RawReportType.cspViolation} when the environment rejects the event listener`, () => {
;(EventTarget.prototype.addEventListener as jasmine.Spy).and.callFake((type: string) => {
if (type === 'securitypolicyviolation') {
throw new Error('unsupported event listener')
}
})

expect(() => {
consoleSubscription = initReportObservable(configuration, [RawReportType.cspViolation]).subscribe(notifyReport)
}).not.toThrow()

expect(notifyReport).not.toHaveBeenCalled()
})
})
12 changes: 8 additions & 4 deletions packages/core/src/domain/report/reportObservable.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,11 +60,15 @@ function createReportObservable(reportTypes: ReportType[]) {

function createCspViolationReportObservable(configuration: Configuration) {
return new Observable<RawReportError>((observable) => {
const { stop } = addEventListener(configuration, document, DOM_EVENT.SECURITY_POLICY_VIOLATION, (event) => {
observable.notify(buildRawReportErrorFromCspViolation(event))
})
try {
const { stop } = addEventListener(configuration, document, DOM_EVENT.SECURITY_POLICY_VIOLATION, (event) => {
observable.notify(buildRawReportErrorFromCspViolation(event))
})

return stop
return stop
} catch {
return
}
})
}

Expand Down
70 changes: 70 additions & 0 deletions packages/core/src/tools/globalObject.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import { getGlobalObject } from './globalObject'

describe('getGlobalObject', () => {
it('returns self when globalThis is unavailable', () => {
const globalThisDescriptor = Object.getOwnPropertyDescriptor(window, 'globalThis')
const selfDescriptor = Object.getOwnPropertyDescriptor(window, 'self')

if (!globalThisDescriptor?.configurable) {
pending('globalThis descriptor is not configurable in this environment')
}
if (!selfDescriptor?.configurable) {
pending('self descriptor is not configurable in this environment')
}

const fakeSelf = { dd: 'sandbox-global' }

Object.defineProperty(window, 'globalThis', {
value: undefined,
configurable: true,
writable: true,
})
Object.defineProperty(window, 'self', {
value: fakeSelf,
configurable: true,
writable: true,
})

try {
expect(getGlobalObject()).toBe(fakeSelf)
} finally {
Object.defineProperty(window, 'globalThis', globalThisDescriptor!)
Object.defineProperty(window, 'self', selfDescriptor!)
}
})

it('returns self without relying on the Object.prototype fallback when globalThis is unavailable', () => {
const globalThisDescriptor = Object.getOwnPropertyDescriptor(window, 'globalThis')
const selfDescriptor = Object.getOwnPropertyDescriptor(window, 'self')

if (!globalThisDescriptor?.configurable) {
pending('globalThis descriptor is not configurable in this environment')
}
if (!selfDescriptor?.configurable) {
pending('self descriptor is not configurable in this environment')
}

const fakeSelf = { dd: 'sandbox-global' }

Object.defineProperty(window, 'globalThis', {
value: undefined,
configurable: true,
writable: true,
})
Object.defineProperty(window, 'self', {
value: fakeSelf,
configurable: true,
writable: true,
})

const definePropertySpy = spyOn(Object, 'defineProperty').and.callThrough()

try {
expect(getGlobalObject()).toBe(fakeSelf)
expect(definePropertySpy).not.toHaveBeenCalledWith(Object.prototype, '_dd_temp_', jasmine.any(Object))
} finally {
Object.defineProperty(window, 'globalThis', globalThisDescriptor!)
Object.defineProperty(window, 'self', selfDescriptor!)
}
})
})
50 changes: 31 additions & 19 deletions packages/core/src/tools/globalObject.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,27 +17,39 @@ export function getGlobalObject<T = typeof globalThis>(): T {
if (typeof globalThis === 'object') {
return globalThis as unknown as T
}
Object.defineProperty(Object.prototype, '_dd_temp_', {
get() {
return this as object
},
configurable: true,
})
// @ts-ignore _dd_temp is defined using defineProperty
let globalObject: unknown = _dd_temp_
// @ts-ignore _dd_temp is defined using defineProperty
delete Object.prototype._dd_temp_

// Under Lightning Web Security, third-party code should rely on `self` to
// access the sandbox global object. The Object.prototype fallback below can
// also fail there because Object.prototype is sealed.
if (typeof self === 'object') {
return self as unknown as T
}

if (typeof window === 'object') {
return window as unknown as T
}

let globalObject: unknown

try {
Object.defineProperty(Object.prototype, '_dd_temp_', {
get() {
return this as object
},
configurable: true,
})
// @ts-ignore _dd_temp is defined using defineProperty
globalObject = _dd_temp_
// @ts-ignore _dd_temp is defined using defineProperty
delete Object.prototype._dd_temp_
} catch {
globalObject = {}
}

if (typeof globalObject !== 'object') {
// on safari _dd_temp_ is available on window but not globally
// fallback on other browser globals check
if (typeof self === 'object') {
globalObject = self
} else if (typeof window === 'object') {
globalObject = window
} else {
globalObject = {}
}
globalObject = {}
}

return globalObject as T
}

Expand Down
37 changes: 37 additions & 0 deletions packages/core/src/tools/instrumentMethod.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,43 @@ describe('instrumentMethod', () => {
expect('onevent' in object).toBeFalse()
})

it('skips instrumentation on readonly methods', () => {
const originalMethod = () => 1
const object = {} as { method: () => number }
Object.defineProperty(object, 'method', {
value: originalMethod,
writable: false,
configurable: true,
})

const instrumentationSpy = jasmine.createSpy()
const { stop } = instrumentMethod(object, 'method', instrumentationSpy)

expect(object.method).toBe(originalMethod)
expect(object.method()).toBe(1)
expect(instrumentationSpy).not.toHaveBeenCalled()
expect(stop).not.toThrow()
})

it('skips instrumentation on readonly methods defined on the prototype chain', () => {
const originalMethod = jasmine.createSpy().and.returnValue(1)
const prototype = {} as { method: () => number }
Object.defineProperty(prototype, 'method', {
value: originalMethod,
writable: false,
configurable: true,
})
const object = Object.create(prototype) as { method: () => number }

const instrumentationSpy = jasmine.createSpy()
const { stop } = instrumentMethod(object, 'method', instrumentationSpy)

expect(object.method()).toBe(1)
expect(instrumentationSpy).not.toHaveBeenCalled()
expect(Object.prototype.hasOwnProperty.call(object, 'method')).toBeFalse()
expect(stop).not.toThrow()
})

it('calls the instrumentation with method target and parameters', () => {
const object = { method: (a: number, b: number) => a + b }
const instrumentationSpy = jasmine.createSpy<(call: InstrumentedMethodCall<typeof object, 'method'>) => void>()
Expand Down
39 changes: 37 additions & 2 deletions packages/core/src/tools/instrumentMethod.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,11 @@ export function instrumentMethod<TARGET extends { [key: string]: any }, METHOD e
onPreCall: (this: null, callInfos: InstrumentedMethodCall<TARGET, METHOD>) => void,
{ computeHandlingStack }: { computeHandlingStack?: boolean } = {}
) {
const methodDescriptor = findDescriptorInPrototypeChain(targetPrototype, method)
if (methodDescriptor && !canAssignDescriptor(methodDescriptor)) {
return { stop: noop }
}

let original = targetPrototype[method]

if (typeof original !== 'function') {
Expand Down Expand Up @@ -117,14 +122,22 @@ export function instrumentMethod<TARGET extends { [key: string]: any }, METHOD e
return result
}

targetPrototype[method] = instrumentation as TARGET[METHOD]
try {
targetPrototype[method] = instrumentation as TARGET[METHOD]
} catch {
return { stop: noop }
}

return {
stop: () => {
stopped = true
// If the instrumentation has been removed by a third party, keep the last one
if (targetPrototype[method] === instrumentation) {
targetPrototype[method] = original
try {
targetPrototype[method] = original
} catch {
// Ignore restore failures on readonly properties.
}
}
},
}
Expand Down Expand Up @@ -168,3 +181,25 @@ export function instrumentSetter<TARGET extends { [key: string]: any }, PROPERTY
},
}
}

function findDescriptorInPrototypeChain(target: object, property: PropertyKey): PropertyDescriptor | undefined {
let currentTarget: object | null = target

while (currentTarget) {
const descriptor = Object.getOwnPropertyDescriptor(currentTarget, property)
if (descriptor) {
return descriptor
}
currentTarget = Object.getPrototypeOf(currentTarget)
}

return undefined
}

function canAssignDescriptor(descriptor: PropertyDescriptor) {
if ('writable' in descriptor) {
return descriptor.writable !== false
}

return typeof descriptor.set === 'function'
}
23 changes: 23 additions & 0 deletions packages/rum-core/src/browser/cookieObservable.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -108,4 +108,27 @@ describe('cookieObservable', () => {

expect(cookieChanges).toEqual(['foo', 'bar'])
})

it('should fallback to polling when cookieStore rejects change listeners', () => {
Object.defineProperty(window, 'cookieStore', {
configurable: true,
get: () => ({
addEventListener: () => {
throw new Error("Lightning Web Security: Cannot add 'change' event listener to CookieStore object.")
},
removeEventListener: () => undefined,
}),
})
const observable = createCookieObservable(mockRumConfiguration(), COOKIE_NAME)

let cookieChange: string | undefined
expect(() => {
subscription = observable.subscribe((change) => (cookieChange = change))
}).not.toThrow()

setCookie(COOKIE_NAME, 'foo', COOKIE_DURATION)
clock.tick(WATCH_COOKIE_INTERVAL_DELAY)

expect(cookieChange).toEqual('foo')
})
})
Loading
Loading