From afdc5179c7774d2984a8f27f1d3f5b7fdeb28100 Mon Sep 17 00:00:00 2001 From: "beltran.bulbarella" Date: Wed, 22 Apr 2026 13:34:03 +0200 Subject: [PATCH 01/16] feat: add Salesforce POC with view tracking and e2e tests --- package.json | 2 + packages/core/src/browser/addEventListener.ts | 25 ++++- packages/core/src/tools/globalObject.ts | 14 +-- packages/core/src/tools/instrumentMethod.ts | 41 ++++++- packages/core/src/tools/utils/urlPolyfill.ts | 7 +- packages/rum-core/src/boot/preStartRum.ts | 10 +- .../rum-core/src/boot/rumPublicApi.spec.ts | 22 ++++ packages/rum-core/src/boot/rumPublicApi.ts | 13 ++- .../rum-core/src/boot/runtimeCapabilities.ts | 26 +++++ packages/rum-core/src/boot/startRum.ts | 29 +++-- .../rum-core/src/browser/cookieObservable.ts | 24 +++++ .../src/browser/locationChangeObservable.ts | 26 ++++- .../src/domain/contexts/urlContexts.ts | 7 +- .../rum-core/src/domain/view/trackViews.ts | 41 ++++--- .../src/domain/view/viewCollection.ts | 6 +- packages/rum-slim/README.md | 12 +++ packages/rum-slim/package.json | 1 + .../boot/salesforceInitConfiguration.spec.ts | 40 +++++++ .../src/boot/salesforceInitConfiguration.ts | 18 ++++ packages/rum-slim/src/entries/salesforce.ts | 81 ++++++++++++++ rum-events-format | 2 +- scripts/build/build-package.ts | 8 +- test/e2e/lib/helpers/playwright.ts | 13 ++- test/e2e/lib/salesforce.ts | 98 +++++++++++++++++ test/e2e/playwright.salesforce.base.config.ts | 11 ++ test/e2e/playwright.salesforce.bs.config.ts | 22 ++++ .../e2e/playwright.salesforce.local.config.ts | 17 +++ test/e2e/scenario/salesforce/README.md | 9 ++ .../e2e/scenario/salesforce/views.scenario.ts | 101 ++++++++++++++++++ 29 files changed, 673 insertions(+), 53 deletions(-) create mode 100644 packages/rum-core/src/boot/runtimeCapabilities.ts create mode 100644 packages/rum-slim/src/boot/salesforceInitConfiguration.spec.ts create mode 100644 packages/rum-slim/src/boot/salesforceInitConfiguration.ts create mode 100644 packages/rum-slim/src/entries/salesforce.ts create mode 100644 test/e2e/lib/salesforce.ts create mode 100644 test/e2e/playwright.salesforce.base.config.ts create mode 100644 test/e2e/playwright.salesforce.bs.config.ts create mode 100644 test/e2e/playwright.salesforce.local.config.ts create mode 100644 test/e2e/scenario/salesforce/README.md create mode 100644 test/e2e/scenario/salesforce/views.scenario.ts diff --git a/package.json b/package.json index 52d0d958ff..74e65b0eee 100644 --- a/package.json +++ b/package.json @@ -32,7 +32,9 @@ "test:unit:bs": "node --env-file-if-exists=.env ./scripts/test/bs-wrapper.ts karma start test/unit/karma.bs.conf.js", "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:salesforce": "playwright test --config test/e2e/playwright.salesforce.local.config.ts", "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:bs": "node --env-file-if-exists=.env ./scripts/test/bs-wrapper.ts playwright test --config test/e2e/playwright.salesforce.bs.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", diff --git a/packages/core/src/browser/addEventListener.ts b/packages/core/src/browser/addEventListener.ts index 6fb2c43ba5..7af92a4b05 100644 --- a/packages/core/src/browser/addEventListener.ts +++ b/packages/core/src/browser/addEventListener.ts @@ -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 & { __ddIsTrusted?: boolean } @@ -132,11 +133,31 @@ export function addEventListeners add.call(eventTarget, eventName, listenerWithMonitor, options)) + console.log('datadogRUM: add', add) + if (typeof add !== 'function') { + return { + stop: noop, + } + } + + try { + eventNames.forEach((eventName) => add.call(eventTarget, eventName, listenerWithMonitor, options)) + } catch { + return { + stop: noop, + } + } function stop() { const remove = getZoneJsOriginalValue(listenerTarget, 'removeEventListener') - eventNames.forEach((eventName) => remove.call(eventTarget, eventName, listenerWithMonitor, options)) + if (typeof remove !== 'function') { + return + } + try { + eventNames.forEach((eventName) => remove.call(eventTarget, eventName, listenerWithMonitor, options)) + } catch { + // Ignore cleanup errors on runtimes exposing partial EventTarget implementations. + } } return { diff --git a/packages/core/src/tools/globalObject.ts b/packages/core/src/tools/globalObject.ts index ce9b637c02..2723ea6bbd 100644 --- a/packages/core/src/tools/globalObject.ts +++ b/packages/core/src/tools/globalObject.ts @@ -17,6 +17,12 @@ export function getGlobalObject(): T { if (typeof globalThis === 'object') { return globalThis as unknown as T } + if (typeof self === 'object') { + return self as unknown as T + } + if (typeof window === 'object') { + return window as unknown as T + } Object.defineProperty(Object.prototype, '_dd_temp_', { get() { return this as object @@ -30,13 +36,7 @@ export function getGlobalObject(): T { 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 } diff --git a/packages/core/src/tools/instrumentMethod.ts b/packages/core/src/tools/instrumentMethod.ts index 5d5a398824..2339dde2a2 100644 --- a/packages/core/src/tools/instrumentMethod.ts +++ b/packages/core/src/tools/instrumentMethod.ts @@ -74,6 +74,7 @@ export function instrumentMethod { @@ -117,19 +126,45 @@ export function instrumentMethod { stopped = true // If the instrumentation has been removed by a third party, keep the last one - if (targetPrototype[method] === instrumentation) { - targetPrototype[method] = original + try { + if (targetPrototype[method] === instrumentation) { + targetPrototype[method] = original + } + } catch { + // Some runtimes expose instrumentable methods as readonly. In that case, the + // instrumentation is already inert thanks to the `stopped` flag above. } }, } } +function findPropertyDescriptor( + target: TARGET, + method: METHOD +): PropertyDescriptor | undefined { + let currentTarget: object | null = target + + while (currentTarget) { + const descriptor = Object.getOwnPropertyDescriptor(currentTarget, method) + if (descriptor) { + return descriptor + } + currentTarget = Object.getPrototypeOf(currentTarget) + } + + return undefined +} + export function instrumentSetter( targetPrototype: TARGET, property: PROPERTY, diff --git a/packages/core/src/tools/utils/urlPolyfill.ts b/packages/core/src/tools/utils/urlPolyfill.ts index 812b407bb7..7eea9a51cf 100644 --- a/packages/core/src/tools/utils/urlPolyfill.ts +++ b/packages/core/src/tools/utils/urlPolyfill.ts @@ -1,7 +1,7 @@ import { globalObject } from '../globalObject' export function normalizeUrl(url: string) { - return buildUrl(url, location.href).href + return buildUrl(url, getLocationHref()).href } export function isValidUrl(url: string) { @@ -27,6 +27,11 @@ export function buildUrl(url: string, base?: string) { } } +function getLocationHref() { + const href = (globalObject as Window & typeof globalThis).location?.href + return typeof href === 'string' && href.trim() ? href : undefined +} + /** * Get native URL constructor from a clean iframe * This avoids polyfill issues by getting the native implementation from a fresh iframe context diff --git a/packages/rum-core/src/boot/preStartRum.ts b/packages/rum-core/src/boot/preStartRum.ts index 58cf5ad3e4..7e1962505c 100644 --- a/packages/rum-core/src/boot/preStartRum.ts +++ b/packages/rum-core/src/boot/preStartRum.ts @@ -59,7 +59,11 @@ export type DoStartRum = ( ) => StartRumResult export function createPreStartStrategy( - { ignoreInitIfSyntheticsWillInjectRum = true, startDeflateWorker }: RumPublicApiOptions, + { + ignoreInitIfSyntheticsWillInjectRum = true, + startDeflateWorker, + runtimeCapabilities, + }: RumPublicApiOptions, trackingConsentState: TrackingConsentState, customVitalsState: CustomVitalsState, doStartRum: DoStartRum @@ -169,7 +173,9 @@ export function createPreStartStrategy( // This is needed in case the consent is not granted and some customer // library (Apollo Client) is storing uninstrumented fetch to be used later // The subscription is needed so that the instrumentation process is completed - initFetchObservable().subscribe(noop) + if (runtimeCapabilities?.requestCollection !== false) { + initFetchObservable().subscribe(noop) + } trackingConsentState.tryToInit(configuration.trackingConsent) tryStartRum() diff --git a/packages/rum-core/src/boot/rumPublicApi.spec.ts b/packages/rum-core/src/boot/rumPublicApi.spec.ts index e9bbccc102..8f2eef5e4d 100644 --- a/packages/rum-core/src/boot/rumPublicApi.spec.ts +++ b/packages/rum-core/src/boot/rumPublicApi.spec.ts @@ -1134,6 +1134,28 @@ describe('rum public api', () => { expect(sdkName).toBe('rum-slim') }) }) + + describe('runtime capabilities', () => { + it('passes them down to startRum', () => { + const { rumPublicApi, startRumSpy } = makeRumPublicApiWithDefaults({ + rumPublicApiOptions: { + runtimeCapabilities: { + requestCollection: false, + runtimeErrors: false, + viewMetrics: false, + }, + }, + }) + + rumPublicApi.init(DEFAULT_INIT_CONFIGURATION) + + expect(startRumSpy.calls.argsFor(0)[11]).toEqual({ + requestCollection: false, + runtimeErrors: false, + viewMetrics: false, + }) + }) + }) }) function makeRumPublicApiWithDefaults({ diff --git a/packages/rum-core/src/boot/rumPublicApi.ts b/packages/rum-core/src/boot/rumPublicApi.ts index a3fd82649a..ccc3c20dc9 100644 --- a/packages/rum-core/src/boot/rumPublicApi.ts +++ b/packages/rum-core/src/boot/rumPublicApi.ts @@ -15,6 +15,7 @@ import type { Telemetry, Encoder, ResourceType, + BufferedData, } from '@datadog/browser-core' import { ContextManagerMethod, @@ -38,6 +39,7 @@ import { mockable, generateUUID, timeStampNow, + BufferedObservable, } from '@datadog/browser-core' import type { LifeCycle } from '../domain/lifeCycle' @@ -63,6 +65,8 @@ import type { ResourceOptions, ResourceStopOptions } from '../domain/resource/tr import { createPreStartStrategy } from './preStartRum' import type { StartRumResult } from './startRum' import { startRum } from './startRum' +import type { RumRuntimeCapabilities } from './runtimeCapabilities' +import { resolveRumRuntimeCapabilities } from './runtimeCapabilities' export interface StartRecordingOptions { force: boolean @@ -556,6 +560,7 @@ export interface RumPublicApiOptions { streamId: DeflateEncoderStreamId ) => DeflateEncoder sdkName?: SdkName + runtimeCapabilities?: RumRuntimeCapabilities } export interface Strategy { @@ -596,7 +601,10 @@ export function makeRumPublicApi( ): RumPublicApi { const trackingConsentState = createTrackingConsentState() const customVitalsState = createCustomVitalsState() - const bufferedDataObservable = startBufferingData().observable + const runtimeCapabilities = resolveRumRuntimeCapabilities(options.runtimeCapabilities) + const bufferedDataObservable = runtimeCapabilities.runtimeErrors + ? startBufferingData().observable + : new BufferedObservable(0) let strategy = createPreStartStrategy( options, @@ -619,7 +627,8 @@ export function makeRumPublicApi( bufferedDataObservable, telemetry, hooks, - options.sdkName + options.sdkName, + runtimeCapabilities ) recorderApi.onRumStart( diff --git a/packages/rum-core/src/boot/runtimeCapabilities.ts b/packages/rum-core/src/boot/runtimeCapabilities.ts new file mode 100644 index 0000000000..52ceac345f --- /dev/null +++ b/packages/rum-core/src/boot/runtimeCapabilities.ts @@ -0,0 +1,26 @@ +export interface RumRuntimeCapabilities { + requestCollection?: boolean + runtimeErrors?: boolean + viewMetrics?: boolean +} + +export interface ResolvedRumRuntimeCapabilities { + requestCollection: boolean + runtimeErrors: boolean + viewMetrics: boolean +} + +export const DEFAULT_RUM_RUNTIME_CAPABILITIES: ResolvedRumRuntimeCapabilities = { + requestCollection: true, + runtimeErrors: true, + viewMetrics: true, +} + +export function resolveRumRuntimeCapabilities( + runtimeCapabilities?: RumRuntimeCapabilities +): ResolvedRumRuntimeCapabilities { + return { + ...DEFAULT_RUM_RUNTIME_CAPABILITIES, + ...runtimeCapabilities, + } +} diff --git a/packages/rum-core/src/boot/startRum.ts b/packages/rum-core/src/boot/startRum.ts index 4dfd44e233..ab4119dca4 100644 --- a/packages/rum-core/src/boot/startRum.ts +++ b/packages/rum-core/src/boot/startRum.ts @@ -17,6 +17,7 @@ import { startGlobalContext, startUserContext, startTabContext, + noop, } from '@datadog/browser-core' import { createDOMMutationObservable } from '../browser/domMutationObservable' import { createWindowOpenObservable } from '../browser/windowOpenObservable' @@ -25,7 +26,7 @@ import { LifeCycle, LifeCycleEventType } from '../domain/lifeCycle' import { startViewHistory } from '../domain/contexts/viewHistory' import { startRequestCollection } from '../domain/requestCollection' import { startActionCollection } from '../domain/action/actionCollection' -import { startErrorCollection } from '../domain/error/errorCollection' +import { doStartErrorCollection, startErrorCollection } from '../domain/error/errorCollection' import { startResourceCollection } from '../domain/resource/resourceCollection' import { startViewCollection } from '../domain/view/viewCollection' import type { RumSessionManager } from '../domain/rumSessionManager' @@ -56,6 +57,8 @@ import { startEventCollection } from '../domain/event/eventCollection' import { startInitialViewMetricsTelemetry } from '../domain/view/viewMetrics/startInitialViewMetricsTelemetry' import { startSourceCodeContext } from '../domain/contexts/sourceCodeContext' import type { RecorderApi, ProfilerApi } from './rumPublicApi' +import type { ResolvedRumRuntimeCapabilities } from './runtimeCapabilities' +import { DEFAULT_RUM_RUNTIME_CAPABILITIES } from './runtimeCapabilities' export type StartRum = typeof startRum export type StartRumResult = ReturnType @@ -75,7 +78,8 @@ export function startRum( bufferedDataObservable: BufferedObservable, telemetry: Telemetry, hooks: Hooks, - sdkName?: SdkName + sdkName?: SdkName, + runtimeCapabilities: ResolvedRumRuntimeCapabilities = DEFAULT_RUM_RUNTIME_CAPABILITIES ) { const cleanupTasks: Array<() => void> = [] const lifeCycle = new LifeCycle() @@ -119,7 +123,9 @@ export function startRum( startTrackingConsentContext(hooks, trackingConsentState) - const { stop: stopInitialViewMetricsTelemetry } = startInitialViewMetricsTelemetry(lifeCycle, telemetry) + const { stop: stopInitialViewMetricsTelemetry } = runtimeCapabilities.viewMetrics + ? startInitialViewMetricsTelemetry(lifeCycle, telemetry) + : { stop: noop } cleanupTasks.push(stopInitialViewMetricsTelemetry) const { stop: stopRumEventCollection, ...startRumEventCollectionResult } = startRumEventCollection( @@ -132,7 +138,8 @@ export function startRum( customVitalsState, bufferedDataObservable, sdkName, - reportError + reportError, + runtimeCapabilities ) cleanupTasks.push(stopRumEventCollection) bufferedDataObservable.unbuffer() @@ -163,7 +170,8 @@ export function startRumEventCollection( customVitalsState: CustomVitalsState, bufferedDataObservable: Observable, sdkName: SdkName | undefined, - reportError: (error: RawError) => void + reportError: (error: RawError) => void, + runtimeCapabilities: ResolvedRumRuntimeCapabilities = DEFAULT_RUM_RUNTIME_CAPABILITIES ) { const cleanupTasks: Array<() => void> = [] @@ -224,7 +232,8 @@ export function startRumEventCollection( locationChangeObservable, recorderApi, viewHistory, - initialViewOptions + initialViewOptions, + runtimeCapabilities.viewMetrics ) startSourceCodeContext(hooks) @@ -237,9 +246,13 @@ export function startRumEventCollection( const { stop: stopLongTaskCollection } = startLongTaskCollection(lifeCycle, configuration) cleanupTasks.push(stopLongTaskCollection) - const { addError } = startErrorCollection(lifeCycle, configuration, bufferedDataObservable) + const { addError } = runtimeCapabilities.runtimeErrors + ? startErrorCollection(lifeCycle, configuration, bufferedDataObservable) + : doStartErrorCollection(lifeCycle) - startRequestCollection(lifeCycle, configuration, session, userContext, accountContext) + if (runtimeCapabilities.requestCollection) { + startRequestCollection(lifeCycle, configuration, session, userContext, accountContext) + } const vitalCollection = startVitalCollection(lifeCycle, pageStateHistory, customVitalsState) diff --git a/packages/rum-core/src/browser/cookieObservable.ts b/packages/rum-core/src/browser/cookieObservable.ts index 3bb3080d19..6285d1cded 100644 --- a/packages/rum-core/src/browser/cookieObservable.ts +++ b/packages/rum-core/src/browser/cookieObservable.ts @@ -7,6 +7,7 @@ import { ONE_SECOND, findCommaSeparatedValue, DOM_EVENT, + getZoneJsOriginalValue, } from '@datadog/browser-core' export interface CookieStoreWindow { @@ -27,6 +28,10 @@ export function createCookieObservable(configuration: Configuration, cookieName: function listenToCookieStoreChange(configuration: Configuration) { return (cookieName: string, callback: (event: string | undefined) => void) => { + if (!canListenToCookieStoreChanges()) { + return watchCookieFallback(cookieName, callback) + } + const listener = addEventListener( configuration, (window as CookieStoreWindow).cookieStore!, @@ -46,6 +51,25 @@ function listenToCookieStoreChange(configuration: Configuration) { } } +function canListenToCookieStoreChanges() { + const cookieStore = (window as CookieStoreWindow).cookieStore + const add = cookieStore && getZoneJsOriginalValue(cookieStore, 'addEventListener') + const remove = cookieStore && getZoneJsOriginalValue(cookieStore, 'removeEventListener') + + if (typeof add !== 'function' || typeof remove !== 'function') { + return false + } + + const noopListener = () => undefined + try { + add.call(cookieStore, DOM_EVENT.CHANGE, noopListener) + remove.call(cookieStore, DOM_EVENT.CHANGE, noopListener) + return true + } catch { + return false + } +} + export const WATCH_COOKIE_INTERVAL_DELAY = ONE_SECOND function watchCookieFallback(cookieName: string, callback: (event: string | undefined) => void) { diff --git a/packages/rum-core/src/browser/locationChangeObservable.ts b/packages/rum-core/src/browser/locationChangeObservable.ts index fefa7d6ee0..1431270fdc 100644 --- a/packages/rum-core/src/browser/locationChangeObservable.ts +++ b/packages/rum-core/src/browser/locationChangeObservable.ts @@ -7,22 +7,22 @@ export interface LocationChange { } export function createLocationChangeObservable(configuration: RumConfiguration) { - let currentLocation = shallowClone(location) + let currentLocation = getLocationSnapshot() return new Observable((observable) => { const { stop: stopHistoryTracking } = trackHistory(configuration, onLocationChange) const { stop: stopHashTracking } = trackHash(configuration, onLocationChange) function onLocationChange() { - if (currentLocation.href === location.href) { + const nextLocation = getLocationSnapshot() + if (currentLocation.href === nextLocation.href) { return } - const newLocation = shallowClone(location) observable.notify({ - newLocation, + newLocation: nextLocation, oldLocation: currentLocation, }) - currentLocation = newLocation + currentLocation = nextLocation } return () => { @@ -32,6 +32,22 @@ export function createLocationChangeObservable(configuration: RumConfiguration) }) } +function getLocationSnapshot(): Readonly { + const currentLocation = window.location + return shallowClone({ + ancestorOrigins: currentLocation?.ancestorOrigins, + hash: currentLocation?.hash ?? '', + host: currentLocation?.host ?? '', + hostname: currentLocation?.hostname ?? '', + href: currentLocation?.href ?? 'about:blank', + origin: currentLocation?.origin ?? '', + pathname: currentLocation?.pathname ?? '/', + port: currentLocation?.port ?? '', + protocol: currentLocation?.protocol ?? '', + search: currentLocation?.search ?? '', + }) as unknown as Readonly +} + function trackHistory(configuration: RumConfiguration, onHistoryChange: () => void) { const { stop: stopInstrumentingPushState } = instrumentMethod( getHistoryInstrumentationTarget('pushState'), diff --git a/packages/rum-core/src/domain/contexts/urlContexts.ts b/packages/rum-core/src/domain/contexts/urlContexts.ts index 530172a84c..976b325037 100644 --- a/packages/rum-core/src/domain/contexts/urlContexts.ts +++ b/packages/rum-core/src/domain/contexts/urlContexts.ts @@ -41,7 +41,7 @@ export function startUrlContexts( let previousViewUrl: string | undefined lifeCycle.subscribe(LifeCycleEventType.BEFORE_VIEW_CREATED, ({ startClocks, url }) => { - const locationHref = mockable(location).href + const locationHref = getLocationHref() const viewUrl = url !== undefined ? buildUrl(url, locationHref).href : locationHref urlContextHistory.add( buildUrlContext({ @@ -103,3 +103,8 @@ export function startUrlContexts( }, } } + +function getLocationHref() { + const href = mockable(() => window.location?.href)() + return typeof href === 'string' && href.trim() ? href : 'about:blank' +} diff --git a/packages/rum-core/src/domain/view/trackViews.ts b/packages/rum-core/src/domain/view/trackViews.ts index 3cbcb8d223..efe58a5531 100644 --- a/packages/rum-core/src/domain/view/trackViews.ts +++ b/packages/rum-core/src/domain/view/trackViews.ts @@ -113,7 +113,8 @@ export function trackViews( configuration: RumConfiguration, locationChangeObservable: Observable, areViewsTrackedAutomatically: boolean, - initialViewOptions?: ViewOptions + initialViewOptions?: ViewOptions, + collectViewMetrics: boolean = true ) { const activeViews: Set> = new Set() let currentView = startNewView(ViewLoadingType.INITIAL_LOAD, clocksOrigin(), initialViewOptions) @@ -141,7 +142,8 @@ export function trackViews( configuration, loadingType, startClocks, - viewOptions + viewOptions, + collectViewMetrics ) activeViews.add(newlyCreatedView) newlyCreatedView.stopObservable.subscribe(() => { @@ -215,7 +217,8 @@ function newView( configuration: RumConfiguration, loadingType: ViewLoadingType, startClocks: ClocksState = clocksNow(), - viewOptions?: ViewOptions + viewOptions?: ViewOptions, + collectViewMetrics: boolean = true ) { // Setup initial values const id = generateUUID() @@ -254,6 +257,7 @@ function newView( leading: false, }) + const emptyCommonViewMetrics: CommonViewMetrics = {} const { setLoadEvent, setViewEnd, @@ -261,23 +265,32 @@ function newView( stopINPTracking, getCommonViewMetrics, setLoadingTime, - } = trackCommonViewMetrics( - lifeCycle, - domMutationObservable, - windowOpenObservable, - configuration, - scheduleViewUpdate, - loadingType, - startClocks - ) + } = collectViewMetrics + ? trackCommonViewMetrics( + lifeCycle, + domMutationObservable, + windowOpenObservable, + configuration, + scheduleViewUpdate, + loadingType, + startClocks + ) + : { + setLoadEvent: noop, + setViewEnd: noop, + stop: noop, + stopINPTracking: noop, + getCommonViewMetrics: () => emptyCommonViewMetrics, + setLoadingTime: noop, + } const { stop: stopInitialViewMetricsTracking, initialViewMetrics } = - loadingType === ViewLoadingType.INITIAL_LOAD + collectViewMetrics && loadingType === ViewLoadingType.INITIAL_LOAD ? trackInitialViewMetrics(configuration, startClocks, setLoadEvent, scheduleViewUpdate) : { stop: noop, initialViewMetrics: {} as InitialViewMetrics } // Start BFCache-specific metrics when restoring from BFCache - if (loadingType === ViewLoadingType.BF_CACHE) { + if (collectViewMetrics && loadingType === ViewLoadingType.BF_CACHE) { trackBfcacheMetrics(startClocks, initialViewMetrics, scheduleViewUpdate) } diff --git a/packages/rum-core/src/domain/view/viewCollection.ts b/packages/rum-core/src/domain/view/viewCollection.ts index bda7502a28..fce3db14ff 100644 --- a/packages/rum-core/src/domain/view/viewCollection.ts +++ b/packages/rum-core/src/domain/view/viewCollection.ts @@ -25,7 +25,8 @@ export function startViewCollection( locationChangeObservable: Observable, recorderApi: RecorderApi, viewHistory: ViewHistory, - initialViewOptions?: ViewOptions + initialViewOptions?: ViewOptions, + collectViewMetrics: boolean = true ) { lifeCycle.subscribe(LifeCycleEventType.VIEW_UPDATED, (view) => lifeCycle.notify(LifeCycleEventType.RAW_RUM_EVENT_COLLECTED, processViewUpdate(view, configuration, recorderApi)) @@ -66,7 +67,8 @@ export function startViewCollection( configuration, locationChangeObservable, !configuration.trackViewsManually, - initialViewOptions + initialViewOptions, + collectViewMetrics ) } diff --git a/packages/rum-slim/README.md b/packages/rum-slim/README.md index 29067bf0b5..742e2d57e2 100644 --- a/packages/rum-slim/README.md +++ b/packages/rum-slim/README.md @@ -9,4 +9,16 @@ recording and the [`compressIntakeRequests`][1] initialization parameter. See the [RUM package](../rum/README.md) documentation. +## Salesforce bundle + +The Salesforce bundle is a dedicated `rum-slim` build for Lightning / Experience Cloud wrappers +that: + +- keeps the standard public API: `DD_RUM.init(...)` and `DD_RUM.startView(...)` +- forces manual view tracking +- disables request collection, runtime error collection, and view metrics by design + +The Salesforce wrapper is responsible for observing route changes and calling `startView()` with the +normalized pathname and current URL. + [1]: https://docs.datadoghq.com/real_user_monitoring/browser/setup/client/?tab=rum#initialization-parameters:~:text=compressIntakeRequests diff --git a/packages/rum-slim/package.json b/packages/rum-slim/package.json index 4811ac7daf..ec0e69e855 100644 --- a/packages/rum-slim/package.json +++ b/packages/rum-slim/package.json @@ -16,6 +16,7 @@ "scripts": { "build": "node ../../scripts/build/build-package.ts --modules --bundle datadog-rum-slim.js", "build:bundle": "node ../../scripts/build/build-package.ts --bundle datadog-rum-slim.js", + "build:salesforce": "node ../../scripts/build/build-package.ts --bundle datadog-rum-slim-salesforce.js --entry ./src/entries/salesforce.ts", "prepack": "yarn build" }, "dependencies": { diff --git a/packages/rum-slim/src/boot/salesforceInitConfiguration.spec.ts b/packages/rum-slim/src/boot/salesforceInitConfiguration.spec.ts new file mode 100644 index 0000000000..b1b5fe57d3 --- /dev/null +++ b/packages/rum-slim/src/boot/salesforceInitConfiguration.spec.ts @@ -0,0 +1,40 @@ +import type { RumInitConfiguration } from '@datadog/browser-rum-core' +import { buildSalesforceInitConfiguration } from './salesforceInitConfiguration' + +describe('salesforce init configuration', () => { + it('forces the slim salesforce bundle into manual view tracking mode', () => { + const initConfiguration = buildSalesforceInitConfiguration({ + applicationId: 'app-id', + clientToken: 'client-token', + trackViewsManually: false, + trackResources: true, + trackUserInteractions: true, + trackLongTasks: true, + } as RumInitConfiguration) + + expect(initConfiguration.trackViewsManually).toBeTrue() + expect(initConfiguration.trackResources).toBeFalse() + expect(initConfiguration.trackUserInteractions).toBeFalse() + expect(initConfiguration.trackLongTasks).toBeFalse() + }) + + it('preserves customer configuration unrelated to the salesforce view-tracking policy', () => { + const initConfiguration = buildSalesforceInitConfiguration({ + applicationId: 'app-id', + clientToken: 'client-token', + service: 'browser-sdk-sandbox', + env: 'dev', + site: 'datadoghq.com', + } as RumInitConfiguration) + + expect(initConfiguration).toEqual( + jasmine.objectContaining({ + applicationId: 'app-id', + clientToken: 'client-token', + service: 'browser-sdk-sandbox', + env: 'dev', + site: 'datadoghq.com', + }) + ) + }) +}) diff --git a/packages/rum-slim/src/boot/salesforceInitConfiguration.ts b/packages/rum-slim/src/boot/salesforceInitConfiguration.ts new file mode 100644 index 0000000000..a9bdd53de9 --- /dev/null +++ b/packages/rum-slim/src/boot/salesforceInitConfiguration.ts @@ -0,0 +1,18 @@ +import type { RumInitConfiguration } from '@datadog/browser-rum-core' + +const SALESFORCE_VIEW_TRACKING_DEFAULTS: Pick< + RumInitConfiguration, + 'trackViewsManually' | 'trackResources' | 'trackUserInteractions' | 'trackLongTasks' +> = { + trackViewsManually: true, + trackResources: false, + trackUserInteractions: false, + trackLongTasks: false, +} + +export function buildSalesforceInitConfiguration(initConfiguration: RumInitConfiguration): RumInitConfiguration { + return { + ...initConfiguration, + ...SALESFORCE_VIEW_TRACKING_DEFAULTS, + } +} diff --git a/packages/rum-slim/src/entries/salesforce.ts b/packages/rum-slim/src/entries/salesforce.ts new file mode 100644 index 0000000000..e315428b58 --- /dev/null +++ b/packages/rum-slim/src/entries/salesforce.ts @@ -0,0 +1,81 @@ +import { defineGlobal, getGlobalObject } from '@datadog/browser-core' +import type { RumPublicApi, RumInitConfiguration } from '@datadog/browser-rum-core' +import { makeRumPublicApi } from '@datadog/browser-rum-core' +import { makeRecorderApiStub } from '../boot/stubRecorderApi' +import { makeProfilerApiStub } from '../boot/stubProfilerApi' +import { buildSalesforceInitConfiguration } from '../boot/salesforceInitConfiguration' + +export type { + User, + Account, + TraceContextInjection, + SessionPersistence, + TrackingConsent, + MatchOption, + ProxyFn, + Site, + Context, + ContextValue, + ContextArray, + RumInternalContext, +} from '@datadog/browser-core' + +export type { + RumPublicApi as DatadogRum, + RumInitConfiguration, + RumBeforeSend, + ViewOptions, + StartRecordingOptions, + AddDurationVitalOptions, + DurationVitalOptions, + DurationVitalReference, + TracingOption, + RumPlugin, + OnRumStartOptions, + PropagatorType, + FeatureFlagsForEvents, + MatchHeader, + CommonProperties, + RumEvent, + RumActionEvent, + RumErrorEvent, + RumLongTaskEvent, + RumResourceEvent, + RumViewEvent, + RumVitalEvent, + RumEventDomainContext, + RumViewEventDomainContext, + RumErrorEventDomainContext, + RumActionEventDomainContext, + RumVitalEventDomainContext, + RumFetchResourceEventDomainContext, + RumXhrResourceEventDomainContext, + RumOtherResourceEventDomainContext, + RumLongTaskEventDomainContext, +} from '@datadog/browser-rum-core' +export { DEFAULT_TRACKED_RESOURCE_HEADERS } from '@datadog/browser-rum-core' +export { DefaultPrivacyLevel } from '@datadog/browser-core' + +// eslint-disable-next-line local-rules/disallow-side-effects +const baseRum = makeRumPublicApi(makeRecorderApiStub(), makeProfilerApiStub(), { + sdkName: 'rum-slim', + runtimeCapabilities: { + requestCollection: false, + runtimeErrors: false, + viewMetrics: false, + }, +}) + +export const datadogRum: RumPublicApi = { + ...baseRum, + init(initConfiguration: RumInitConfiguration) { + baseRum.init(buildSalesforceInitConfiguration(initConfiguration)) + }, +} + +interface BrowserWindow extends Window { + DD_RUM?: RumPublicApi +} + +// eslint-disable-next-line local-rules/disallow-side-effects +defineGlobal(getGlobalObject(), 'DD_RUM', datadogRum) diff --git a/rum-events-format b/rum-events-format index 4f4ab2c504..5a80fb9a3c 160000 --- a/rum-events-format +++ b/rum-events-format @@ -1 +1 @@ -Subproject commit 4f4ab2c50456d688ae228904ced4c68aa382ae4c +Subproject commit 5a80fb9a3c054b28fba195fd301cadb094bccef8 diff --git a/scripts/build/build-package.ts b/scripts/build/build-package.ts index a60b5ff78e..3fe681e68d 100644 --- a/scripts/build/build-package.ts +++ b/scripts/build/build-package.ts @@ -19,6 +19,9 @@ runMain(async () => { bundle: { type: 'string', }, + entry: { + type: 'string', + }, verbose: { type: 'boolean', default: false, @@ -43,6 +46,7 @@ runMain(async () => { if (values.bundle) { printLog('Building bundle...') await buildBundle({ + entry: values.entry ?? './src/entries/main.ts', filename: values.bundle, verbose: values.verbose, }) @@ -51,13 +55,13 @@ runMain(async () => { printLog('Done.') }) -async function buildBundle({ filename, verbose }: { filename: string; verbose: boolean }) { +async function buildBundle({ entry, filename, verbose }: { entry: string; filename: string; verbose: boolean }) { await fs.rm('./bundle', { recursive: true, force: true }) return new Promise((resolve, reject) => { webpack( webpackBase({ mode: 'production', - entry: './src/entries/main.ts', + entry, filename, }), (error, stats) => { diff --git a/test/e2e/lib/helpers/playwright.ts b/test/e2e/lib/helpers/playwright.ts index ef1f2344ae..0437e98813 100644 --- a/test/e2e/lib/helpers/playwright.ts +++ b/test/e2e/lib/helpers/playwright.ts @@ -21,11 +21,18 @@ export function getPlaywrightConfigBrowserName(name: string): PlaywrightWorkerOp } export function getEncodedCapabilities(configuration: BrowserConfiguration) { - return encodeURIComponent(JSON.stringify(getCapabilities(configuration))) + return getEncodedCapabilitiesWithOptions(configuration) +} + +export function getEncodedCapabilitiesWithOptions( + configuration: BrowserConfiguration, + { localTesting = true }: { localTesting?: boolean } = {} +) { + return encodeURIComponent(JSON.stringify(getCapabilities(configuration, { localTesting }))) } // see: https://www.browserstack.com/docs/automate/playwright/playwright-capabilities -function getCapabilities(configuration: BrowserConfiguration) { +function getCapabilities(configuration: BrowserConfiguration, { localTesting }: { localTesting: boolean }) { const playwrightVersion = resolvePlaywrightVersionFromPackageJson() return { os: configuration.os, @@ -37,7 +44,7 @@ function getCapabilities(configuration: BrowserConfiguration) { project: 'browser sdk e2e', build: getBuildInfos(), name: configuration.sessionName, - 'browserstack.local': true, + 'browserstack.local': localTesting, 'browserstack.playwrightVersion': playwrightVersion, 'client.playwrightVersion': playwrightVersion, 'browserstack.debug': false, diff --git a/test/e2e/lib/salesforce.ts b/test/e2e/lib/salesforce.ts new file mode 100644 index 0000000000..e994db037c --- /dev/null +++ b/test/e2e/lib/salesforce.ts @@ -0,0 +1,98 @@ +import { execFileSync } from 'node:child_process' +import { URL } from 'node:url' +import { expect, type Page, type Request } from '@playwright/test' + +export interface RumViewEventPayload { + type: 'view' + view: { + name?: string + url?: string + } +} + +export function normalizePathname(pathname: string) { + if (!pathname.trim()) { + return '/' + } + + let normalizedPathname = pathname.trim() + if (!normalizedPathname.startsWith('/')) { + normalizedPathname = `/${normalizedPathname}` + } + if (normalizedPathname.length > 1) { + normalizedPathname = normalizedPathname.replace(/\/+$/, '') + } + return normalizedPathname || '/' +} + +export function createRumViewTracker(page: Page) { + const viewEvents: RumViewEventPayload[] = [] + + page.on('request', (request) => { + if (!isRumIntakeRequest(request)) { + return + } + + for (const event of parseRumPayload(request)) { + if (event.type === 'view') { + viewEvents.push(event as RumViewEventPayload) + } + } + }) + + return { + viewEvents, + async waitForViewCount(expectedCount: number) { + await expect + .poll( + () => viewEvents.length, + { + timeout: 30_000, + message: `Expected at least ${expectedCount} RUM view events from Salesforce navigation`, + } + ) + .toBeGreaterThanOrEqual(expectedCount) + }, + } +} + +export async function flushRumEvents(page: Page) { + await page.goto('about:blank') +} + +export async function openLightningWithSf(page: Page, orgAlias: string, path: string) { + const rawResponse = execFileSync('sf', ['org', 'open', '-o', orgAlias, '-p', path, '-r', '--json'], { + encoding: 'utf8', + }) + const response = JSON.parse(rawResponse) as { result?: { url?: string } } + const url = response.result?.url + + if (!url) { + throw new Error(`Unable to open Salesforce org '${orgAlias}' with a one-time login URL.`) + } + + await page.goto(url) +} + +export function getExpectedViewUrl(baseUrl: string, pathname: string) { + return new URL(pathname, `${baseUrl.replace(/\/+$/, '')}/`).href +} + +function isRumIntakeRequest(request: Request) { + const url = request.url() + return request.method() === 'POST' && /\/api\/v2\/rum(?:[/?]|$)/.test(url) +} + +function parseRumPayload(request: Request) { + const bodyBuffer = request.postDataBuffer() + if (!bodyBuffer?.length) { + return [] + } + + return bodyBuffer + .toString('utf8') + .split('\n') + .map((line) => line.trim()) + .filter(Boolean) + .map((line) => JSON.parse(line) as { type?: string }) +} diff --git a/test/e2e/playwright.salesforce.base.config.ts b/test/e2e/playwright.salesforce.base.config.ts new file mode 100644 index 0000000000..1f76790cc7 --- /dev/null +++ b/test/e2e/playwright.salesforce.base.config.ts @@ -0,0 +1,11 @@ +import { defineConfig } from '@playwright/test' +import { config as baseConfig } from './playwright.base.config' + +// eslint-disable-next-line import/no-default-export +export default defineConfig({ + ...baseConfig, + testDir: './scenario/salesforce', + webServer: undefined, + fullyParallel: false, + workers: 1, +}) diff --git a/test/e2e/playwright.salesforce.bs.config.ts b/test/e2e/playwright.salesforce.bs.config.ts new file mode 100644 index 0000000000..252c28237b --- /dev/null +++ b/test/e2e/playwright.salesforce.bs.config.ts @@ -0,0 +1,22 @@ +import { defineConfig } from '@playwright/test' +import { browserConfigurations } from './browsers.conf' +import { getEncodedCapabilitiesWithOptions, getPlaywrightConfigBrowserName } from './lib/helpers/playwright' +import baseConfig from './playwright.salesforce.base.config' + +// eslint-disable-next-line import/no-default-export +export default defineConfig({ + ...baseConfig, + workers: 5, + projects: browserConfigurations.map((configuration) => ({ + name: configuration.sessionName, + metadata: configuration, + use: { + browserName: getPlaywrightConfigBrowserName(configuration.name), + connectOptions: { + wsEndpoint: `wss://cdp.browserstack.com/playwright?caps=${getEncodedCapabilitiesWithOptions(configuration, { + localTesting: false, + })}`, + }, + }, + })), +}) diff --git a/test/e2e/playwright.salesforce.local.config.ts b/test/e2e/playwright.salesforce.local.config.ts new file mode 100644 index 0000000000..11a9386c28 --- /dev/null +++ b/test/e2e/playwright.salesforce.local.config.ts @@ -0,0 +1,17 @@ +import { defineConfig, devices } from '@playwright/test' +import baseConfig from './playwright.salesforce.base.config' + +// eslint-disable-next-line import/no-default-export +export default defineConfig({ + ...baseConfig, + projects: [ + { + name: 'chromium', + metadata: { + sessionName: 'Desktop Chrome', + name: 'chromium', + }, + use: devices['Desktop Chrome'], + }, + ], +}) diff --git a/test/e2e/scenario/salesforce/README.md b/test/e2e/scenario/salesforce/README.md new file mode 100644 index 0000000000..ddf4fb8ace --- /dev/null +++ b/test/e2e/scenario/salesforce/README.md @@ -0,0 +1,9 @@ +# Salesforce E2E + +This suite validates Salesforce view tracking against a deployed `ebikes` org by intercepting real +RUM intake requests in Playwright. + +Required environment variables: + +- `SALESFORCE_EBIKES_SITE_URL` +- `SALESFORCE_EBIKES_ORG_ALIAS` diff --git a/test/e2e/scenario/salesforce/views.scenario.ts b/test/e2e/scenario/salesforce/views.scenario.ts new file mode 100644 index 0000000000..cbc12da1fa --- /dev/null +++ b/test/e2e/scenario/salesforce/views.scenario.ts @@ -0,0 +1,101 @@ +import { test, expect } from '@playwright/test' +import { + createRumViewTracker, + flushRumEvents, + getExpectedViewUrl, + normalizePathname, + openLightningWithSf, +} from '../../lib/salesforce' + +const SALESFORCE_EBIKES_SITE_URL = process.env.SALESFORCE_EBIKES_SITE_URL +const SALESFORCE_EBIKES_ORG_ALIAS = process.env.SALESFORCE_EBIKES_ORG_ALIAS + +test.describe('salesforce view tracking', () => { + test('emits views while navigating in Experience Cloud', async ({ page }) => { + test.skip(!SALESFORCE_EBIKES_SITE_URL, 'Set SALESFORCE_EBIKES_SITE_URL to run Salesforce Experience tests.') + + const baseUrl = SALESFORCE_EBIKES_SITE_URL! + const viewTracker = createRumViewTracker(page) + const baseHomePath = normalizePathname(new URL(baseUrl).pathname) + + await page.goto(baseUrl) + await page.getByRole('link', { name: /^Product Explorer$/i }).click() + await page.waitForURL('**/product-explorer') + + await page.getByRole('link', { name: /FUSE X1/i }).click() + await page.waitForURL('**/product/**') + + await page.getByRole('link', { name: /^Home$/i }).click() + await page.waitForURL((url) => normalizePathname(url.pathname) === baseHomePath) + + await flushRumEvents(page) + await viewTracker.waitForViewCount(4) + + const productExplorerPath = `${baseHomePath}/product-explorer`.replace(/\/+/g, '/') + const productDetailView = viewTracker.viewEvents.find((event) => event.view.name?.includes('/product/')) + + expect(viewTracker.viewEvents).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + view: expect.objectContaining({ + name: baseHomePath, + url: getExpectedViewUrl(baseUrl, baseHomePath), + }), + }), + expect.objectContaining({ + view: expect.objectContaining({ + name: productExplorerPath, + url: getExpectedViewUrl(baseUrl, productExplorerPath), + }), + }), + ]) + ) + expect(productDetailView).toEqual( + expect.objectContaining({ + view: expect.objectContaining({ + url: expect.stringContaining('/product/'), + }), + }) + ) + }) + + test('emits views while navigating in Lightning Experience', async ({ page }) => { + test.skip(!SALESFORCE_EBIKES_ORG_ALIAS, 'Set SALESFORCE_EBIKES_ORG_ALIAS to run Salesforce Lightning tests.') + + const viewTracker = createRumViewTracker(page) + await openLightningWithSf(page, SALESFORCE_EBIKES_ORG_ALIAS!, '/lightning/n/Product_Explorer') + await page.waitForURL('**/lightning/n/Product_Explorer') + + await page.getByRole('link', { name: /FUSE X1/i }).click() + await page.waitForURL('**/lightning/r/**/view') + + const recordPath = normalizePathname(new URL(page.url()).pathname) + + await page.goto(new URL('/lightning/n/Product_Explorer', page.url()).href) + await page.waitForURL('**/lightning/n/Product_Explorer') + + await flushRumEvents(page) + await viewTracker.waitForViewCount(3) + + expect(viewTracker.viewEvents).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + view: expect.objectContaining({ + name: '/lightning/n/Product_Explorer', + url: expect.stringContaining('/lightning/n/Product_Explorer'), + }), + }), + expect.objectContaining({ + view: expect.objectContaining({ + name: recordPath, + url: expect.stringContaining(recordPath), + }), + }), + ]) + ) + + expect( + viewTracker.viewEvents.filter((event) => event.view.name === '/lightning/n/Product_Explorer').length + ).toBeGreaterThanOrEqual(2) + }) +}) From 36c6bfb19a5e0c28b50abd9cb9ac324237b8e090 Mon Sep 17 00:00:00 2001 From: "beltran.bulbarella" Date: Wed, 22 Apr 2026 14:01:47 +0200 Subject: [PATCH 02/16] Remove view tracking and related code from Salesforce POC --- package.json | 2 - packages/core/src/browser/addEventListener.ts | 25 +---- packages/core/src/tools/globalObject.ts | 14 +-- packages/core/src/tools/instrumentMethod.ts | 41 +------ packages/core/src/tools/utils/urlPolyfill.ts | 7 +- packages/rum-core/src/boot/preStartRum.ts | 10 +- .../rum-core/src/boot/rumPublicApi.spec.ts | 22 ---- packages/rum-core/src/boot/rumPublicApi.ts | 13 +-- .../rum-core/src/boot/runtimeCapabilities.ts | 26 ----- packages/rum-core/src/boot/startRum.ts | 29 ++--- .../rum-core/src/browser/cookieObservable.ts | 24 ----- .../src/browser/locationChangeObservable.ts | 26 +---- .../src/domain/contexts/urlContexts.ts | 7 +- .../rum-core/src/domain/view/trackViews.ts | 41 +++---- .../src/domain/view/viewCollection.ts | 6 +- packages/rum-slim/src/entries/salesforce.ts | 5 - test/e2e/lib/helpers/playwright.ts | 13 +-- test/e2e/lib/salesforce.ts | 98 ----------------- test/e2e/playwright.salesforce.base.config.ts | 11 -- test/e2e/playwright.salesforce.bs.config.ts | 22 ---- .../e2e/playwright.salesforce.local.config.ts | 17 --- test/e2e/scenario/salesforce/README.md | 9 -- .../e2e/scenario/salesforce/views.scenario.ts | 101 ------------------ 23 files changed, 50 insertions(+), 519 deletions(-) delete mode 100644 packages/rum-core/src/boot/runtimeCapabilities.ts delete mode 100644 test/e2e/lib/salesforce.ts delete mode 100644 test/e2e/playwright.salesforce.base.config.ts delete mode 100644 test/e2e/playwright.salesforce.bs.config.ts delete mode 100644 test/e2e/playwright.salesforce.local.config.ts delete mode 100644 test/e2e/scenario/salesforce/README.md delete mode 100644 test/e2e/scenario/salesforce/views.scenario.ts diff --git a/package.json b/package.json index 74e65b0eee..52d0d958ff 100644 --- a/package.json +++ b/package.json @@ -32,9 +32,7 @@ "test:unit:bs": "node --env-file-if-exists=.env ./scripts/test/bs-wrapper.ts karma start test/unit/karma.bs.conf.js", "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:salesforce": "playwright test --config test/e2e/playwright.salesforce.local.config.ts", "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:bs": "node --env-file-if-exists=.env ./scripts/test/bs-wrapper.ts playwright test --config test/e2e/playwright.salesforce.bs.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", diff --git a/packages/core/src/browser/addEventListener.ts b/packages/core/src/browser/addEventListener.ts index 7af92a4b05..6fb2c43ba5 100644 --- a/packages/core/src/browser/addEventListener.ts +++ b/packages/core/src/browser/addEventListener.ts @@ -1,6 +1,5 @@ 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 & { __ddIsTrusted?: boolean } @@ -133,31 +132,11 @@ export function addEventListeners add.call(eventTarget, eventName, listenerWithMonitor, options)) - } catch { - return { - stop: noop, - } - } + eventNames.forEach((eventName) => add.call(eventTarget, eventName, listenerWithMonitor, options)) function stop() { const remove = getZoneJsOriginalValue(listenerTarget, 'removeEventListener') - if (typeof remove !== 'function') { - return - } - try { - eventNames.forEach((eventName) => remove.call(eventTarget, eventName, listenerWithMonitor, options)) - } catch { - // Ignore cleanup errors on runtimes exposing partial EventTarget implementations. - } + eventNames.forEach((eventName) => remove.call(eventTarget, eventName, listenerWithMonitor, options)) } return { diff --git a/packages/core/src/tools/globalObject.ts b/packages/core/src/tools/globalObject.ts index 2723ea6bbd..ce9b637c02 100644 --- a/packages/core/src/tools/globalObject.ts +++ b/packages/core/src/tools/globalObject.ts @@ -17,12 +17,6 @@ export function getGlobalObject(): T { if (typeof globalThis === 'object') { return globalThis as unknown as T } - if (typeof self === 'object') { - return self as unknown as T - } - if (typeof window === 'object') { - return window as unknown as T - } Object.defineProperty(Object.prototype, '_dd_temp_', { get() { return this as object @@ -36,7 +30,13 @@ export function getGlobalObject(): T { if (typeof globalObject !== 'object') { // on safari _dd_temp_ is available on window but not globally // fallback on other browser globals check - globalObject = {} + if (typeof self === 'object') { + globalObject = self + } else if (typeof window === 'object') { + globalObject = window + } else { + globalObject = {} + } } return globalObject as T } diff --git a/packages/core/src/tools/instrumentMethod.ts b/packages/core/src/tools/instrumentMethod.ts index 2339dde2a2..5d5a398824 100644 --- a/packages/core/src/tools/instrumentMethod.ts +++ b/packages/core/src/tools/instrumentMethod.ts @@ -74,7 +74,6 @@ export function instrumentMethod { @@ -126,45 +117,19 @@ export function instrumentMethod { stopped = true // If the instrumentation has been removed by a third party, keep the last one - try { - if (targetPrototype[method] === instrumentation) { - targetPrototype[method] = original - } - } catch { - // Some runtimes expose instrumentable methods as readonly. In that case, the - // instrumentation is already inert thanks to the `stopped` flag above. + if (targetPrototype[method] === instrumentation) { + targetPrototype[method] = original } }, } } -function findPropertyDescriptor( - target: TARGET, - method: METHOD -): PropertyDescriptor | undefined { - let currentTarget: object | null = target - - while (currentTarget) { - const descriptor = Object.getOwnPropertyDescriptor(currentTarget, method) - if (descriptor) { - return descriptor - } - currentTarget = Object.getPrototypeOf(currentTarget) - } - - return undefined -} - export function instrumentSetter( targetPrototype: TARGET, property: PROPERTY, diff --git a/packages/core/src/tools/utils/urlPolyfill.ts b/packages/core/src/tools/utils/urlPolyfill.ts index 7eea9a51cf..812b407bb7 100644 --- a/packages/core/src/tools/utils/urlPolyfill.ts +++ b/packages/core/src/tools/utils/urlPolyfill.ts @@ -1,7 +1,7 @@ import { globalObject } from '../globalObject' export function normalizeUrl(url: string) { - return buildUrl(url, getLocationHref()).href + return buildUrl(url, location.href).href } export function isValidUrl(url: string) { @@ -27,11 +27,6 @@ export function buildUrl(url: string, base?: string) { } } -function getLocationHref() { - const href = (globalObject as Window & typeof globalThis).location?.href - return typeof href === 'string' && href.trim() ? href : undefined -} - /** * Get native URL constructor from a clean iframe * This avoids polyfill issues by getting the native implementation from a fresh iframe context diff --git a/packages/rum-core/src/boot/preStartRum.ts b/packages/rum-core/src/boot/preStartRum.ts index 7e1962505c..58cf5ad3e4 100644 --- a/packages/rum-core/src/boot/preStartRum.ts +++ b/packages/rum-core/src/boot/preStartRum.ts @@ -59,11 +59,7 @@ export type DoStartRum = ( ) => StartRumResult export function createPreStartStrategy( - { - ignoreInitIfSyntheticsWillInjectRum = true, - startDeflateWorker, - runtimeCapabilities, - }: RumPublicApiOptions, + { ignoreInitIfSyntheticsWillInjectRum = true, startDeflateWorker }: RumPublicApiOptions, trackingConsentState: TrackingConsentState, customVitalsState: CustomVitalsState, doStartRum: DoStartRum @@ -173,9 +169,7 @@ export function createPreStartStrategy( // This is needed in case the consent is not granted and some customer // library (Apollo Client) is storing uninstrumented fetch to be used later // The subscription is needed so that the instrumentation process is completed - if (runtimeCapabilities?.requestCollection !== false) { - initFetchObservable().subscribe(noop) - } + initFetchObservable().subscribe(noop) trackingConsentState.tryToInit(configuration.trackingConsent) tryStartRum() diff --git a/packages/rum-core/src/boot/rumPublicApi.spec.ts b/packages/rum-core/src/boot/rumPublicApi.spec.ts index 8f2eef5e4d..e9bbccc102 100644 --- a/packages/rum-core/src/boot/rumPublicApi.spec.ts +++ b/packages/rum-core/src/boot/rumPublicApi.spec.ts @@ -1134,28 +1134,6 @@ describe('rum public api', () => { expect(sdkName).toBe('rum-slim') }) }) - - describe('runtime capabilities', () => { - it('passes them down to startRum', () => { - const { rumPublicApi, startRumSpy } = makeRumPublicApiWithDefaults({ - rumPublicApiOptions: { - runtimeCapabilities: { - requestCollection: false, - runtimeErrors: false, - viewMetrics: false, - }, - }, - }) - - rumPublicApi.init(DEFAULT_INIT_CONFIGURATION) - - expect(startRumSpy.calls.argsFor(0)[11]).toEqual({ - requestCollection: false, - runtimeErrors: false, - viewMetrics: false, - }) - }) - }) }) function makeRumPublicApiWithDefaults({ diff --git a/packages/rum-core/src/boot/rumPublicApi.ts b/packages/rum-core/src/boot/rumPublicApi.ts index ccc3c20dc9..a3fd82649a 100644 --- a/packages/rum-core/src/boot/rumPublicApi.ts +++ b/packages/rum-core/src/boot/rumPublicApi.ts @@ -15,7 +15,6 @@ import type { Telemetry, Encoder, ResourceType, - BufferedData, } from '@datadog/browser-core' import { ContextManagerMethod, @@ -39,7 +38,6 @@ import { mockable, generateUUID, timeStampNow, - BufferedObservable, } from '@datadog/browser-core' import type { LifeCycle } from '../domain/lifeCycle' @@ -65,8 +63,6 @@ import type { ResourceOptions, ResourceStopOptions } from '../domain/resource/tr import { createPreStartStrategy } from './preStartRum' import type { StartRumResult } from './startRum' import { startRum } from './startRum' -import type { RumRuntimeCapabilities } from './runtimeCapabilities' -import { resolveRumRuntimeCapabilities } from './runtimeCapabilities' export interface StartRecordingOptions { force: boolean @@ -560,7 +556,6 @@ export interface RumPublicApiOptions { streamId: DeflateEncoderStreamId ) => DeflateEncoder sdkName?: SdkName - runtimeCapabilities?: RumRuntimeCapabilities } export interface Strategy { @@ -601,10 +596,7 @@ export function makeRumPublicApi( ): RumPublicApi { const trackingConsentState = createTrackingConsentState() const customVitalsState = createCustomVitalsState() - const runtimeCapabilities = resolveRumRuntimeCapabilities(options.runtimeCapabilities) - const bufferedDataObservable = runtimeCapabilities.runtimeErrors - ? startBufferingData().observable - : new BufferedObservable(0) + const bufferedDataObservable = startBufferingData().observable let strategy = createPreStartStrategy( options, @@ -627,8 +619,7 @@ export function makeRumPublicApi( bufferedDataObservable, telemetry, hooks, - options.sdkName, - runtimeCapabilities + options.sdkName ) recorderApi.onRumStart( diff --git a/packages/rum-core/src/boot/runtimeCapabilities.ts b/packages/rum-core/src/boot/runtimeCapabilities.ts deleted file mode 100644 index 52ceac345f..0000000000 --- a/packages/rum-core/src/boot/runtimeCapabilities.ts +++ /dev/null @@ -1,26 +0,0 @@ -export interface RumRuntimeCapabilities { - requestCollection?: boolean - runtimeErrors?: boolean - viewMetrics?: boolean -} - -export interface ResolvedRumRuntimeCapabilities { - requestCollection: boolean - runtimeErrors: boolean - viewMetrics: boolean -} - -export const DEFAULT_RUM_RUNTIME_CAPABILITIES: ResolvedRumRuntimeCapabilities = { - requestCollection: true, - runtimeErrors: true, - viewMetrics: true, -} - -export function resolveRumRuntimeCapabilities( - runtimeCapabilities?: RumRuntimeCapabilities -): ResolvedRumRuntimeCapabilities { - return { - ...DEFAULT_RUM_RUNTIME_CAPABILITIES, - ...runtimeCapabilities, - } -} diff --git a/packages/rum-core/src/boot/startRum.ts b/packages/rum-core/src/boot/startRum.ts index ab4119dca4..4dfd44e233 100644 --- a/packages/rum-core/src/boot/startRum.ts +++ b/packages/rum-core/src/boot/startRum.ts @@ -17,7 +17,6 @@ import { startGlobalContext, startUserContext, startTabContext, - noop, } from '@datadog/browser-core' import { createDOMMutationObservable } from '../browser/domMutationObservable' import { createWindowOpenObservable } from '../browser/windowOpenObservable' @@ -26,7 +25,7 @@ import { LifeCycle, LifeCycleEventType } from '../domain/lifeCycle' import { startViewHistory } from '../domain/contexts/viewHistory' import { startRequestCollection } from '../domain/requestCollection' import { startActionCollection } from '../domain/action/actionCollection' -import { doStartErrorCollection, startErrorCollection } from '../domain/error/errorCollection' +import { startErrorCollection } from '../domain/error/errorCollection' import { startResourceCollection } from '../domain/resource/resourceCollection' import { startViewCollection } from '../domain/view/viewCollection' import type { RumSessionManager } from '../domain/rumSessionManager' @@ -57,8 +56,6 @@ import { startEventCollection } from '../domain/event/eventCollection' import { startInitialViewMetricsTelemetry } from '../domain/view/viewMetrics/startInitialViewMetricsTelemetry' import { startSourceCodeContext } from '../domain/contexts/sourceCodeContext' import type { RecorderApi, ProfilerApi } from './rumPublicApi' -import type { ResolvedRumRuntimeCapabilities } from './runtimeCapabilities' -import { DEFAULT_RUM_RUNTIME_CAPABILITIES } from './runtimeCapabilities' export type StartRum = typeof startRum export type StartRumResult = ReturnType @@ -78,8 +75,7 @@ export function startRum( bufferedDataObservable: BufferedObservable, telemetry: Telemetry, hooks: Hooks, - sdkName?: SdkName, - runtimeCapabilities: ResolvedRumRuntimeCapabilities = DEFAULT_RUM_RUNTIME_CAPABILITIES + sdkName?: SdkName ) { const cleanupTasks: Array<() => void> = [] const lifeCycle = new LifeCycle() @@ -123,9 +119,7 @@ export function startRum( startTrackingConsentContext(hooks, trackingConsentState) - const { stop: stopInitialViewMetricsTelemetry } = runtimeCapabilities.viewMetrics - ? startInitialViewMetricsTelemetry(lifeCycle, telemetry) - : { stop: noop } + const { stop: stopInitialViewMetricsTelemetry } = startInitialViewMetricsTelemetry(lifeCycle, telemetry) cleanupTasks.push(stopInitialViewMetricsTelemetry) const { stop: stopRumEventCollection, ...startRumEventCollectionResult } = startRumEventCollection( @@ -138,8 +132,7 @@ export function startRum( customVitalsState, bufferedDataObservable, sdkName, - reportError, - runtimeCapabilities + reportError ) cleanupTasks.push(stopRumEventCollection) bufferedDataObservable.unbuffer() @@ -170,8 +163,7 @@ export function startRumEventCollection( customVitalsState: CustomVitalsState, bufferedDataObservable: Observable, sdkName: SdkName | undefined, - reportError: (error: RawError) => void, - runtimeCapabilities: ResolvedRumRuntimeCapabilities = DEFAULT_RUM_RUNTIME_CAPABILITIES + reportError: (error: RawError) => void ) { const cleanupTasks: Array<() => void> = [] @@ -232,8 +224,7 @@ export function startRumEventCollection( locationChangeObservable, recorderApi, viewHistory, - initialViewOptions, - runtimeCapabilities.viewMetrics + initialViewOptions ) startSourceCodeContext(hooks) @@ -246,13 +237,9 @@ export function startRumEventCollection( const { stop: stopLongTaskCollection } = startLongTaskCollection(lifeCycle, configuration) cleanupTasks.push(stopLongTaskCollection) - const { addError } = runtimeCapabilities.runtimeErrors - ? startErrorCollection(lifeCycle, configuration, bufferedDataObservable) - : doStartErrorCollection(lifeCycle) + const { addError } = startErrorCollection(lifeCycle, configuration, bufferedDataObservable) - if (runtimeCapabilities.requestCollection) { - startRequestCollection(lifeCycle, configuration, session, userContext, accountContext) - } + startRequestCollection(lifeCycle, configuration, session, userContext, accountContext) const vitalCollection = startVitalCollection(lifeCycle, pageStateHistory, customVitalsState) diff --git a/packages/rum-core/src/browser/cookieObservable.ts b/packages/rum-core/src/browser/cookieObservable.ts index 6285d1cded..3bb3080d19 100644 --- a/packages/rum-core/src/browser/cookieObservable.ts +++ b/packages/rum-core/src/browser/cookieObservable.ts @@ -7,7 +7,6 @@ import { ONE_SECOND, findCommaSeparatedValue, DOM_EVENT, - getZoneJsOriginalValue, } from '@datadog/browser-core' export interface CookieStoreWindow { @@ -28,10 +27,6 @@ export function createCookieObservable(configuration: Configuration, cookieName: function listenToCookieStoreChange(configuration: Configuration) { return (cookieName: string, callback: (event: string | undefined) => void) => { - if (!canListenToCookieStoreChanges()) { - return watchCookieFallback(cookieName, callback) - } - const listener = addEventListener( configuration, (window as CookieStoreWindow).cookieStore!, @@ -51,25 +46,6 @@ function listenToCookieStoreChange(configuration: Configuration) { } } -function canListenToCookieStoreChanges() { - const cookieStore = (window as CookieStoreWindow).cookieStore - const add = cookieStore && getZoneJsOriginalValue(cookieStore, 'addEventListener') - const remove = cookieStore && getZoneJsOriginalValue(cookieStore, 'removeEventListener') - - if (typeof add !== 'function' || typeof remove !== 'function') { - return false - } - - const noopListener = () => undefined - try { - add.call(cookieStore, DOM_EVENT.CHANGE, noopListener) - remove.call(cookieStore, DOM_EVENT.CHANGE, noopListener) - return true - } catch { - return false - } -} - export const WATCH_COOKIE_INTERVAL_DELAY = ONE_SECOND function watchCookieFallback(cookieName: string, callback: (event: string | undefined) => void) { diff --git a/packages/rum-core/src/browser/locationChangeObservable.ts b/packages/rum-core/src/browser/locationChangeObservable.ts index 1431270fdc..fefa7d6ee0 100644 --- a/packages/rum-core/src/browser/locationChangeObservable.ts +++ b/packages/rum-core/src/browser/locationChangeObservable.ts @@ -7,22 +7,22 @@ export interface LocationChange { } export function createLocationChangeObservable(configuration: RumConfiguration) { - let currentLocation = getLocationSnapshot() + let currentLocation = shallowClone(location) return new Observable((observable) => { const { stop: stopHistoryTracking } = trackHistory(configuration, onLocationChange) const { stop: stopHashTracking } = trackHash(configuration, onLocationChange) function onLocationChange() { - const nextLocation = getLocationSnapshot() - if (currentLocation.href === nextLocation.href) { + if (currentLocation.href === location.href) { return } + const newLocation = shallowClone(location) observable.notify({ - newLocation: nextLocation, + newLocation, oldLocation: currentLocation, }) - currentLocation = nextLocation + currentLocation = newLocation } return () => { @@ -32,22 +32,6 @@ export function createLocationChangeObservable(configuration: RumConfiguration) }) } -function getLocationSnapshot(): Readonly { - const currentLocation = window.location - return shallowClone({ - ancestorOrigins: currentLocation?.ancestorOrigins, - hash: currentLocation?.hash ?? '', - host: currentLocation?.host ?? '', - hostname: currentLocation?.hostname ?? '', - href: currentLocation?.href ?? 'about:blank', - origin: currentLocation?.origin ?? '', - pathname: currentLocation?.pathname ?? '/', - port: currentLocation?.port ?? '', - protocol: currentLocation?.protocol ?? '', - search: currentLocation?.search ?? '', - }) as unknown as Readonly -} - function trackHistory(configuration: RumConfiguration, onHistoryChange: () => void) { const { stop: stopInstrumentingPushState } = instrumentMethod( getHistoryInstrumentationTarget('pushState'), diff --git a/packages/rum-core/src/domain/contexts/urlContexts.ts b/packages/rum-core/src/domain/contexts/urlContexts.ts index 976b325037..530172a84c 100644 --- a/packages/rum-core/src/domain/contexts/urlContexts.ts +++ b/packages/rum-core/src/domain/contexts/urlContexts.ts @@ -41,7 +41,7 @@ export function startUrlContexts( let previousViewUrl: string | undefined lifeCycle.subscribe(LifeCycleEventType.BEFORE_VIEW_CREATED, ({ startClocks, url }) => { - const locationHref = getLocationHref() + const locationHref = mockable(location).href const viewUrl = url !== undefined ? buildUrl(url, locationHref).href : locationHref urlContextHistory.add( buildUrlContext({ @@ -103,8 +103,3 @@ export function startUrlContexts( }, } } - -function getLocationHref() { - const href = mockable(() => window.location?.href)() - return typeof href === 'string' && href.trim() ? href : 'about:blank' -} diff --git a/packages/rum-core/src/domain/view/trackViews.ts b/packages/rum-core/src/domain/view/trackViews.ts index efe58a5531..3cbcb8d223 100644 --- a/packages/rum-core/src/domain/view/trackViews.ts +++ b/packages/rum-core/src/domain/view/trackViews.ts @@ -113,8 +113,7 @@ export function trackViews( configuration: RumConfiguration, locationChangeObservable: Observable, areViewsTrackedAutomatically: boolean, - initialViewOptions?: ViewOptions, - collectViewMetrics: boolean = true + initialViewOptions?: ViewOptions ) { const activeViews: Set> = new Set() let currentView = startNewView(ViewLoadingType.INITIAL_LOAD, clocksOrigin(), initialViewOptions) @@ -142,8 +141,7 @@ export function trackViews( configuration, loadingType, startClocks, - viewOptions, - collectViewMetrics + viewOptions ) activeViews.add(newlyCreatedView) newlyCreatedView.stopObservable.subscribe(() => { @@ -217,8 +215,7 @@ function newView( configuration: RumConfiguration, loadingType: ViewLoadingType, startClocks: ClocksState = clocksNow(), - viewOptions?: ViewOptions, - collectViewMetrics: boolean = true + viewOptions?: ViewOptions ) { // Setup initial values const id = generateUUID() @@ -257,7 +254,6 @@ function newView( leading: false, }) - const emptyCommonViewMetrics: CommonViewMetrics = {} const { setLoadEvent, setViewEnd, @@ -265,32 +261,23 @@ function newView( stopINPTracking, getCommonViewMetrics, setLoadingTime, - } = collectViewMetrics - ? trackCommonViewMetrics( - lifeCycle, - domMutationObservable, - windowOpenObservable, - configuration, - scheduleViewUpdate, - loadingType, - startClocks - ) - : { - setLoadEvent: noop, - setViewEnd: noop, - stop: noop, - stopINPTracking: noop, - getCommonViewMetrics: () => emptyCommonViewMetrics, - setLoadingTime: noop, - } + } = trackCommonViewMetrics( + lifeCycle, + domMutationObservable, + windowOpenObservable, + configuration, + scheduleViewUpdate, + loadingType, + startClocks + ) const { stop: stopInitialViewMetricsTracking, initialViewMetrics } = - collectViewMetrics && loadingType === ViewLoadingType.INITIAL_LOAD + loadingType === ViewLoadingType.INITIAL_LOAD ? trackInitialViewMetrics(configuration, startClocks, setLoadEvent, scheduleViewUpdate) : { stop: noop, initialViewMetrics: {} as InitialViewMetrics } // Start BFCache-specific metrics when restoring from BFCache - if (collectViewMetrics && loadingType === ViewLoadingType.BF_CACHE) { + if (loadingType === ViewLoadingType.BF_CACHE) { trackBfcacheMetrics(startClocks, initialViewMetrics, scheduleViewUpdate) } diff --git a/packages/rum-core/src/domain/view/viewCollection.ts b/packages/rum-core/src/domain/view/viewCollection.ts index fce3db14ff..bda7502a28 100644 --- a/packages/rum-core/src/domain/view/viewCollection.ts +++ b/packages/rum-core/src/domain/view/viewCollection.ts @@ -25,8 +25,7 @@ export function startViewCollection( locationChangeObservable: Observable, recorderApi: RecorderApi, viewHistory: ViewHistory, - initialViewOptions?: ViewOptions, - collectViewMetrics: boolean = true + initialViewOptions?: ViewOptions ) { lifeCycle.subscribe(LifeCycleEventType.VIEW_UPDATED, (view) => lifeCycle.notify(LifeCycleEventType.RAW_RUM_EVENT_COLLECTED, processViewUpdate(view, configuration, recorderApi)) @@ -67,8 +66,7 @@ export function startViewCollection( configuration, locationChangeObservable, !configuration.trackViewsManually, - initialViewOptions, - collectViewMetrics + initialViewOptions ) } diff --git a/packages/rum-slim/src/entries/salesforce.ts b/packages/rum-slim/src/entries/salesforce.ts index e315428b58..8f2167ab80 100644 --- a/packages/rum-slim/src/entries/salesforce.ts +++ b/packages/rum-slim/src/entries/salesforce.ts @@ -59,11 +59,6 @@ export { DefaultPrivacyLevel } from '@datadog/browser-core' // eslint-disable-next-line local-rules/disallow-side-effects const baseRum = makeRumPublicApi(makeRecorderApiStub(), makeProfilerApiStub(), { sdkName: 'rum-slim', - runtimeCapabilities: { - requestCollection: false, - runtimeErrors: false, - viewMetrics: false, - }, }) export const datadogRum: RumPublicApi = { diff --git a/test/e2e/lib/helpers/playwright.ts b/test/e2e/lib/helpers/playwright.ts index 0437e98813..ef1f2344ae 100644 --- a/test/e2e/lib/helpers/playwright.ts +++ b/test/e2e/lib/helpers/playwright.ts @@ -21,18 +21,11 @@ export function getPlaywrightConfigBrowserName(name: string): PlaywrightWorkerOp } export function getEncodedCapabilities(configuration: BrowserConfiguration) { - return getEncodedCapabilitiesWithOptions(configuration) -} - -export function getEncodedCapabilitiesWithOptions( - configuration: BrowserConfiguration, - { localTesting = true }: { localTesting?: boolean } = {} -) { - return encodeURIComponent(JSON.stringify(getCapabilities(configuration, { localTesting }))) + return encodeURIComponent(JSON.stringify(getCapabilities(configuration))) } // see: https://www.browserstack.com/docs/automate/playwright/playwright-capabilities -function getCapabilities(configuration: BrowserConfiguration, { localTesting }: { localTesting: boolean }) { +function getCapabilities(configuration: BrowserConfiguration) { const playwrightVersion = resolvePlaywrightVersionFromPackageJson() return { os: configuration.os, @@ -44,7 +37,7 @@ function getCapabilities(configuration: BrowserConfiguration, { localTesting }: project: 'browser sdk e2e', build: getBuildInfos(), name: configuration.sessionName, - 'browserstack.local': localTesting, + 'browserstack.local': true, 'browserstack.playwrightVersion': playwrightVersion, 'client.playwrightVersion': playwrightVersion, 'browserstack.debug': false, diff --git a/test/e2e/lib/salesforce.ts b/test/e2e/lib/salesforce.ts deleted file mode 100644 index e994db037c..0000000000 --- a/test/e2e/lib/salesforce.ts +++ /dev/null @@ -1,98 +0,0 @@ -import { execFileSync } from 'node:child_process' -import { URL } from 'node:url' -import { expect, type Page, type Request } from '@playwright/test' - -export interface RumViewEventPayload { - type: 'view' - view: { - name?: string - url?: string - } -} - -export function normalizePathname(pathname: string) { - if (!pathname.trim()) { - return '/' - } - - let normalizedPathname = pathname.trim() - if (!normalizedPathname.startsWith('/')) { - normalizedPathname = `/${normalizedPathname}` - } - if (normalizedPathname.length > 1) { - normalizedPathname = normalizedPathname.replace(/\/+$/, '') - } - return normalizedPathname || '/' -} - -export function createRumViewTracker(page: Page) { - const viewEvents: RumViewEventPayload[] = [] - - page.on('request', (request) => { - if (!isRumIntakeRequest(request)) { - return - } - - for (const event of parseRumPayload(request)) { - if (event.type === 'view') { - viewEvents.push(event as RumViewEventPayload) - } - } - }) - - return { - viewEvents, - async waitForViewCount(expectedCount: number) { - await expect - .poll( - () => viewEvents.length, - { - timeout: 30_000, - message: `Expected at least ${expectedCount} RUM view events from Salesforce navigation`, - } - ) - .toBeGreaterThanOrEqual(expectedCount) - }, - } -} - -export async function flushRumEvents(page: Page) { - await page.goto('about:blank') -} - -export async function openLightningWithSf(page: Page, orgAlias: string, path: string) { - const rawResponse = execFileSync('sf', ['org', 'open', '-o', orgAlias, '-p', path, '-r', '--json'], { - encoding: 'utf8', - }) - const response = JSON.parse(rawResponse) as { result?: { url?: string } } - const url = response.result?.url - - if (!url) { - throw new Error(`Unable to open Salesforce org '${orgAlias}' with a one-time login URL.`) - } - - await page.goto(url) -} - -export function getExpectedViewUrl(baseUrl: string, pathname: string) { - return new URL(pathname, `${baseUrl.replace(/\/+$/, '')}/`).href -} - -function isRumIntakeRequest(request: Request) { - const url = request.url() - return request.method() === 'POST' && /\/api\/v2\/rum(?:[/?]|$)/.test(url) -} - -function parseRumPayload(request: Request) { - const bodyBuffer = request.postDataBuffer() - if (!bodyBuffer?.length) { - return [] - } - - return bodyBuffer - .toString('utf8') - .split('\n') - .map((line) => line.trim()) - .filter(Boolean) - .map((line) => JSON.parse(line) as { type?: string }) -} diff --git a/test/e2e/playwright.salesforce.base.config.ts b/test/e2e/playwright.salesforce.base.config.ts deleted file mode 100644 index 1f76790cc7..0000000000 --- a/test/e2e/playwright.salesforce.base.config.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { defineConfig } from '@playwright/test' -import { config as baseConfig } from './playwright.base.config' - -// eslint-disable-next-line import/no-default-export -export default defineConfig({ - ...baseConfig, - testDir: './scenario/salesforce', - webServer: undefined, - fullyParallel: false, - workers: 1, -}) diff --git a/test/e2e/playwright.salesforce.bs.config.ts b/test/e2e/playwright.salesforce.bs.config.ts deleted file mode 100644 index 252c28237b..0000000000 --- a/test/e2e/playwright.salesforce.bs.config.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { defineConfig } from '@playwright/test' -import { browserConfigurations } from './browsers.conf' -import { getEncodedCapabilitiesWithOptions, getPlaywrightConfigBrowserName } from './lib/helpers/playwright' -import baseConfig from './playwright.salesforce.base.config' - -// eslint-disable-next-line import/no-default-export -export default defineConfig({ - ...baseConfig, - workers: 5, - projects: browserConfigurations.map((configuration) => ({ - name: configuration.sessionName, - metadata: configuration, - use: { - browserName: getPlaywrightConfigBrowserName(configuration.name), - connectOptions: { - wsEndpoint: `wss://cdp.browserstack.com/playwright?caps=${getEncodedCapabilitiesWithOptions(configuration, { - localTesting: false, - })}`, - }, - }, - })), -}) diff --git a/test/e2e/playwright.salesforce.local.config.ts b/test/e2e/playwright.salesforce.local.config.ts deleted file mode 100644 index 11a9386c28..0000000000 --- a/test/e2e/playwright.salesforce.local.config.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { defineConfig, devices } from '@playwright/test' -import baseConfig from './playwright.salesforce.base.config' - -// eslint-disable-next-line import/no-default-export -export default defineConfig({ - ...baseConfig, - projects: [ - { - name: 'chromium', - metadata: { - sessionName: 'Desktop Chrome', - name: 'chromium', - }, - use: devices['Desktop Chrome'], - }, - ], -}) diff --git a/test/e2e/scenario/salesforce/README.md b/test/e2e/scenario/salesforce/README.md deleted file mode 100644 index ddf4fb8ace..0000000000 --- a/test/e2e/scenario/salesforce/README.md +++ /dev/null @@ -1,9 +0,0 @@ -# Salesforce E2E - -This suite validates Salesforce view tracking against a deployed `ebikes` org by intercepting real -RUM intake requests in Playwright. - -Required environment variables: - -- `SALESFORCE_EBIKES_SITE_URL` -- `SALESFORCE_EBIKES_ORG_ALIAS` diff --git a/test/e2e/scenario/salesforce/views.scenario.ts b/test/e2e/scenario/salesforce/views.scenario.ts deleted file mode 100644 index cbc12da1fa..0000000000 --- a/test/e2e/scenario/salesforce/views.scenario.ts +++ /dev/null @@ -1,101 +0,0 @@ -import { test, expect } from '@playwright/test' -import { - createRumViewTracker, - flushRumEvents, - getExpectedViewUrl, - normalizePathname, - openLightningWithSf, -} from '../../lib/salesforce' - -const SALESFORCE_EBIKES_SITE_URL = process.env.SALESFORCE_EBIKES_SITE_URL -const SALESFORCE_EBIKES_ORG_ALIAS = process.env.SALESFORCE_EBIKES_ORG_ALIAS - -test.describe('salesforce view tracking', () => { - test('emits views while navigating in Experience Cloud', async ({ page }) => { - test.skip(!SALESFORCE_EBIKES_SITE_URL, 'Set SALESFORCE_EBIKES_SITE_URL to run Salesforce Experience tests.') - - const baseUrl = SALESFORCE_EBIKES_SITE_URL! - const viewTracker = createRumViewTracker(page) - const baseHomePath = normalizePathname(new URL(baseUrl).pathname) - - await page.goto(baseUrl) - await page.getByRole('link', { name: /^Product Explorer$/i }).click() - await page.waitForURL('**/product-explorer') - - await page.getByRole('link', { name: /FUSE X1/i }).click() - await page.waitForURL('**/product/**') - - await page.getByRole('link', { name: /^Home$/i }).click() - await page.waitForURL((url) => normalizePathname(url.pathname) === baseHomePath) - - await flushRumEvents(page) - await viewTracker.waitForViewCount(4) - - const productExplorerPath = `${baseHomePath}/product-explorer`.replace(/\/+/g, '/') - const productDetailView = viewTracker.viewEvents.find((event) => event.view.name?.includes('/product/')) - - expect(viewTracker.viewEvents).toEqual( - expect.arrayContaining([ - expect.objectContaining({ - view: expect.objectContaining({ - name: baseHomePath, - url: getExpectedViewUrl(baseUrl, baseHomePath), - }), - }), - expect.objectContaining({ - view: expect.objectContaining({ - name: productExplorerPath, - url: getExpectedViewUrl(baseUrl, productExplorerPath), - }), - }), - ]) - ) - expect(productDetailView).toEqual( - expect.objectContaining({ - view: expect.objectContaining({ - url: expect.stringContaining('/product/'), - }), - }) - ) - }) - - test('emits views while navigating in Lightning Experience', async ({ page }) => { - test.skip(!SALESFORCE_EBIKES_ORG_ALIAS, 'Set SALESFORCE_EBIKES_ORG_ALIAS to run Salesforce Lightning tests.') - - const viewTracker = createRumViewTracker(page) - await openLightningWithSf(page, SALESFORCE_EBIKES_ORG_ALIAS!, '/lightning/n/Product_Explorer') - await page.waitForURL('**/lightning/n/Product_Explorer') - - await page.getByRole('link', { name: /FUSE X1/i }).click() - await page.waitForURL('**/lightning/r/**/view') - - const recordPath = normalizePathname(new URL(page.url()).pathname) - - await page.goto(new URL('/lightning/n/Product_Explorer', page.url()).href) - await page.waitForURL('**/lightning/n/Product_Explorer') - - await flushRumEvents(page) - await viewTracker.waitForViewCount(3) - - expect(viewTracker.viewEvents).toEqual( - expect.arrayContaining([ - expect.objectContaining({ - view: expect.objectContaining({ - name: '/lightning/n/Product_Explorer', - url: expect.stringContaining('/lightning/n/Product_Explorer'), - }), - }), - expect.objectContaining({ - view: expect.objectContaining({ - name: recordPath, - url: expect.stringContaining(recordPath), - }), - }), - ]) - ) - - expect( - viewTracker.viewEvents.filter((event) => event.view.name === '/lightning/n/Product_Explorer').length - ).toBeGreaterThanOrEqual(2) - }) -}) From 98b9440cf5e84f102add177a915e21aad5db1ada Mon Sep 17 00:00:00 2001 From: "beltran.bulbarella" Date: Wed, 22 Apr 2026 16:57:05 +0200 Subject: [PATCH 03/16] First pass: Preview the SDK from crashing by patching globalThis, readonly objects and event listeners. --- .../core/src/browser/addEventListener.spec.ts | 23 +++++++++ packages/core/src/browser/addEventListener.ts | 11 +++++ packages/core/src/tools/globalObject.spec.ts | 46 +++++++++++++++++ packages/core/src/tools/globalObject.ts | 47 +++++++++++------- .../core/src/tools/instrumentMethod.spec.ts | 37 ++++++++++++++ packages/core/src/tools/instrumentMethod.ts | 39 ++++++++++++++- packages/rum-slim/src/entries/salesforce.ts | 49 +++++++++++++++++-- 7 files changed, 228 insertions(+), 24 deletions(-) create mode 100644 packages/core/src/tools/globalObject.spec.ts diff --git a/packages/core/src/browser/addEventListener.spec.ts b/packages/core/src/browser/addEventListener.spec.ts index dc59626848..2a044f6aa1 100644 --- a/packages/core/src/browser/addEventListener.spec.ts +++ b/packages/core/src/browser/addEventListener.spec.ts @@ -87,6 +87,29 @@ describe('addEventListener', () => { expect(customEventTarget.removeEventListener).toHaveBeenCalled() }) + it('does not break stop() when removeEventListener is missing', () => { + const customEventTarget = { + addEventListener: jasmine.createSpy(), + } as unknown as HTMLElement + + const { stop } = addEventListener({ allowUntrustedEvents: false }, customEventTarget, 'change', noop) + + expect(customEventTarget.addEventListener).toHaveBeenCalled() + expect(stop).not.toThrow() + }) + + it('skips registration when addEventListener is missing', () => { + const listener = jasmine.createSpy() + const customEventTarget = { + removeEventListener: jasmine.createSpy(), + } as unknown as HTMLElement + + const { stop } = addEventListener({ allowUntrustedEvents: false }, customEventTarget, 'change', listener) + + expect(stop).not.toThrow() + expect(customEventTarget.removeEventListener).not.toHaveBeenCalled() + }) + describe('Untrusted event', () => { beforeEach(() => { configuration = { allowUntrustedEvents: false } as Configuration diff --git a/packages/core/src/browser/addEventListener.ts b/packages/core/src/browser/addEventListener.ts index 6fb2c43ba5..2ef7b0bf30 100644 --- a/packages/core/src/browser/addEventListener.ts +++ b/packages/core/src/browser/addEventListener.ts @@ -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 & { __ddIsTrusted?: boolean } @@ -132,10 +133,20 @@ export function addEventListeners 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)) } diff --git a/packages/core/src/tools/globalObject.spec.ts b/packages/core/src/tools/globalObject.spec.ts new file mode 100644 index 0000000000..8ed95e38d4 --- /dev/null +++ b/packages/core/src/tools/globalObject.spec.ts @@ -0,0 +1,46 @@ +import { getGlobalObject } from './globalObject' + +describe('getGlobalObject', () => { + it('returns window when globalThis is unavailable', () => { + const globalThisDescriptor = Object.getOwnPropertyDescriptor(window, 'globalThis') + + if (!globalThisDescriptor?.configurable) { + pending('globalThis descriptor is not configurable in this environment') + } + + Object.defineProperty(window, 'globalThis', { + value: undefined, + configurable: true, + writable: true, + }) + + try { + expect(getGlobalObject()).toBe(window) + } finally { + Object.defineProperty(window, 'globalThis', globalThisDescriptor!) + } + }) + + it('returns window without relying on the Object.prototype fallback when globalThis is unavailable', () => { + const globalThisDescriptor = Object.getOwnPropertyDescriptor(window, 'globalThis') + + if (!globalThisDescriptor?.configurable) { + pending('globalThis descriptor is not configurable in this environment') + } + + Object.defineProperty(window, 'globalThis', { + value: undefined, + configurable: true, + writable: true, + }) + + const definePropertySpy = spyOn(Object, 'defineProperty').and.callThrough() + + try { + expect(getGlobalObject()).toBe(window) + expect(definePropertySpy).not.toHaveBeenCalledWith(Object.prototype, '_dd_temp_', jasmine.any(Object)) + } finally { + Object.defineProperty(window, 'globalThis', globalThisDescriptor!) + } + }) +}) diff --git a/packages/core/src/tools/globalObject.ts b/packages/core/src/tools/globalObject.ts index ce9b637c02..91f0521793 100644 --- a/packages/core/src/tools/globalObject.ts +++ b/packages/core/src/tools/globalObject.ts @@ -17,27 +17,36 @@ export function getGlobalObject(): 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_ + + if (typeof window === 'object') { + return window as unknown as T + } + + if (typeof self === 'object') { + return self 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 } diff --git a/packages/core/src/tools/instrumentMethod.spec.ts b/packages/core/src/tools/instrumentMethod.spec.ts index 097f665bc4..826076f328 100644 --- a/packages/core/src/tools/instrumentMethod.spec.ts +++ b/packages/core/src/tools/instrumentMethod.spec.ts @@ -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) => void>() diff --git a/packages/core/src/tools/instrumentMethod.ts b/packages/core/src/tools/instrumentMethod.ts index 5d5a398824..92641af433 100644 --- a/packages/core/src/tools/instrumentMethod.ts +++ b/packages/core/src/tools/instrumentMethod.ts @@ -73,6 +73,11 @@ export function instrumentMethod) => void, { computeHandlingStack }: { computeHandlingStack?: boolean } = {} ) { + const methodDescriptor = findDescriptorInPrototypeChain(targetPrototype, method) + if (methodDescriptor && !canAssignDescriptor(methodDescriptor)) { + return { stop: noop } + } + let original = targetPrototype[method] if (typeof original !== 'function') { @@ -117,14 +122,22 @@ export function instrumentMethod { 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. + } } }, } @@ -168,3 +181,25 @@ export function instrumentSetter() +sdkLog('global-object-ready', describeGlobalCandidates()) + const baseRum = makeRumPublicApi(makeRecorderApiStub(), makeProfilerApiStub(), { sdkName: 'rum-slim', }) @@ -72,5 +74,46 @@ interface BrowserWindow extends Window { DD_RUM?: RumPublicApi } -// eslint-disable-next-line local-rules/disallow-side-effects -defineGlobal(getGlobalObject(), 'DD_RUM', datadogRum) +sdkLog('define-global:start', describeGlobalCandidates()) +defineGlobal(salesforceGlobal, 'DD_RUM', datadogRum) +sdkLog('define-global:done', describeRegisteredGlobal()) + +function describeGlobalCandidates() { + return { + hasGlobalThis: typeof globalThis === 'object', + hasSelf: typeof self === 'object', + hasWindow: typeof window === 'object', + salesforceGlobalTag: safeCall(() => Object.prototype.toString.call(salesforceGlobal)), + salesforceGlobalHasDdRum: Boolean(safeGet(salesforceGlobal, 'DD_RUM')), + } +} + +function describeRegisteredGlobal() { + return { + salesforceGlobalHasDdRum: Boolean(safeGet(salesforceGlobal, 'DD_RUM')), + globalThisHasDdRum: typeof globalThis === 'object' ? Boolean(safeGet(globalThis, 'DD_RUM')) : undefined, + selfHasDdRum: typeof self === 'object' ? Boolean(safeGet(self, 'DD_RUM')) : undefined, + windowHasDdRum: typeof window === 'object' ? Boolean(safeGet(window, 'DD_RUM')) : undefined, + } +} + +function sdkLog(message: string, payload?: unknown) { + if (typeof payload === 'undefined') { + console.info(`[DD_RUM Salesforce] ${message}`) + return + } + + console.info(`[DD_RUM Salesforce] ${message}`, payload) +} + +function safeGet(value: unknown, propertyName: string | symbol) { + return safeCall(() => Reflect.get(value as object, propertyName)) +} + +function safeCall(callback: () => T, fallbackValue?: T) { + try { + return callback() + } catch { + return fallbackValue as T + } +} From 730973cde58f9b2c53c448889c1a8351405f01e3 Mon Sep 17 00:00:00 2001 From: "beltran.bulbarella" Date: Wed, 22 Apr 2026 17:10:10 +0200 Subject: [PATCH 04/16] Add cookie and location guards. --- .../src/browser/cookieObservable.spec.ts | 23 ++++++++++++ .../rum-core/src/browser/cookieObservable.ts | 35 +++++++++++-------- .../domain/contexts/urlContexts.lws.spec.ts | 35 +++++++++++++++++++ .../src/domain/contexts/urlContexts.ts | 6 +++- 4 files changed, 83 insertions(+), 16 deletions(-) create mode 100644 packages/rum-core/src/domain/contexts/urlContexts.lws.spec.ts diff --git a/packages/rum-core/src/browser/cookieObservable.spec.ts b/packages/rum-core/src/browser/cookieObservable.spec.ts index 9898705276..cfa534d8e4 100644 --- a/packages/rum-core/src/browser/cookieObservable.spec.ts +++ b/packages/rum-core/src/browser/cookieObservable.spec.ts @@ -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') + }) }) diff --git a/packages/rum-core/src/browser/cookieObservable.ts b/packages/rum-core/src/browser/cookieObservable.ts index 3bb3080d19..128a5eea0e 100644 --- a/packages/rum-core/src/browser/cookieObservable.ts +++ b/packages/rum-core/src/browser/cookieObservable.ts @@ -27,22 +27,27 @@ export function createCookieObservable(configuration: Configuration, cookieName: function listenToCookieStoreChange(configuration: Configuration) { return (cookieName: string, callback: (event: string | undefined) => void) => { - const listener = addEventListener( - configuration, - (window as CookieStoreWindow).cookieStore!, - DOM_EVENT.CHANGE, - (event) => { - // Based on our experimentation, we're assuming that entries for the same cookie cannot be in both the 'changed' and 'deleted' arrays. - // However, due to ambiguity in the specification, we asked for clarification: https://github.com/WICG/cookie-store/issues/226 - const changeEvent = - event.changed.find((event) => event.name === cookieName) || - event.deleted.find((event) => event.name === cookieName) - if (changeEvent) { - callback(changeEvent.value) + try { + const listener = addEventListener( + configuration, + (window as CookieStoreWindow).cookieStore!, + DOM_EVENT.CHANGE, + (event) => { + // Based on our experimentation, we're assuming that entries for the same cookie cannot be in both the 'changed' and 'deleted' arrays. + // However, due to ambiguity in the specification, we asked for clarification: https://github.com/WICG/cookie-store/issues/226 + const changeEvent = + event.changed.find((event) => event.name === cookieName) || + event.deleted.find((event) => event.name === cookieName) + if (changeEvent) { + callback(changeEvent.value) + } } - } - ) - return listener.stop + ) + return listener.stop + } catch { + // Some runtimes expose cookieStore but reject event listeners (for example under sandboxed security layers). + return watchCookieFallback(cookieName, callback) + } } } diff --git a/packages/rum-core/src/domain/contexts/urlContexts.lws.spec.ts b/packages/rum-core/src/domain/contexts/urlContexts.lws.spec.ts new file mode 100644 index 0000000000..4fbf47691f --- /dev/null +++ b/packages/rum-core/src/domain/contexts/urlContexts.lws.spec.ts @@ -0,0 +1,35 @@ +import { Observable, relativeToClocks } from '@datadog/browser-core' +import { registerCleanupTask, replaceMockable } from '@datadog/browser-core/test' +import type { LocationChange } from '../../browser/locationChangeObservable' +import { LifeCycle, LifeCycleEventType } from '../lifeCycle' +import type { ViewCreatedEvent } from '../view/trackViews' +import { createHooks } from '../hooks' +import { startUrlContexts } from './urlContexts' + +describe('urlContexts LWS compatibility', () => { + it('should use the provided view url when global location is unavailable', () => { + const lifeCycle = new LifeCycle() + const hooks = createHooks() + const locationChangeObservable = new Observable() + const originalLocation = window.location + + replaceMockable(originalLocation, undefined as unknown as Location) + + const urlContexts = startUrlContexts(lifeCycle, hooks, locationChangeObservable) + registerCleanupTask(() => { + urlContexts.stop() + }) + + expect(() => { + lifeCycle.notify(LifeCycleEventType.BEFORE_VIEW_CREATED, { + startClocks: relativeToClocks(0), + url: 'https://example.com/lightning/page/home', + } as ViewCreatedEvent) + }).not.toThrow() + + expect(urlContexts.findUrl()).toEqual({ + url: 'https://example.com/lightning/page/home', + referrer: document.referrer, + }) + }) +}) diff --git a/packages/rum-core/src/domain/contexts/urlContexts.ts b/packages/rum-core/src/domain/contexts/urlContexts.ts index 530172a84c..c33875ffed 100644 --- a/packages/rum-core/src/domain/contexts/urlContexts.ts +++ b/packages/rum-core/src/domain/contexts/urlContexts.ts @@ -7,6 +7,7 @@ import { DISCARDED, mockable, buildUrl, + getGlobalObject, } from '@datadog/browser-core' import type { LocationChange } from '../../browser/locationChangeObservable' import type { LifeCycle } from '../lifeCycle' @@ -41,8 +42,11 @@ export function startUrlContexts( let previousViewUrl: string | undefined lifeCycle.subscribe(LifeCycleEventType.BEFORE_VIEW_CREATED, ({ startClocks, url }) => { - const locationHref = mockable(location).href + const locationHref = mockable(getGlobalObject().location)?.href const viewUrl = url !== undefined ? buildUrl(url, locationHref).href : locationHref + if (!viewUrl) { + return + } urlContextHistory.add( buildUrlContext({ url: viewUrl, From 6fffdb70477d559c785a4f220d37170c6533084f Mon Sep 17 00:00:00 2001 From: "beltran.bulbarella" Date: Thu, 23 Apr 2026 14:44:49 +0200 Subject: [PATCH 05/16] Add first pass of e2e test setup for Salesforce POC. --- package.json | 1 + test/e2e/playwright.salesforce.config.ts | 45 ++++++++ test/e2e/salesforce/auth.setup.ts | 15 +++ test/e2e/salesforce/experienceCloud.spec.ts | 22 ++++ .../salesforce/lightningExperience.spec.ts | 16 +++ .../support/salesforceRumRegistry.ts | 100 ++++++++++++++++++ .../salesforce/support/salesforceTargets.ts | 56 ++++++++++ 7 files changed, 255 insertions(+) create mode 100644 test/e2e/playwright.salesforce.config.ts create mode 100644 test/e2e/salesforce/auth.setup.ts create mode 100644 test/e2e/salesforce/experienceCloud.spec.ts create mode 100644 test/e2e/salesforce/lightningExperience.spec.ts create mode 100644 test/e2e/salesforce/support/salesforceRumRegistry.ts create mode 100644 test/e2e/salesforce/support/salesforceTargets.ts diff --git a/package.json b/package.json index 52d0d958ff..a38a2a74b4 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/test/e2e/playwright.salesforce.config.ts b/test/e2e/playwright.salesforce.config.ts new file mode 100644 index 0000000000..cb5f938b88 --- /dev/null +++ b/test/e2e/playwright.salesforce.config.ts @@ -0,0 +1,45 @@ +import path from 'node:path' +import { defineConfig, devices } from '@playwright/test' + +const lightningStorageState = path.resolve(__dirname, 'test-results/.auth/salesforce-lightning.json') + +// eslint-disable-next-line import/no-default-export +export default defineConfig({ + testDir: './salesforce', + testMatch: ['**/*.spec.ts'], + tsconfig: './tsconfig.json', + fullyParallel: false, + timeout: 60_000, + workers: 1, + reporter: [['line'], ['./noticeReporter.ts'], ['html']], + use: { + trace: 'retain-on-failure', + }, + projects: [ + { + name: 'setup', + testMatch: ['**/auth.setup.ts'], + use: { + ...devices['Desktop Chrome'], + trace: 'off', + screenshot: 'off', + video: 'off', + }, + }, + { + name: 'experience-chromium', + dependencies: ['setup'], + testMatch: ['**/experienceCloud.spec.ts'], + use: devices['Desktop Chrome'], + }, + { + name: 'lightning-chromium', + dependencies: ['setup'], + testMatch: ['**/lightningExperience.spec.ts'], + use: { + ...devices['Desktop Chrome'], + storageState: lightningStorageState, + }, + }, + ], +}) diff --git a/test/e2e/salesforce/auth.setup.ts b/test/e2e/salesforce/auth.setup.ts new file mode 100644 index 0000000000..12ff5614e4 --- /dev/null +++ b/test/e2e/salesforce/auth.setup.ts @@ -0,0 +1,15 @@ +import { expect, test as setup } from '@playwright/test' +import { mkdirSync } from 'node:fs' +import path from 'node:path' +import { getSalesforceTargets } from './support/salesforceTargets' + +const authDirectory = path.resolve(__dirname, '../test-results/.auth') +const lightningStorageState = path.join(authDirectory, 'salesforce-lightning.json') + +setup('authenticate Lightning Experience via sf org open', async ({ page }) => { + const { loginUrl } = getSalesforceTargets() + await page.goto(loginUrl, { waitUntil: 'commit' }) + await page.waitForURL('**/lightning/page/home', { timeout: 30_000 }) + mkdirSync(authDirectory, { recursive: true }) + await page.context().storageState({ path: lightningStorageState }) +}) diff --git a/test/e2e/salesforce/experienceCloud.spec.ts b/test/e2e/salesforce/experienceCloud.spec.ts new file mode 100644 index 0000000000..efa5094302 --- /dev/null +++ b/test/e2e/salesforce/experienceCloud.spec.ts @@ -0,0 +1,22 @@ +import { expect, test } from '@playwright/test' +import { createSalesforceRumRegistry } from './support/salesforceRumRegistry' +import { getSalesforceTargets } from './support/salesforceTargets' + +test('experience cloud emits a home RUM view before navigating to Product Explorer', async ({ page }) => { + const targets = getSalesforceTargets() + const rumRegistry = createSalesforceRumRegistry(page) + + try { + await page.goto(targets.experienceUrl, { waitUntil: 'domcontentloaded' }) + const productExplorerLink = page.getByRole('link', { name: 'Product Explorer' }) + + await expect(productExplorerLink).toBeVisible() + await expect(page).toHaveURL(/\/ebikes\/s\/?$/) + + await productExplorerLink.click() + await expect(page).toHaveURL(targets.experienceProductExplorerUrl) + await expect.poll(() => rumRegistry.hasViewPath('/ebikes/s'), { timeout: 40_000 }).toBe(true) + } finally { + rumRegistry.stop() + } +}) diff --git a/test/e2e/salesforce/lightningExperience.spec.ts b/test/e2e/salesforce/lightningExperience.spec.ts new file mode 100644 index 0000000000..08ccb31025 --- /dev/null +++ b/test/e2e/salesforce/lightningExperience.spec.ts @@ -0,0 +1,16 @@ +import { expect, test } from '@playwright/test' +import { createSalesforceRumRegistry } from './support/salesforceRumRegistry' +import { getSalesforceTargets } from './support/salesforceTargets' + +test('lightning experience emits a RUM view to real intake on home page load', async ({ page }) => { + const targets = getSalesforceTargets() + const rumRegistry = createSalesforceRumRegistry(page) + + try { + await page.goto(targets.lightningHomeUrl, { waitUntil: 'domcontentloaded' }) + await expect(page).toHaveURL(targets.lightningHomeUrl) + await expect.poll(() => rumRegistry.hasViewPath('/lightning/page/home'), { timeout: 40_000 }).toBe(true) + } finally { + rumRegistry.stop() + } +}) diff --git a/test/e2e/salesforce/support/salesforceRumRegistry.ts b/test/e2e/salesforce/support/salesforceRumRegistry.ts new file mode 100644 index 0000000000..41f610f766 --- /dev/null +++ b/test/e2e/salesforce/support/salesforceRumRegistry.ts @@ -0,0 +1,100 @@ +import type { Page, Request } from '@playwright/test' + +interface SalesforceRumEventView { + url?: unknown +} + +export interface SalesforceRumEvent { + type?: unknown + view?: SalesforceRumEventView +} + +export interface SalesforceRumRegistry { + rumRequests: Request[] + rumEvents: SalesforceRumEvent[] + rumViewEvents: SalesforceRumEvent[] + findViewByPath: (pathname: string) => SalesforceRumEvent | undefined + hasViewPath: (pathname: string) => boolean + stop: () => void +} + +export function createSalesforceRumRegistry(page: Page): SalesforceRumRegistry { + const rumRequests: Request[] = [] + + const onRequest = (request: Request) => { + if (isRumIntakeRequest(request)) { + rumRequests.push(request) + } + } + + page.context().on('request', onRequest) + + const registry: SalesforceRumRegistry = { + get rumRequests() { + return rumRequests + }, + get rumEvents() { + return rumRequests.flatMap((request) => getRumEvents(request)) + }, + get rumViewEvents() { + return registry.rumEvents.filter((event) => event.type === 'view') + }, + findViewByPath(pathname: string) { + const expectedPath = normalizePathname(pathname) + return registry.rumViewEvents.find((event) => normalizePathname(event.view?.url) === expectedPath) + }, + hasViewPath(pathname: string) { + return registry.findViewByPath(pathname) !== undefined + }, + stop() { + page.context().off('request', onRequest) + }, + } + + return registry +} + +function isRumIntakeRequest(request: Request) { + return request.method() === 'POST' && isRumIntakeUrl(request.url()) +} + +function isRumIntakeUrl(candidate: string) { + try { + return new URL(candidate).pathname === '/api/v2/rum' + } catch { + return false + } +} + +function getRumEvents(request: Request): SalesforceRumEvent[] { + const rawBody = request.postDataBuffer()?.toString('utf8') ?? '' + + if (!rawBody.trim()) { + return [] + } + + return rawBody + .split('\n') + .map((line) => line.trim()) + .filter(Boolean) + .flatMap((line) => { + try { + return [JSON.parse(line) as SalesforceRumEvent] + } catch { + return [] + } + }) +} + +function normalizePathname(candidate: unknown) { + if (typeof candidate !== 'string' || !candidate.trim()) { + return undefined + } + + try { + const pathname = new URL(candidate, 'https://example.org').pathname + return pathname.endsWith('/') ? pathname.slice(0, -1) || '/' : pathname + } catch { + return undefined + } +} diff --git a/test/e2e/salesforce/support/salesforceTargets.ts b/test/e2e/salesforce/support/salesforceTargets.ts new file mode 100644 index 0000000000..0ebae26525 --- /dev/null +++ b/test/e2e/salesforce/support/salesforceTargets.ts @@ -0,0 +1,56 @@ +import { execFileSync } from 'node:child_process' + +let cachedTargets: SalesforceTargets | undefined + +export interface SalesforceTargets { + loginUrl: string + experienceUrl: string + experienceProductExplorerUrl: string + lightningHomeUrl: string + lightningProductExplorerUrl: string +} + +// Uses `sf org open --url-only --json` to obtain an authenticated org URL. +// Reference: https://github.com/salesforcecli/plugin-org#sf-org-open +export function getSalesforceTargets() { + if (cachedTargets) { + return cachedTargets + } + + const { NO_COLOR: _ignoredNoColor, ...environment } = process.env + const stdout = execFileSync( + 'sf', + ['org', 'open', '-o', 'ebikes', '--url-only', '--path', '/lightning/page/home', '--json'], + { + cwd: process.cwd(), + encoding: 'utf8', + env: { ...environment, FORCE_COLOR: '0', CLICOLOR: '0' }, + } + ) + + const result = JSON.parse(stripAnsi(stdout)) as { result?: { url?: string } } + const loginUrl = result.result?.url + + if (!loginUrl) { + throw new Error('sf org open did not return an authenticated URL.') + } + + // Derive Lightning and Experience Cloud origins from the authenticated org URL. + const loginOrigin = new URL(loginUrl).origin + const lightningOrigin = loginOrigin.replace('.my.salesforce.com', '.lightning.force.com') + const experienceOrigin = loginOrigin.replace('.my.salesforce.com', '.my.site.com') + + cachedTargets = { + loginUrl, + experienceUrl: `${experienceOrigin}/ebikes/s`, + experienceProductExplorerUrl: `${experienceOrigin}/ebikes/s/product-explorer`, + lightningHomeUrl: `${lightningOrigin}/lightning/page/home`, + lightningProductExplorerUrl: `${lightningOrigin}/lightning/n/Product_Explorer`, + } + + return cachedTargets +} + +function stripAnsi(candidate: string) { + return candidate.replace(/\u001b\[[0-9;]*m/g, '') +} From 794398dd9c198c945e80f711e43e0cc33ea2a72f Mon Sep 17 00:00:00 2001 From: "beltran.bulbarella" Date: Fri, 24 Apr 2026 10:58:23 +0200 Subject: [PATCH 06/16] Ignore CSP violation events when the environment rejects the event listener. --- .../src/domain/report/reportObservable.spec.ts | 14 ++++++++++++++ .../core/src/domain/report/reportObservable.ts | 12 ++++++++---- 2 files changed, 22 insertions(+), 4 deletions(-) diff --git a/packages/core/src/domain/report/reportObservable.spec.ts b/packages/core/src/domain/report/reportObservable.spec.ts index 2d8c4ecd6b..54ed64249a 100644 --- a/packages/core/src/domain/report/reportObservable.spec.ts +++ b/packages/core/src/domain/report/reportObservable.spec.ts @@ -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() + }) }) diff --git a/packages/core/src/domain/report/reportObservable.ts b/packages/core/src/domain/report/reportObservable.ts index 282c5fc935..566eccee4d 100644 --- a/packages/core/src/domain/report/reportObservable.ts +++ b/packages/core/src/domain/report/reportObservable.ts @@ -60,11 +60,15 @@ function createReportObservable(reportTypes: ReportType[]) { function createCspViolationReportObservable(configuration: Configuration) { return new Observable((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 + } }) } From 4b9b2297d9b9d7d195dd745649114a189b086edf Mon Sep 17 00:00:00 2001 From: "beltran.bulbarella" Date: Fri, 24 Apr 2026 11:03:25 +0200 Subject: [PATCH 07/16] Add view tracking by polling window.location.pathname. --- packages/rum-slim/README.md | 5 +- .../src/domain/salesforceViewTracker.spec.ts | 108 +++++++++++++++ .../src/domain/salesforceViewTracker.ts | 131 ++++++++++++++++++ packages/rum-slim/src/entries/salesforce.ts | 71 +++------- 4 files changed, 262 insertions(+), 53 deletions(-) create mode 100644 packages/rum-slim/src/domain/salesforceViewTracker.spec.ts create mode 100644 packages/rum-slim/src/domain/salesforceViewTracker.ts diff --git a/packages/rum-slim/README.md b/packages/rum-slim/README.md index 742e2d57e2..0d805ff47c 100644 --- a/packages/rum-slim/README.md +++ b/packages/rum-slim/README.md @@ -16,9 +16,10 @@ that: - keeps the standard public API: `DD_RUM.init(...)` and `DD_RUM.startView(...)` - forces manual view tracking +- starts and updates views automatically by polling `window.location.pathname` - disables request collection, runtime error collection, and view metrics by design -The Salesforce wrapper is responsible for observing route changes and calling `startView()` with the -normalized pathname and current URL. +The Salesforce wrapper should only load the bundle and initialize RUM once. The bundle emits the +initial view during `init()` and starts a lightweight pathname watcher to emit route-change views. [1]: https://docs.datadoghq.com/real_user_monitoring/browser/setup/client/?tab=rum#initialization-parameters:~:text=compressIntakeRequests diff --git a/packages/rum-slim/src/domain/salesforceViewTracker.spec.ts b/packages/rum-slim/src/domain/salesforceViewTracker.spec.ts new file mode 100644 index 0000000000..0b3a484d88 --- /dev/null +++ b/packages/rum-slim/src/domain/salesforceViewTracker.spec.ts @@ -0,0 +1,108 @@ +import type { Clock } from '@datadog/browser-core/test' +import { mockClock } from '@datadog/browser-core/test' +import { startSalesforceViewTracking } from './salesforceViewTracker' + +describe('salesforce view tracker', () => { + let clock: Clock + let startView: jasmine.Spy + let location: { pathname?: string; href?: string } | undefined + + beforeEach(() => { + clock = mockClock() + startView = jasmine.createSpy() + location = { + pathname: '/lightning/page/home', + href: 'https://example.lightning.force.com/lightning/page/home', + } + }) + + it('starts the current Lightning view on bootstrap', () => { + startSalesforceViewTracking({ + getRumPublicApi: () => ({ startView }), + getLocation: () => location, + }) + + expect(startView).toHaveBeenCalledOnceWith({ + name: '/lightning/page/home', + url: 'https://example.lightning.force.com/lightning/page/home', + }) + }) + + it('does not duplicate the same pathname when only query string or hash changes', () => { + startSalesforceViewTracking({ + getRumPublicApi: () => ({ startView }), + getLocation: () => location, + pollInterval: 500, + }) + + location = { + pathname: '/lightning/page/home/', + href: 'https://example.lightning.force.com/lightning/page/home?foo=bar#hash', + } + clock.tick(500) + + expect(startView).toHaveBeenCalledTimes(1) + }) + + it('starts a new view when polling detects a Lightning pathname change', () => { + startSalesforceViewTracking({ + getRumPublicApi: () => ({ startView }), + getLocation: () => location, + pollInterval: 500, + }) + + location = { + pathname: '/lightning/n/Product_Explorer', + href: 'https://example.lightning.force.com/lightning/n/Product_Explorer', + } + clock.tick(500) + + expect(startView).toHaveBeenCalledTimes(2) + expect(startView.calls.argsFor(1)).toEqual([ + { + name: '/lightning/n/Product_Explorer', + url: 'https://example.lightning.force.com/lightning/n/Product_Explorer', + }, + ]) + }) + + it('keeps polling when location is temporarily unavailable', () => { + location = undefined + + startSalesforceViewTracking({ + getRumPublicApi: () => ({ startView }), + getLocation: () => location, + pollInterval: 500, + }) + + expect(startView).not.toHaveBeenCalled() + + location = { + pathname: '/lightning/page/home', + href: 'https://example.lightning.force.com/lightning/page/home', + } + clock.tick(500) + + expect(startView).toHaveBeenCalledOnceWith({ + name: '/lightning/page/home', + url: 'https://example.lightning.force.com/lightning/page/home', + }) + }) + + it('stops polling', () => { + const subscription = startSalesforceViewTracking({ + getRumPublicApi: () => ({ startView }), + getLocation: () => location, + pollInterval: 500, + }) + + subscription.stop() + location = { + pathname: '/lightning/n/Product_Explorer', + href: 'https://example.lightning.force.com/lightning/n/Product_Explorer', + } + clock.tick(500) + + expect(startView).toHaveBeenCalledTimes(1) + }) +}) diff --git a/packages/rum-slim/src/domain/salesforceViewTracker.ts b/packages/rum-slim/src/domain/salesforceViewTracker.ts new file mode 100644 index 0000000000..32abf9438b --- /dev/null +++ b/packages/rum-slim/src/domain/salesforceViewTracker.ts @@ -0,0 +1,131 @@ +import { clearInterval, setInterval } from '@datadog/browser-core' +import type { TimeoutId } from '@datadog/browser-core' +import type { RumPublicApi, ViewOptions } from '@datadog/browser-rum-core' + +export interface SalesforceLocation { + pathname?: string + href?: string +} + +interface StartSalesforceViewTrackingOptions { + getRumPublicApi: () => Pick | undefined + getLocation?: () => SalesforceLocation | undefined + pollInterval?: number +} + +interface SalesforceView { + key: string + url?: string +} + +const DEFAULT_LOCATION_POLL_INTERVAL = 500 + +export function startSalesforceViewTracking(options: StartSalesforceViewTrackingOptions) { + const getLocation = options.getLocation ?? getNavigationLocation + const pollInterval = options.pollInterval ?? DEFAULT_LOCATION_POLL_INTERVAL + + let lastEmittedRouteKey: string | undefined + let pollIntervalId: TimeoutId | undefined + + trackCurrentView() + pollIntervalId = setInterval(trackCurrentView, pollInterval) + + function trackCurrentView() { + const currentView = resolveCurrentView(getLocation()) + + if (!currentView || currentView.key === lastEmittedRouteKey) { + return + } + + const rumPublicApi = options.getRumPublicApi() + + if (!rumPublicApi) { + return + } + + rumPublicApi.startView(toViewOptions(currentView)) + lastEmittedRouteKey = currentView.key + } + + return { + stop() { + clearInterval(pollIntervalId) + pollIntervalId = undefined + }, + } +} + +function getNavigationLocation(): SalesforceLocation | undefined { + try { + return { + href: window.location.href, + pathname: window.location.pathname, + } + } catch { + return undefined + } +} + +function resolveCurrentView(location: SalesforceLocation | undefined): SalesforceView | undefined { + if (!location) { + return undefined + } + + const url = normalizeLocationHref(location.href) + const key = normalizePathname(location.pathname) ?? getPathnameFromHref(url) + + if (!key) { + return undefined + } + + return { + key, + url, + } +} + +function toViewOptions(view: SalesforceView): ViewOptions { + return view.url ? { name: view.key, url: view.url } : { name: view.key } +} + +function getPathnameFromHref(href: string | undefined) { + if (!href) { + return undefined + } + + try { + return normalizePathname(new URL(href).pathname) + } catch { + return undefined + } +} + +function normalizePathname(pathname: unknown) { + if (typeof pathname !== 'string' || !pathname.trim()) { + return undefined + } + + let normalizedPathname = pathname.trim() + + if (!normalizedPathname.startsWith('/')) { + normalizedPathname = `/${normalizedPathname}` + } + + if (normalizedPathname.length > 1) { + normalizedPathname = normalizedPathname.replace(/\/+$/, '') + } + + return normalizedPathname || '/' +} + +function normalizeLocationHref(href: unknown) { + if (typeof href !== 'string' || !href.trim()) { + return undefined + } + + try { + return new URL(href).href + } catch { + return undefined + } +} diff --git a/packages/rum-slim/src/entries/salesforce.ts b/packages/rum-slim/src/entries/salesforce.ts index 1707c5bd91..d7c00cf4c0 100644 --- a/packages/rum-slim/src/entries/salesforce.ts +++ b/packages/rum-slim/src/entries/salesforce.ts @@ -1,9 +1,10 @@ import { defineGlobal, getGlobalObject } from '@datadog/browser-core' import type { RumPublicApi, RumInitConfiguration } from '@datadog/browser-rum-core' import { makeRumPublicApi } from '@datadog/browser-rum-core' -import { makeRecorderApiStub } from '../boot/stubRecorderApi' -import { makeProfilerApiStub } from '../boot/stubProfilerApi' import { buildSalesforceInitConfiguration } from '../boot/salesforceInitConfiguration' +import { makeProfilerApiStub } from '../boot/stubProfilerApi' +import { makeRecorderApiStub } from '../boot/stubRecorderApi' +import { startSalesforceViewTracking } from '../domain/salesforceViewTracker' export type { User, @@ -21,7 +22,6 @@ export type { } from '@datadog/browser-core' export type { - RumPublicApi as DatadogRum, RumInitConfiguration, RumBeforeSend, ViewOptions, @@ -55,65 +55,34 @@ export type { } from '@datadog/browser-rum-core' export { DEFAULT_TRACKED_RESOURCE_HEADERS } from '@datadog/browser-rum-core' export { DefaultPrivacyLevel } from '@datadog/browser-core' +export type { RumPublicApi as DatadogRum } from '@datadog/browser-rum-core' const salesforceGlobal = getGlobalObject() -sdkLog('global-object-ready', describeGlobalCandidates()) -const baseRum = makeRumPublicApi(makeRecorderApiStub(), makeProfilerApiStub(), { - sdkName: 'rum-slim', -}) +export const datadogRum = createSalesforceDatadogRum() -export const datadogRum: RumPublicApi = { - ...baseRum, - init(initConfiguration: RumInitConfiguration) { - baseRum.init(buildSalesforceInitConfiguration(initConfiguration)) - }, -} +defineGlobal(salesforceGlobal, 'DD_RUM', datadogRum) interface BrowserWindow extends Window { DD_RUM?: RumPublicApi } -sdkLog('define-global:start', describeGlobalCandidates()) -defineGlobal(salesforceGlobal, 'DD_RUM', datadogRum) -sdkLog('define-global:done', describeRegisteredGlobal()) +function createSalesforceDatadogRum(): RumPublicApi { + const baseRum = makeRumPublicApi(makeRecorderApiStub(), makeProfilerApiStub(), { + sdkName: 'rum-slim', + }) + const baseInit = baseRum.init + let stopSalesforceViewTracking: (() => void) | undefined -function describeGlobalCandidates() { - return { - hasGlobalThis: typeof globalThis === 'object', - hasSelf: typeof self === 'object', - hasWindow: typeof window === 'object', - salesforceGlobalTag: safeCall(() => Object.prototype.toString.call(salesforceGlobal)), - salesforceGlobalHasDdRum: Boolean(safeGet(salesforceGlobal, 'DD_RUM')), - } -} + baseRum.init = (initConfiguration: RumInitConfiguration) => { + baseInit(buildSalesforceInitConfiguration(initConfiguration)) -function describeRegisteredGlobal() { - return { - salesforceGlobalHasDdRum: Boolean(safeGet(salesforceGlobal, 'DD_RUM')), - globalThisHasDdRum: typeof globalThis === 'object' ? Boolean(safeGet(globalThis, 'DD_RUM')) : undefined, - selfHasDdRum: typeof self === 'object' ? Boolean(safeGet(self, 'DD_RUM')) : undefined, - windowHasDdRum: typeof window === 'object' ? Boolean(safeGet(window, 'DD_RUM')) : undefined, + if (!stopSalesforceViewTracking) { + stopSalesforceViewTracking = startSalesforceViewTracking({ + getRumPublicApi: () => baseRum, + }).stop + } } -} - -function sdkLog(message: string, payload?: unknown) { - if (typeof payload === 'undefined') { - console.info(`[DD_RUM Salesforce] ${message}`) - return - } - - console.info(`[DD_RUM Salesforce] ${message}`, payload) -} - -function safeGet(value: unknown, propertyName: string | symbol) { - return safeCall(() => Reflect.get(value as object, propertyName)) -} -function safeCall(callback: () => T, fallbackValue?: T) { - try { - return callback() - } catch { - return fallbackValue as T - } + return baseRum } From dc44af115fab561a45362ab40e9045c7190f95d8 Mon Sep 17 00:00:00 2001 From: "beltran.bulbarella" Date: Fri, 24 Apr 2026 14:45:52 +0200 Subject: [PATCH 08/16] Add e2e proxy for Salesforce RUM intake. Do not forward events to Datadog. --- test/e2e/lib/framework/index.ts | 1 + .../lib/framework/intakeProxyMiddleware.ts | 4 +- test/e2e/playwright.salesforce.config.ts | 6 + test/e2e/salesforce/experienceCloud.spec.ts | 27 +- .../salesforce/lightningExperience.spec.ts | 33 +- .../support/salesforceIntakeProxy.ts | 345 ++++++++++++++++++ 6 files changed, 404 insertions(+), 12 deletions(-) create mode 100644 test/e2e/salesforce/support/salesforceIntakeProxy.ts diff --git a/test/e2e/lib/framework/index.ts b/test/e2e/lib/framework/index.ts index 5e275e1610..d847cf9b08 100644 --- a/test/e2e/lib/framework/index.ts +++ b/test/e2e/lib/framework/index.ts @@ -12,6 +12,7 @@ export { microfrontendSetup, } from './pageSetups' export { IntakeRegistry } from './intakeRegistry' +export { createIntakeProxyMiddleware } from './intakeProxyMiddleware' export { getTestServers, waitForServersIdle } from './httpServers' export { flushEvents } from './flushEvents' export { waitForRequests } from './waitForRequests' diff --git a/test/e2e/lib/framework/intakeProxyMiddleware.ts b/test/e2e/lib/framework/intakeProxyMiddleware.ts index 4cfc0dbf24..83b0a01135 100644 --- a/test/e2e/lib/framework/intakeProxyMiddleware.ts +++ b/test/e2e/lib/framework/intakeProxyMiddleware.ts @@ -62,16 +62,18 @@ interface IntakeRequestInfos { interface IntakeProxyOptions { onRequest?: (request: IntakeRequest) => void + forward?: boolean } export function createIntakeProxyMiddleware(options: IntakeProxyOptions): express.RequestHandler { return async (req, res) => { const infos = computeIntakeRequestInfos(req) + const shouldForward = options.forward ?? true try { const [intakeRequest] = await Promise.all([ readIntakeRequest(req, infos), - !infos.isBridge && forwardIntakeRequestToDatadog(req), + shouldForward && !infos.isBridge && forwardIntakeRequestToDatadog(req), ]) options.onRequest?.(intakeRequest) } catch (error) { diff --git a/test/e2e/playwright.salesforce.config.ts b/test/e2e/playwright.salesforce.config.ts index cb5f938b88..001f82d063 100644 --- a/test/e2e/playwright.salesforce.config.ts +++ b/test/e2e/playwright.salesforce.config.ts @@ -13,6 +13,12 @@ export default defineConfig({ workers: 1, reporter: [['line'], ['./noticeReporter.ts'], ['html']], use: { + // So we can send to the intake from the Salesforce Domain. + bypassCSP: true, + // We'll ignore HTTPS errors since we're using self-signed certificates. + ignoreHTTPSErrors: true, + // So we can send to the intake from the Salesforce Domain. + permissions: ['local-network-access'], trace: 'retain-on-failure', }, projects: [ diff --git a/test/e2e/salesforce/experienceCloud.spec.ts b/test/e2e/salesforce/experienceCloud.spec.ts index efa5094302..5f754c3544 100644 --- a/test/e2e/salesforce/experienceCloud.spec.ts +++ b/test/e2e/salesforce/experienceCloud.spec.ts @@ -1,13 +1,21 @@ import { expect, test } from '@playwright/test' -import { createSalesforceRumRegistry } from './support/salesforceRumRegistry' +import { + flushSalesforceRumEvents, + installSalesforceRumProxy, + startSalesforceIntakeProxy, + waitForRumProxyInitialization, +} from './support/salesforceIntakeProxy' import { getSalesforceTargets } from './support/salesforceTargets' -test('experience cloud emits a home RUM view before navigating to Product Explorer', async ({ page }) => { +test('experience cloud emits an initial home view and a route-change Product Explorer view', async ({ page }) => { const targets = getSalesforceTargets() - const rumRegistry = createSalesforceRumRegistry(page) + const intakeProxy = await startSalesforceIntakeProxy() + const productExplorerContent = page.getByText('DYNAMO X1') try { + await installSalesforceRumProxy(page.context(), intakeProxy.origin) await page.goto(targets.experienceUrl, { waitUntil: 'domcontentloaded' }) + await waitForRumProxyInitialization(page, intakeProxy.origin) const productExplorerLink = page.getByRole('link', { name: 'Product Explorer' }) await expect(productExplorerLink).toBeVisible() @@ -15,8 +23,15 @@ test('experience cloud emits a home RUM view before navigating to Product Explor await productExplorerLink.click() await expect(page).toHaveURL(targets.experienceProductExplorerUrl) - await expect.poll(() => rumRegistry.hasViewPath('/ebikes/s'), { timeout: 40_000 }).toBe(true) - } finally { - rumRegistry.stop() + await expect(productExplorerContent).toBeVisible() + + await flushSalesforceRumEvents(page) + + await intakeProxy.waitForViews([ + { path: '/ebikes/s', loadingType: 'initial_load' }, + { path: '/ebikes/s/product-explorer', loadingType: 'route_change' }, + ]) + } finally { + await intakeProxy.stop() } }) diff --git a/test/e2e/salesforce/lightningExperience.spec.ts b/test/e2e/salesforce/lightningExperience.spec.ts index 08ccb31025..c19627c3d4 100644 --- a/test/e2e/salesforce/lightningExperience.spec.ts +++ b/test/e2e/salesforce/lightningExperience.spec.ts @@ -1,16 +1,39 @@ import { expect, test } from '@playwright/test' -import { createSalesforceRumRegistry } from './support/salesforceRumRegistry' +import { + flushSalesforceRumEvents, + installSalesforceRumProxy, + startSalesforceIntakeProxy, + waitForRumProxyInitialization, +} from './support/salesforceIntakeProxy' import { getSalesforceTargets } from './support/salesforceTargets' -test('lightning experience emits a RUM view to real intake on home page load', async ({ page }) => { +test('lightning experience emits an initial home view and a route-change Product Explorer view', async ({ + page, +}) => { const targets = getSalesforceTargets() - const rumRegistry = createSalesforceRumRegistry(page) + const intakeProxy = await startSalesforceIntakeProxy() + const productExplorerContent = page.getByText('DYNAMO X1') try { + await installSalesforceRumProxy(page.context(), intakeProxy.origin) await page.goto(targets.lightningHomeUrl, { waitUntil: 'domcontentloaded' }) + await waitForRumProxyInitialization(page, intakeProxy.origin) + const productExplorerLink = page.getByRole('link', { name: 'Product Explorer' }) + + await expect(productExplorerLink).toBeVisible() await expect(page).toHaveURL(targets.lightningHomeUrl) - await expect.poll(() => rumRegistry.hasViewPath('/lightning/page/home'), { timeout: 40_000 }).toBe(true) + + await productExplorerLink.click() + await expect(page).toHaveURL(targets.lightningProductExplorerUrl) + await expect(productExplorerContent).toBeVisible() + + await flushSalesforceRumEvents(page) + + await intakeProxy.waitForViews([ + { path: '/lightning/page/home', loadingType: 'initial_load' }, + { path: '/lightning/n/Product_Explorer', loadingType: 'route_change' }, + ]) } finally { - rumRegistry.stop() + await intakeProxy.stop() } }) diff --git a/test/e2e/salesforce/support/salesforceIntakeProxy.ts b/test/e2e/salesforce/support/salesforceIntakeProxy.ts new file mode 100644 index 0000000000..bbdf698812 --- /dev/null +++ b/test/e2e/salesforce/support/salesforceIntakeProxy.ts @@ -0,0 +1,345 @@ +import https from 'node:https' +import type http from 'node:http' +import type { AddressInfo } from 'node:net' +import express from 'express' +import forge from 'node-forge' +import type { BrowserContext, Page } from '@playwright/test' +import { createIntakeProxyMiddleware, IntakeRegistry } from '../../lib/framework' + +const SALESFORCE_INTAKE_PROXY_PORT = 9242 +const SALESFORCE_INTAKE_PROXY_IDLE_DELAY = 200 +const SALESFORCE_INTAKE_PROXY_CLOSE_DELAY = 1_000 + +export interface ExpectedSalesforceRumView { + path: string + loadingType: string +} + +export interface SalesforceIntakeProxy { + origin: string + intakeRegistry: IntakeRegistry + waitForViews: (expectedViews: ExpectedSalesforceRumView[], options?: { timeout?: number }) => Promise + waitForIdle: () => Promise + stop: () => Promise +} + +export async function startSalesforceIntakeProxy(): Promise { + const intakeRegistry = new IntakeRegistry() + const waiters = new Set() + const idleWaiter = createIdleWaiter() + const app = express() + + app.use((_req, res, next) => { + idleWaiter.trackResponse(res) + next() + }) + app.use(allowCrossOriginLoopbackRequests()) + app.post( + '/', + createIntakeProxyMiddleware({ + forward: false, + onRequest: (request) => { + intakeRegistry.push(request) + notifyWaiters(waiters, intakeRegistry) + }, + }) + ) + + const server = https.createServer(generateSelfSignedCertificate(), app) + const origin = await listen(server) + + return { + origin, + intakeRegistry, + waitForViews: (expectedViews, options) => waitForViews(waiters, intakeRegistry, expectedViews, options), + waitForIdle: () => idleWaiter.wait(), + stop: () => close(server), + } +} + +function allowCrossOriginLoopbackRequests(): express.RequestHandler { + return (req, res, next) => { + const origin = req.header('origin') + const requestedHeaders = req.header('access-control-request-headers') + + res.header('Access-Control-Allow-Origin', origin || '*') + res.header('Access-Control-Allow-Methods', 'GET,POST,OPTIONS') + res.header('Access-Control-Allow-Headers', requestedHeaders || 'content-type') + res.header('Access-Control-Allow-Private-Network', 'true') + res.header('Vary', 'Origin, Access-Control-Request-Headers') + + if (req.method === 'OPTIONS') { + res.sendStatus(204) + return + } + + next() + } +} + +export async function installSalesforceRumProxy(browserContext: BrowserContext, proxyOrigin: string) { + await browserContext.addInitScript((proxy) => { + const wrappedFlag = '__ddSalesforceRumProxyWrapped__' + + function isRecord(value: unknown): value is Record { + return typeof value === 'object' && value !== null + } + + function hasRumInit( + value: unknown + ): value is Record & { init: (configuration?: unknown, ...args: unknown[]) => unknown } { + return isRecord(value) && typeof value.init === 'function' + } + + function wrapRum(rum: unknown) { + if (!hasRumInit(rum) || rum[wrappedFlag]) { + return rum + } + + const originalInit = rum.init + Object.defineProperty(rum, wrappedFlag, { + configurable: true, + value: true, + }) + rum.init = function (this: unknown, configuration?: unknown, ...args: unknown[]) { + return originalInit.call(this, { ...(isRecord(configuration) ? configuration : {}), proxy }, ...args) + } + return rum + } + + let ddRum = wrapRum(window.DD_RUM) + + try { + Object.defineProperty(window, 'DD_RUM', { + configurable: true, + get() { + return ddRum + }, + set(value) { + ddRum = wrapRum(value) + }, + }) + } catch { + wrapRum(window.DD_RUM) + } + }, proxyOrigin) +} + +export async function waitForRumProxyInitialization(page: Page, proxyOrigin: string) { + await page.waitForFunction( + (expectedProxy) => window.DD_RUM?.getInitConfiguration?.()?.proxy === expectedProxy, + proxyOrigin + ) +} + +export async function flushSalesforceRumEvents(page: Page) { + await page.evaluate(() => { + const beforeUnloadEvent = new Event('beforeunload') as Event & { __ddIsTrusted?: boolean } + beforeUnloadEvent.__ddIsTrusted = true + window.dispatchEvent(beforeUnloadEvent) + }) +} + +interface ViewWaiter { + expectedViews: ExpectedSalesforceRumView[] + timeoutId: NodeJS.Timeout + resolve: () => void + reject: (error: Error) => void +} + +function waitForViews( + waiters: Set, + intakeRegistry: IntakeRegistry, + expectedViews: ExpectedSalesforceRumView[], + { timeout = 10_000 } = {} +) { + return new Promise((resolve, reject) => { + const waiter: ViewWaiter = { + expectedViews, + timeoutId: setTimeout(() => { + waiters.delete(waiter) + reject(createTimeoutError(intakeRegistry, expectedViews)) + }, timeout), + resolve, + reject, + } + + waiters.add(waiter) + notifyWaiters(waiters, intakeRegistry) + }) +} + +function notifyWaiters(waiters: Set, intakeRegistry: IntakeRegistry) { + for (const waiter of waiters) { + if (findMissingViews(intakeRegistry, waiter.expectedViews).length === 0) { + clearTimeout(waiter.timeoutId) + waiters.delete(waiter) + waiter.resolve() + } + } +} + +function findMissingViews(intakeRegistry: IntakeRegistry, expectedViews: ExpectedSalesforceRumView[]) { + return expectedViews.filter( + ({ path, loadingType }) => + !intakeRegistry.rumViewEvents.some( + (event) => normalizePathname(event.view.url) === normalizePathname(path) && event.view.loading_type === loadingType + ) + ) +} + +function createTimeoutError(intakeRegistry: IntakeRegistry, expectedViews: ExpectedSalesforceRumView[]) { + const expected = expectedViews.map(formatExpectedView).join(', ') + const captured = intakeRegistry.rumViewEvents.map(formatCapturedView).join(', ') || 'none' + + return new Error(`Timed out waiting for Salesforce RUM views. Expected: ${expected}. Captured: ${captured}.`) +} + +function formatExpectedView({ path, loadingType }: ExpectedSalesforceRumView) { + return `${path} (${loadingType})` +} + +function formatCapturedView(event: IntakeRegistry['rumViewEvents'][number]) { + return `${normalizePathname(event.view.url) || 'unknown'} (${event.view.loading_type || 'unknown'})` +} + +function normalizePathname(candidate: unknown) { + if (typeof candidate !== 'string' || !candidate.trim()) { + return undefined + } + + try { + const pathname = new URL(candidate, 'https://example.org').pathname + return pathname.endsWith('/') ? pathname.slice(0, -1) || '/' : pathname + } catch { + return undefined + } +} + +function listen(server: https.Server) { + return new Promise((resolve, reject) => { + server.once('error', reject) + server.listen(SALESFORCE_INTAKE_PROXY_PORT, () => { + server.off('error', reject) + const { port } = server.address() as AddressInfo + resolve(`https://localhost:${port}`) + }) + }).catch((error) => { + if ((error as NodeJS.ErrnoException).code === 'EADDRINUSE') { + throw new Error(`Salesforce intake proxy port ${SALESFORCE_INTAKE_PROXY_PORT} is already in use.`) + } + throw error + }) +} + +function close(server: https.Server) { + return new Promise((resolve, reject) => { + const forceCloseTimeoutId = setTimeout(() => { + server.closeAllConnections() + }, SALESFORCE_INTAKE_PROXY_CLOSE_DELAY) + + server.close((error) => { + clearTimeout(forceCloseTimeoutId) + if (error) { + reject(error) + return + } + resolve() + }) + }) +} + +function createIdleWaiter() { + let pendingCount = 0 + let idlePromise = Promise.resolve() + let resolveIdlePromise: undefined | (() => void) + let waitTimeoutId: NodeJS.Timeout | undefined + + function resolveAfterDelay() { + waitTimeoutId = setTimeout(() => { + resolveIdlePromise?.() + resolveIdlePromise = undefined + }, SALESFORCE_INTAKE_PROXY_IDLE_DELAY) + } + + return { + trackResponse(res: http.ServerResponse) { + clearTimeout(waitTimeoutId) + if (!resolveIdlePromise) { + idlePromise = new Promise((resolve) => { + resolveIdlePromise = resolve + }) + } + + pendingCount += 1 + res.on('close', () => { + pendingCount -= 1 + if (pendingCount === 0) { + resolveAfterDelay() + } + }) + }, + wait() { + return idlePromise + }, + } +} + +function generateSelfSignedCertificate() { + const pki = forge.pki + const md = forge.md + const keys = pki.rsa.generateKeyPair(2048) + const cert = pki.createCertificate() + + cert.publicKey = keys.publicKey + cert.serialNumber = '01' + cert.validity.notBefore = new Date() + cert.validity.notAfter = new Date() + cert.validity.notAfter.setFullYear(cert.validity.notBefore.getFullYear() + 1) + + const attrs = [ + { + name: 'commonName', + value: 'localhost', + }, + ] + + cert.setSubject(attrs) + cert.setIssuer(attrs) + cert.setExtensions([ + { + name: 'basicConstraints', + cA: true, + }, + { + name: 'keyUsage', + keyCertSign: true, + digitalSignature: true, + keyEncipherment: true, + }, + { + name: 'extKeyUsage', + serverAuth: true, + }, + { + name: 'subjectAltName', + altNames: [ + { + type: 2, + value: 'localhost', + }, + { + type: 7, + ip: '127.0.0.1', + }, + ], + }, + ]) + + cert.sign(keys.privateKey, md.sha256.create()) + + return { + key: pki.privateKeyToPem(keys.privateKey), + cert: pki.certificateToPem(cert), + } +} From f77d99a5c731512219d21a90c2354b5082be0abf Mon Sep 17 00:00:00 2001 From: "beltran.bulbarella" Date: Fri, 24 Apr 2026 15:23:33 +0200 Subject: [PATCH 09/16] Fix CI, Format and typecheck --- eslint-local-rules/disallowSideEffects.js | 1 + packages/core/src/browser/addEventListener.spec.ts | 10 ++++++---- packages/core/src/tools/globalObject.spec.ts | 4 ++-- .../src/domain/contexts/urlContexts.lws.spec.ts | 4 ++-- packages/rum-slim/src/domain/salesforceViewTracker.ts | 6 +++--- packages/rum-slim/src/entries/salesforce.ts | 5 +++-- test/e2e/salesforce/auth.setup.ts | 2 +- test/e2e/salesforce/lightningExperience.spec.ts | 4 +--- test/e2e/salesforce/support/salesforceIntakeProxy.ts | 3 ++- test/e2e/salesforce/support/salesforceTargets.ts | 6 ++++-- 10 files changed, 25 insertions(+), 20 deletions(-) diff --git a/eslint-local-rules/disallowSideEffects.js b/eslint-local-rules/disallowSideEffects.js index 890c5852df..babceda977 100644 --- a/eslint-local-rules/disallowSideEffects.js +++ b/eslint-local-rules/disallowSideEffects.js @@ -29,6 +29,7 @@ const pathsWithSideEffect = new Set([ `${packagesRoot}/logs/src/entries/main.ts`, `${packagesRoot}/rum/src/entries/main.ts`, `${packagesRoot}/rum-slim/src/entries/main.ts`, + `${packagesRoot}/rum-slim/src/entries/salesforce.ts`, ]) // Those packages are known to have no side effects when evaluated diff --git a/packages/core/src/browser/addEventListener.spec.ts b/packages/core/src/browser/addEventListener.spec.ts index 2a044f6aa1..e7abeb784c 100644 --- a/packages/core/src/browser/addEventListener.spec.ts +++ b/packages/core/src/browser/addEventListener.spec.ts @@ -88,26 +88,28 @@ describe('addEventListener', () => { }) it('does not break stop() when removeEventListener is missing', () => { + const addEventListenerSpy = jasmine.createSpy() const customEventTarget = { - addEventListener: jasmine.createSpy(), + addEventListener: addEventListenerSpy, } as unknown as HTMLElement const { stop } = addEventListener({ allowUntrustedEvents: false }, customEventTarget, 'change', noop) - expect(customEventTarget.addEventListener).toHaveBeenCalled() + 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: jasmine.createSpy(), + removeEventListener: removeEventListenerSpy, } as unknown as HTMLElement const { stop } = addEventListener({ allowUntrustedEvents: false }, customEventTarget, 'change', listener) expect(stop).not.toThrow() - expect(customEventTarget.removeEventListener).not.toHaveBeenCalled() + expect(removeEventListenerSpy).not.toHaveBeenCalled() }) describe('Untrusted event', () => { diff --git a/packages/core/src/tools/globalObject.spec.ts b/packages/core/src/tools/globalObject.spec.ts index 8ed95e38d4..0feb838671 100644 --- a/packages/core/src/tools/globalObject.spec.ts +++ b/packages/core/src/tools/globalObject.spec.ts @@ -15,7 +15,7 @@ describe('getGlobalObject', () => { }) try { - expect(getGlobalObject()).toBe(window) + expect(getGlobalObject() === window).toBe(true) } finally { Object.defineProperty(window, 'globalThis', globalThisDescriptor!) } @@ -37,7 +37,7 @@ describe('getGlobalObject', () => { const definePropertySpy = spyOn(Object, 'defineProperty').and.callThrough() try { - expect(getGlobalObject()).toBe(window) + expect(getGlobalObject() === window).toBe(true) expect(definePropertySpy).not.toHaveBeenCalledWith(Object.prototype, '_dd_temp_', jasmine.any(Object)) } finally { Object.defineProperty(window, 'globalThis', globalThisDescriptor!) diff --git a/packages/rum-core/src/domain/contexts/urlContexts.lws.spec.ts b/packages/rum-core/src/domain/contexts/urlContexts.lws.spec.ts index 4fbf47691f..6c210148f9 100644 --- a/packages/rum-core/src/domain/contexts/urlContexts.lws.spec.ts +++ b/packages/rum-core/src/domain/contexts/urlContexts.lws.spec.ts @@ -1,4 +1,4 @@ -import { Observable, relativeToClocks } from '@datadog/browser-core' +import { clocksOrigin, Observable } from '@datadog/browser-core' import { registerCleanupTask, replaceMockable } from '@datadog/browser-core/test' import type { LocationChange } from '../../browser/locationChangeObservable' import { LifeCycle, LifeCycleEventType } from '../lifeCycle' @@ -22,7 +22,7 @@ describe('urlContexts LWS compatibility', () => { expect(() => { lifeCycle.notify(LifeCycleEventType.BEFORE_VIEW_CREATED, { - startClocks: relativeToClocks(0), + startClocks: clocksOrigin(), url: 'https://example.com/lightning/page/home', } as ViewCreatedEvent) }).not.toThrow() diff --git a/packages/rum-slim/src/domain/salesforceViewTracker.ts b/packages/rum-slim/src/domain/salesforceViewTracker.ts index 32abf9438b..d2ff43f007 100644 --- a/packages/rum-slim/src/domain/salesforceViewTracker.ts +++ b/packages/rum-slim/src/domain/salesforceViewTracker.ts @@ -1,4 +1,4 @@ -import { clearInterval, setInterval } from '@datadog/browser-core' +import { buildUrl, clearInterval, setInterval } from '@datadog/browser-core' import type { TimeoutId } from '@datadog/browser-core' import type { RumPublicApi, ViewOptions } from '@datadog/browser-rum-core' @@ -94,7 +94,7 @@ function getPathnameFromHref(href: string | undefined) { } try { - return normalizePathname(new URL(href).pathname) + return normalizePathname(buildUrl(href).pathname) } catch { return undefined } @@ -124,7 +124,7 @@ function normalizeLocationHref(href: unknown) { } try { - return new URL(href).href + return buildUrl(href).href } catch { return undefined } diff --git a/packages/rum-slim/src/entries/salesforce.ts b/packages/rum-slim/src/entries/salesforce.ts index d7c00cf4c0..47f781ca36 100644 --- a/packages/rum-slim/src/entries/salesforce.ts +++ b/packages/rum-slim/src/entries/salesforce.ts @@ -78,9 +78,10 @@ function createSalesforceDatadogRum(): RumPublicApi { baseInit(buildSalesforceInitConfiguration(initConfiguration)) if (!stopSalesforceViewTracking) { - stopSalesforceViewTracking = startSalesforceViewTracking({ + const salesforceViewTracking = startSalesforceViewTracking({ getRumPublicApi: () => baseRum, - }).stop + }) + stopSalesforceViewTracking = () => salesforceViewTracking.stop() } } diff --git a/test/e2e/salesforce/auth.setup.ts b/test/e2e/salesforce/auth.setup.ts index 12ff5614e4..5193117462 100644 --- a/test/e2e/salesforce/auth.setup.ts +++ b/test/e2e/salesforce/auth.setup.ts @@ -1,6 +1,6 @@ -import { expect, test as setup } from '@playwright/test' import { mkdirSync } from 'node:fs' import path from 'node:path' +import { test as setup } from '@playwright/test' import { getSalesforceTargets } from './support/salesforceTargets' const authDirectory = path.resolve(__dirname, '../test-results/.auth') diff --git a/test/e2e/salesforce/lightningExperience.spec.ts b/test/e2e/salesforce/lightningExperience.spec.ts index c19627c3d4..deeeb207c6 100644 --- a/test/e2e/salesforce/lightningExperience.spec.ts +++ b/test/e2e/salesforce/lightningExperience.spec.ts @@ -7,9 +7,7 @@ import { } from './support/salesforceIntakeProxy' import { getSalesforceTargets } from './support/salesforceTargets' -test('lightning experience emits an initial home view and a route-change Product Explorer view', async ({ - page, -}) => { +test('lightning experience emits an initial home view and a route-change Product Explorer view', async ({ page }) => { const targets = getSalesforceTargets() const intakeProxy = await startSalesforceIntakeProxy() const productExplorerContent = page.getByText('DYNAMO X1') diff --git a/test/e2e/salesforce/support/salesforceIntakeProxy.ts b/test/e2e/salesforce/support/salesforceIntakeProxy.ts index bbdf698812..6504d9491b 100644 --- a/test/e2e/salesforce/support/salesforceIntakeProxy.ts +++ b/test/e2e/salesforce/support/salesforceIntakeProxy.ts @@ -183,7 +183,8 @@ function findMissingViews(intakeRegistry: IntakeRegistry, expectedViews: Expecte return expectedViews.filter( ({ path, loadingType }) => !intakeRegistry.rumViewEvents.some( - (event) => normalizePathname(event.view.url) === normalizePathname(path) && event.view.loading_type === loadingType + (event) => + normalizePathname(event.view.url) === normalizePathname(path) && event.view.loading_type === loadingType ) ) } diff --git a/test/e2e/salesforce/support/salesforceTargets.ts b/test/e2e/salesforce/support/salesforceTargets.ts index 0ebae26525..854aeafc6d 100644 --- a/test/e2e/salesforce/support/salesforceTargets.ts +++ b/test/e2e/salesforce/support/salesforceTargets.ts @@ -1,6 +1,7 @@ import { execFileSync } from 'node:child_process' let cachedTargets: SalesforceTargets | undefined +const ANSI_ESCAPE_SEQUENCE = new RegExp(`${String.fromCharCode(27)}\\[[0-9;]*m`, 'g') export interface SalesforceTargets { loginUrl: string @@ -17,7 +18,8 @@ export function getSalesforceTargets() { return cachedTargets } - const { NO_COLOR: _ignoredNoColor, ...environment } = process.env + const environment = { ...process.env } + delete environment.NO_COLOR const stdout = execFileSync( 'sf', ['org', 'open', '-o', 'ebikes', '--url-only', '--path', '/lightning/page/home', '--json'], @@ -52,5 +54,5 @@ export function getSalesforceTargets() { } function stripAnsi(candidate: string) { - return candidate.replace(/\u001b\[[0-9;]*m/g, '') + return candidate.replace(ANSI_ESCAPE_SEQUENCE, '') } From d76ffe2c1b94da90dab78216aff21d0aae85939f Mon Sep 17 00:00:00 2001 From: "beltran.bulbarella" Date: Tue, 28 Apr 2026 14:38:37 +0200 Subject: [PATCH 10/16] Add Salesforce POC tests for Dreamhouse Aura --- test/e2e/playwright.salesforce.config.ts | 23 +++++++ .../salesforce/dreamhouseAuraAuth.setup.ts | 15 +++++ .../dreamhouseAuraLightningExperience.spec.ts | 39 ++++++++++++ .../salesforce/support/salesforceTargets.ts | 63 +++++++++++++------ 4 files changed, 122 insertions(+), 18 deletions(-) create mode 100644 test/e2e/salesforce/dreamhouseAuraAuth.setup.ts create mode 100644 test/e2e/salesforce/dreamhouseAuraLightningExperience.spec.ts diff --git a/test/e2e/playwright.salesforce.config.ts b/test/e2e/playwright.salesforce.config.ts index 001f82d063..92b6252468 100644 --- a/test/e2e/playwright.salesforce.config.ts +++ b/test/e2e/playwright.salesforce.config.ts @@ -2,6 +2,10 @@ import path from 'node:path' import { defineConfig, devices } from '@playwright/test' const lightningStorageState = path.resolve(__dirname, 'test-results/.auth/salesforce-lightning.json') +const dreamhouseAuraLightningStorageState = path.resolve( + __dirname, + 'test-results/.auth/salesforce-dreamhouse-aura-lightning.json' +) // eslint-disable-next-line import/no-default-export export default defineConfig({ @@ -47,5 +51,24 @@ export default defineConfig({ storageState: lightningStorageState, }, }, + { + name: 'dreamhouse-aura-setup', + testMatch: ['**/dreamhouseAuraAuth.setup.ts'], + use: { + ...devices['Desktop Chrome'], + trace: 'off', + screenshot: 'off', + video: 'off', + }, + }, + { + name: 'dreamhouse-aura-lightning-chromium', + dependencies: ['dreamhouse-aura-setup'], + testMatch: ['**/dreamhouseAuraLightningExperience.spec.ts'], + use: { + ...devices['Desktop Chrome'], + storageState: dreamhouseAuraLightningStorageState, + }, + }, ], }) diff --git a/test/e2e/salesforce/dreamhouseAuraAuth.setup.ts b/test/e2e/salesforce/dreamhouseAuraAuth.setup.ts new file mode 100644 index 0000000000..4e43081315 --- /dev/null +++ b/test/e2e/salesforce/dreamhouseAuraAuth.setup.ts @@ -0,0 +1,15 @@ +import { mkdirSync } from 'node:fs' +import path from 'node:path' +import { test as setup } from '@playwright/test' +import { getDreamhouseAuraSalesforceTargets } from './support/salesforceTargets' + +const authDirectory = path.resolve(__dirname, '../test-results/.auth') +const lightningStorageState = path.join(authDirectory, 'salesforce-dreamhouse-aura-lightning.json') + +setup('authenticate DreamHouse Aura Lightning Experience via sf org open', async ({ page }) => { + const { loginUrl } = getDreamhouseAuraSalesforceTargets() + await page.goto(loginUrl, { waitUntil: 'commit' }) + await page.waitForURL('**/lightning/n/Property_Finder', { timeout: 30_000 }) + mkdirSync(authDirectory, { recursive: true }) + await page.context().storageState({ path: lightningStorageState }) +}) diff --git a/test/e2e/salesforce/dreamhouseAuraLightningExperience.spec.ts b/test/e2e/salesforce/dreamhouseAuraLightningExperience.spec.ts new file mode 100644 index 0000000000..011b991faa --- /dev/null +++ b/test/e2e/salesforce/dreamhouseAuraLightningExperience.spec.ts @@ -0,0 +1,39 @@ +import { expect, test } from '@playwright/test' +import { + flushSalesforceRumEvents, + installSalesforceRumProxy, + startSalesforceIntakeProxy, + waitForRumProxyInitialization, +} from './support/salesforceIntakeProxy' +import { getDreamhouseAuraSalesforceTargets } from './support/salesforceTargets' + +test('dreamhouse aura lightning experience emits an initial Property Finder view and a route-change Property Explorer view', async ({ + page, +}) => { + const targets = getDreamhouseAuraSalesforceTargets() + const intakeProxy = await startSalesforceIntakeProxy() + const propertyExplorerContent = page.getByPlaceholder('How can I assist you?') + + try { + await installSalesforceRumProxy(page.context(), intakeProxy.origin) + await page.goto(targets.lightningPropertyFinderUrl, { waitUntil: 'domcontentloaded' }) + await waitForRumProxyInitialization(page, intakeProxy.origin) + const propertyExplorerLink = page.getByRole('link', { name: 'Property Explorer' }) + + await expect(propertyExplorerLink).toBeVisible() + await expect(page).toHaveURL(targets.lightningPropertyFinderUrl) + + await propertyExplorerLink.click() + await expect(page).toHaveURL(targets.lightningPropertyExplorerUrl) + await expect(propertyExplorerContent).toBeVisible() + + await flushSalesforceRumEvents(page) + + await intakeProxy.waitForViews([ + { path: '/lightning/n/Property_Finder', loadingType: 'initial_load' }, + { path: '/lightning/n/Property_Explorer', loadingType: 'route_change' }, + ]) + } finally { + await intakeProxy.stop() + } +}) diff --git a/test/e2e/salesforce/support/salesforceTargets.ts b/test/e2e/salesforce/support/salesforceTargets.ts index 854aeafc6d..4525219765 100644 --- a/test/e2e/salesforce/support/salesforceTargets.ts +++ b/test/e2e/salesforce/support/salesforceTargets.ts @@ -1,6 +1,7 @@ import { execFileSync } from 'node:child_process' let cachedTargets: SalesforceTargets | undefined +let cachedDreamhouseAuraTargets: DreamhouseAuraSalesforceTargets | undefined const ANSI_ESCAPE_SEQUENCE = new RegExp(`${String.fromCharCode(27)}\\[[0-9;]*m`, 'g') export interface SalesforceTargets { @@ -11,6 +12,12 @@ export interface SalesforceTargets { lightningProductExplorerUrl: string } +export interface DreamhouseAuraSalesforceTargets { + loginUrl: string + lightningPropertyFinderUrl: string + lightningPropertyExplorerUrl: string +} + // Uses `sf org open --url-only --json` to obtain an authenticated org URL. // Reference: https://github.com/salesforcecli/plugin-org#sf-org-open export function getSalesforceTargets() { @@ -18,24 +25,7 @@ export function getSalesforceTargets() { return cachedTargets } - const environment = { ...process.env } - delete environment.NO_COLOR - const stdout = execFileSync( - 'sf', - ['org', 'open', '-o', 'ebikes', '--url-only', '--path', '/lightning/page/home', '--json'], - { - cwd: process.cwd(), - encoding: 'utf8', - env: { ...environment, FORCE_COLOR: '0', CLICOLOR: '0' }, - } - ) - - const result = JSON.parse(stripAnsi(stdout)) as { result?: { url?: string } } - const loginUrl = result.result?.url - - if (!loginUrl) { - throw new Error('sf org open did not return an authenticated URL.') - } + const loginUrl = getAuthenticatedOrgUrl('ebikes', '/lightning/page/home') // Derive Lightning and Experience Cloud origins from the authenticated org URL. const loginOrigin = new URL(loginUrl).origin @@ -53,6 +43,43 @@ export function getSalesforceTargets() { return cachedTargets } +export function getDreamhouseAuraSalesforceTargets() { + if (cachedDreamhouseAuraTargets) { + return cachedDreamhouseAuraTargets + } + + const loginUrl = getAuthenticatedOrgUrl('dreamhouse-aura', '/lightning/n/Property_Finder') + const loginOrigin = new URL(loginUrl).origin + const lightningOrigin = loginOrigin.replace('.my.salesforce.com', '.lightning.force.com') + + cachedDreamhouseAuraTargets = { + loginUrl, + lightningPropertyFinderUrl: `${lightningOrigin}/lightning/n/Property_Finder`, + lightningPropertyExplorerUrl: `${lightningOrigin}/lightning/n/Property_Explorer`, + } + + return cachedDreamhouseAuraTargets +} + +function getAuthenticatedOrgUrl(orgAlias: string, path: string) { + const environment = { ...process.env } + delete environment.NO_COLOR + const stdout = execFileSync('sf', ['org', 'open', '-o', orgAlias, '--url-only', '--path', path, '--json'], { + cwd: process.cwd(), + encoding: 'utf8', + env: { ...environment, FORCE_COLOR: '0', CLICOLOR: '0' }, + }) + + const result = JSON.parse(stripAnsi(stdout)) as { result?: { url?: string } } + const loginUrl = result.result?.url + + if (!loginUrl) { + throw new Error(`sf org open did not return an authenticated URL for ${orgAlias}.`) + } + + return loginUrl +} + function stripAnsi(candidate: string) { return candidate.replace(ANSI_ESCAPE_SEQUENCE, '') } From 4e8305cba6461ade660eb2e97fbc6390cf235623 Mon Sep 17 00:00:00 2001 From: "beltran.bulbarella" Date: Tue, 28 Apr 2026 16:59:03 +0200 Subject: [PATCH 11/16] a --- packages/rum-slim/src/entries/salesforce.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/rum-slim/src/entries/salesforce.ts b/packages/rum-slim/src/entries/salesforce.ts index 47f781ca36..4d881fe687 100644 --- a/packages/rum-slim/src/entries/salesforce.ts +++ b/packages/rum-slim/src/entries/salesforce.ts @@ -87,3 +87,5 @@ function createSalesforceDatadogRum(): RumPublicApi { return baseRum } + +// sss \ No newline at end of file From 5d2a05f5899effa66c1e6b419a5913c1138702ba Mon Sep 17 00:00:00 2001 From: "beltran.bulbarella" Date: Tue, 28 Apr 2026 17:00:02 +0200 Subject: [PATCH 12/16] a --- packages/rum-slim/src/entries/salesforce.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/rum-slim/src/entries/salesforce.ts b/packages/rum-slim/src/entries/salesforce.ts index 4d881fe687..54a170b9b0 100644 --- a/packages/rum-slim/src/entries/salesforce.ts +++ b/packages/rum-slim/src/entries/salesforce.ts @@ -88,4 +88,4 @@ function createSalesforceDatadogRum(): RumPublicApi { return baseRum } -// sss \ No newline at end of file +// sssr shim \ No newline at end of file From 97cadad35446a64314030cad6ce7a4575699615a Mon Sep 17 00:00:00 2001 From: "beltran.bulbarella" Date: Tue, 28 Apr 2026 17:40:12 +0200 Subject: [PATCH 13/16] Revert "Add Salesforce POC tests for Dreamhouse Aura" This reverts commit d76ffe2c1b94da90dab78216aff21d0aae85939f. --- test/e2e/playwright.salesforce.config.ts | 23 ------- .../salesforce/dreamhouseAuraAuth.setup.ts | 15 ----- .../dreamhouseAuraLightningExperience.spec.ts | 39 ------------ .../salesforce/support/salesforceTargets.ts | 63 ++++++------------- 4 files changed, 18 insertions(+), 122 deletions(-) delete mode 100644 test/e2e/salesforce/dreamhouseAuraAuth.setup.ts delete mode 100644 test/e2e/salesforce/dreamhouseAuraLightningExperience.spec.ts diff --git a/test/e2e/playwright.salesforce.config.ts b/test/e2e/playwright.salesforce.config.ts index 92b6252468..001f82d063 100644 --- a/test/e2e/playwright.salesforce.config.ts +++ b/test/e2e/playwright.salesforce.config.ts @@ -2,10 +2,6 @@ import path from 'node:path' import { defineConfig, devices } from '@playwright/test' const lightningStorageState = path.resolve(__dirname, 'test-results/.auth/salesforce-lightning.json') -const dreamhouseAuraLightningStorageState = path.resolve( - __dirname, - 'test-results/.auth/salesforce-dreamhouse-aura-lightning.json' -) // eslint-disable-next-line import/no-default-export export default defineConfig({ @@ -51,24 +47,5 @@ export default defineConfig({ storageState: lightningStorageState, }, }, - { - name: 'dreamhouse-aura-setup', - testMatch: ['**/dreamhouseAuraAuth.setup.ts'], - use: { - ...devices['Desktop Chrome'], - trace: 'off', - screenshot: 'off', - video: 'off', - }, - }, - { - name: 'dreamhouse-aura-lightning-chromium', - dependencies: ['dreamhouse-aura-setup'], - testMatch: ['**/dreamhouseAuraLightningExperience.spec.ts'], - use: { - ...devices['Desktop Chrome'], - storageState: dreamhouseAuraLightningStorageState, - }, - }, ], }) diff --git a/test/e2e/salesforce/dreamhouseAuraAuth.setup.ts b/test/e2e/salesforce/dreamhouseAuraAuth.setup.ts deleted file mode 100644 index 4e43081315..0000000000 --- a/test/e2e/salesforce/dreamhouseAuraAuth.setup.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { mkdirSync } from 'node:fs' -import path from 'node:path' -import { test as setup } from '@playwright/test' -import { getDreamhouseAuraSalesforceTargets } from './support/salesforceTargets' - -const authDirectory = path.resolve(__dirname, '../test-results/.auth') -const lightningStorageState = path.join(authDirectory, 'salesforce-dreamhouse-aura-lightning.json') - -setup('authenticate DreamHouse Aura Lightning Experience via sf org open', async ({ page }) => { - const { loginUrl } = getDreamhouseAuraSalesforceTargets() - await page.goto(loginUrl, { waitUntil: 'commit' }) - await page.waitForURL('**/lightning/n/Property_Finder', { timeout: 30_000 }) - mkdirSync(authDirectory, { recursive: true }) - await page.context().storageState({ path: lightningStorageState }) -}) diff --git a/test/e2e/salesforce/dreamhouseAuraLightningExperience.spec.ts b/test/e2e/salesforce/dreamhouseAuraLightningExperience.spec.ts deleted file mode 100644 index 011b991faa..0000000000 --- a/test/e2e/salesforce/dreamhouseAuraLightningExperience.spec.ts +++ /dev/null @@ -1,39 +0,0 @@ -import { expect, test } from '@playwright/test' -import { - flushSalesforceRumEvents, - installSalesforceRumProxy, - startSalesforceIntakeProxy, - waitForRumProxyInitialization, -} from './support/salesforceIntakeProxy' -import { getDreamhouseAuraSalesforceTargets } from './support/salesforceTargets' - -test('dreamhouse aura lightning experience emits an initial Property Finder view and a route-change Property Explorer view', async ({ - page, -}) => { - const targets = getDreamhouseAuraSalesforceTargets() - const intakeProxy = await startSalesforceIntakeProxy() - const propertyExplorerContent = page.getByPlaceholder('How can I assist you?') - - try { - await installSalesforceRumProxy(page.context(), intakeProxy.origin) - await page.goto(targets.lightningPropertyFinderUrl, { waitUntil: 'domcontentloaded' }) - await waitForRumProxyInitialization(page, intakeProxy.origin) - const propertyExplorerLink = page.getByRole('link', { name: 'Property Explorer' }) - - await expect(propertyExplorerLink).toBeVisible() - await expect(page).toHaveURL(targets.lightningPropertyFinderUrl) - - await propertyExplorerLink.click() - await expect(page).toHaveURL(targets.lightningPropertyExplorerUrl) - await expect(propertyExplorerContent).toBeVisible() - - await flushSalesforceRumEvents(page) - - await intakeProxy.waitForViews([ - { path: '/lightning/n/Property_Finder', loadingType: 'initial_load' }, - { path: '/lightning/n/Property_Explorer', loadingType: 'route_change' }, - ]) - } finally { - await intakeProxy.stop() - } -}) diff --git a/test/e2e/salesforce/support/salesforceTargets.ts b/test/e2e/salesforce/support/salesforceTargets.ts index 4525219765..854aeafc6d 100644 --- a/test/e2e/salesforce/support/salesforceTargets.ts +++ b/test/e2e/salesforce/support/salesforceTargets.ts @@ -1,7 +1,6 @@ import { execFileSync } from 'node:child_process' let cachedTargets: SalesforceTargets | undefined -let cachedDreamhouseAuraTargets: DreamhouseAuraSalesforceTargets | undefined const ANSI_ESCAPE_SEQUENCE = new RegExp(`${String.fromCharCode(27)}\\[[0-9;]*m`, 'g') export interface SalesforceTargets { @@ -12,12 +11,6 @@ export interface SalesforceTargets { lightningProductExplorerUrl: string } -export interface DreamhouseAuraSalesforceTargets { - loginUrl: string - lightningPropertyFinderUrl: string - lightningPropertyExplorerUrl: string -} - // Uses `sf org open --url-only --json` to obtain an authenticated org URL. // Reference: https://github.com/salesforcecli/plugin-org#sf-org-open export function getSalesforceTargets() { @@ -25,7 +18,24 @@ export function getSalesforceTargets() { return cachedTargets } - const loginUrl = getAuthenticatedOrgUrl('ebikes', '/lightning/page/home') + const environment = { ...process.env } + delete environment.NO_COLOR + const stdout = execFileSync( + 'sf', + ['org', 'open', '-o', 'ebikes', '--url-only', '--path', '/lightning/page/home', '--json'], + { + cwd: process.cwd(), + encoding: 'utf8', + env: { ...environment, FORCE_COLOR: '0', CLICOLOR: '0' }, + } + ) + + const result = JSON.parse(stripAnsi(stdout)) as { result?: { url?: string } } + const loginUrl = result.result?.url + + if (!loginUrl) { + throw new Error('sf org open did not return an authenticated URL.') + } // Derive Lightning and Experience Cloud origins from the authenticated org URL. const loginOrigin = new URL(loginUrl).origin @@ -43,43 +53,6 @@ export function getSalesforceTargets() { return cachedTargets } -export function getDreamhouseAuraSalesforceTargets() { - if (cachedDreamhouseAuraTargets) { - return cachedDreamhouseAuraTargets - } - - const loginUrl = getAuthenticatedOrgUrl('dreamhouse-aura', '/lightning/n/Property_Finder') - const loginOrigin = new URL(loginUrl).origin - const lightningOrigin = loginOrigin.replace('.my.salesforce.com', '.lightning.force.com') - - cachedDreamhouseAuraTargets = { - loginUrl, - lightningPropertyFinderUrl: `${lightningOrigin}/lightning/n/Property_Finder`, - lightningPropertyExplorerUrl: `${lightningOrigin}/lightning/n/Property_Explorer`, - } - - return cachedDreamhouseAuraTargets -} - -function getAuthenticatedOrgUrl(orgAlias: string, path: string) { - const environment = { ...process.env } - delete environment.NO_COLOR - const stdout = execFileSync('sf', ['org', 'open', '-o', orgAlias, '--url-only', '--path', path, '--json'], { - cwd: process.cwd(), - encoding: 'utf8', - env: { ...environment, FORCE_COLOR: '0', CLICOLOR: '0' }, - }) - - const result = JSON.parse(stripAnsi(stdout)) as { result?: { url?: string } } - const loginUrl = result.result?.url - - if (!loginUrl) { - throw new Error(`sf org open did not return an authenticated URL for ${orgAlias}.`) - } - - return loginUrl -} - function stripAnsi(candidate: string) { return candidate.replace(ANSI_ESCAPE_SEQUENCE, '') } From a080cc3c2cb2f6c9d5332047d51f766363c76470 Mon Sep 17 00:00:00 2001 From: "beltran.bulbarella" Date: Wed, 29 Apr 2026 10:32:56 +0200 Subject: [PATCH 14/16] Move salesforce package to its own package --- docs/ARCHITECTURE.md | 2 + eslint-local-rules/disallowSideEffects.js | 2 +- eslint.config.mjs | 2 +- packages/rum-slim/README.md | 13 -- packages/rum-slim/package.json | 1 - packages/salesforce/LICENSE | 201 ++++++++++++++++++ packages/salesforce/README.md | 17 ++ packages/salesforce/package.json | 45 ++++ .../boot/salesforceInitConfiguration.spec.ts | 2 +- .../src/boot/salesforceInitConfiguration.ts | 0 .../src/domain/salesforceViewTracker.spec.ts | 0 .../src/domain/salesforceViewTracker.ts | 0 .../src/entries/main.ts} | 40 ++-- packages/salesforce/typedoc.json | 4 + scripts/build/build-package.ts | 17 +- tsconfig.base.json | 1 + yarn.lock | 17 +- 17 files changed, 330 insertions(+), 34 deletions(-) create mode 100644 packages/salesforce/LICENSE create mode 100644 packages/salesforce/README.md create mode 100644 packages/salesforce/package.json rename packages/{rum-slim => salesforce}/src/boot/salesforceInitConfiguration.spec.ts (94%) rename packages/{rum-slim => salesforce}/src/boot/salesforceInitConfiguration.ts (100%) rename packages/{rum-slim => salesforce}/src/domain/salesforceViewTracker.spec.ts (100%) rename packages/{rum-slim => salesforce}/src/domain/salesforceViewTracker.ts (100%) rename packages/{rum-slim/src/entries/salesforce.ts => salesforce/src/entries/main.ts} (75%) create mode 100644 packages/salesforce/typedoc.json diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index f456c993b0..e18f2dea77 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -11,6 +11,7 @@ graph TD core["@datadog/browser-core\n(shared utilities)"] rum-core["@datadog/browser-rum-core\n(core RUM)"] rum["@datadog/browser-rum\n(full RUM)"] + salesforce["@datadog/browser-rum-salesforce\n(Salesforce RUM)"] rum-slim["@datadog/browser-rum-slim\n(lightweight RUM)"] rum-react["@datadog/browser-rum-react\n(React integration)"] logs["@datadog/browser-logs"] @@ -19,6 +20,7 @@ graph TD core --> rum-core core --> logs rum-core --> rum + rum --> salesforce rum-core --> rum-slim rum --> rum-react ``` diff --git a/eslint-local-rules/disallowSideEffects.js b/eslint-local-rules/disallowSideEffects.js index babceda977..7bb92da22c 100644 --- a/eslint-local-rules/disallowSideEffects.js +++ b/eslint-local-rules/disallowSideEffects.js @@ -28,8 +28,8 @@ const packagesRoot = path.resolve(import.meta.dirname, '..', 'packages') const pathsWithSideEffect = new Set([ `${packagesRoot}/logs/src/entries/main.ts`, `${packagesRoot}/rum/src/entries/main.ts`, + `${packagesRoot}/salesforce/src/entries/main.ts`, `${packagesRoot}/rum-slim/src/entries/main.ts`, - `${packagesRoot}/rum-slim/src/entries/salesforce.ts`, ]) // Those packages are known to have no side effects when evaluated diff --git a/eslint.config.mjs b/eslint.config.mjs index 9156c80230..756cc69a42 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -433,7 +433,7 @@ export default tseslint.config( }, { - files: ['packages/{rum,logs,rum-slim}/src/entries/*.ts'], + files: ['packages/{rum,logs,rum-slim,salesforce}/src/entries/*.ts'], rules: { 'local-rules/disallow-enum-exports': 'error', }, diff --git a/packages/rum-slim/README.md b/packages/rum-slim/README.md index 0d805ff47c..29067bf0b5 100644 --- a/packages/rum-slim/README.md +++ b/packages/rum-slim/README.md @@ -9,17 +9,4 @@ recording and the [`compressIntakeRequests`][1] initialization parameter. See the [RUM package](../rum/README.md) documentation. -## Salesforce bundle - -The Salesforce bundle is a dedicated `rum-slim` build for Lightning / Experience Cloud wrappers -that: - -- keeps the standard public API: `DD_RUM.init(...)` and `DD_RUM.startView(...)` -- forces manual view tracking -- starts and updates views automatically by polling `window.location.pathname` -- disables request collection, runtime error collection, and view metrics by design - -The Salesforce wrapper should only load the bundle and initialize RUM once. The bundle emits the -initial view during `init()` and starts a lightweight pathname watcher to emit route-change views. - [1]: https://docs.datadoghq.com/real_user_monitoring/browser/setup/client/?tab=rum#initialization-parameters:~:text=compressIntakeRequests diff --git a/packages/rum-slim/package.json b/packages/rum-slim/package.json index ec0e69e855..4811ac7daf 100644 --- a/packages/rum-slim/package.json +++ b/packages/rum-slim/package.json @@ -16,7 +16,6 @@ "scripts": { "build": "node ../../scripts/build/build-package.ts --modules --bundle datadog-rum-slim.js", "build:bundle": "node ../../scripts/build/build-package.ts --bundle datadog-rum-slim.js", - "build:salesforce": "node ../../scripts/build/build-package.ts --bundle datadog-rum-slim-salesforce.js --entry ./src/entries/salesforce.ts", "prepack": "yarn build" }, "dependencies": { diff --git a/packages/salesforce/LICENSE b/packages/salesforce/LICENSE new file mode 100644 index 0000000000..e6d7fbc979 --- /dev/null +++ b/packages/salesforce/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright 2019-Present Datadog, Inc. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/packages/salesforce/README.md b/packages/salesforce/README.md new file mode 100644 index 0000000000..88e7c7eb60 --- /dev/null +++ b/packages/salesforce/README.md @@ -0,0 +1,17 @@ +# RUM Browser Monitoring - Salesforce package + +## Overview + +This package provides a Salesforce-specific bundle built from the regular +[`@datadog/browser-rum`](../rum) SDK. + +The Salesforce bundle is dedicated to Lightning / Experience Cloud wrappers that: + +- keep the standard public API: `DD_RUM.init(...)` and `DD_RUM.startView(...)` +- force manual view tracking +- start and update views automatically by polling `window.location.pathname` +- disable request collection, runtime error collection, and view metrics by design +- emit a single JavaScript bundle for Salesforce static resource loading + +The Salesforce wrapper should only load the bundle and initialize RUM once. The bundle emits the +initial view during `init()` and starts a lightweight pathname watcher to emit route-change views. diff --git a/packages/salesforce/package.json b/packages/salesforce/package.json new file mode 100644 index 0000000000..e1273e872c --- /dev/null +++ b/packages/salesforce/package.json @@ -0,0 +1,45 @@ +{ + "name": "@datadog/browser-rum-salesforce", + "version": "6.32.0", + "license": "Apache-2.0", + "main": "cjs/entries/main.js", + "module": "esm/entries/main.js", + "types": "cjs/entries/main.d.ts", + "files": [ + "bundle/**/*.js", + "cjs", + "esm", + "src", + "!src/**/*.spec.ts", + "!src/**/*.specHelper.ts" + ], + "scripts": { + "build": "node ../../scripts/build/build-package.ts --modules --bundle datadog-rum-salesforce.js --single-bundle", + "build:bundle": "node ../../scripts/build/build-package.ts --bundle datadog-rum-salesforce.js --single-bundle", + "prepack": "yarn build" + }, + "dependencies": { + "@datadog/browser-core": "6.32.0", + "@datadog/browser-rum": "6.32.0", + "@datadog/browser-rum-core": "6.32.0" + }, + "peerDependencies": { + "@datadog/browser-logs": "6.32.0" + }, + "peerDependenciesMeta": { + "@datadog/browser-logs": { + "optional": true + } + }, + "repository": { + "type": "git", + "url": "https://github.com/DataDog/browser-sdk.git", + "directory": "packages/salesforce" + }, + "volta": { + "extends": "../../package.json" + }, + "publishConfig": { + "access": "public" + } +} diff --git a/packages/rum-slim/src/boot/salesforceInitConfiguration.spec.ts b/packages/salesforce/src/boot/salesforceInitConfiguration.spec.ts similarity index 94% rename from packages/rum-slim/src/boot/salesforceInitConfiguration.spec.ts rename to packages/salesforce/src/boot/salesforceInitConfiguration.spec.ts index b1b5fe57d3..eebcb7da7b 100644 --- a/packages/rum-slim/src/boot/salesforceInitConfiguration.spec.ts +++ b/packages/salesforce/src/boot/salesforceInitConfiguration.spec.ts @@ -2,7 +2,7 @@ import type { RumInitConfiguration } from '@datadog/browser-rum-core' import { buildSalesforceInitConfiguration } from './salesforceInitConfiguration' describe('salesforce init configuration', () => { - it('forces the slim salesforce bundle into manual view tracking mode', () => { + it('forces the salesforce bundle into manual view tracking mode', () => { const initConfiguration = buildSalesforceInitConfiguration({ applicationId: 'app-id', clientToken: 'client-token', diff --git a/packages/rum-slim/src/boot/salesforceInitConfiguration.ts b/packages/salesforce/src/boot/salesforceInitConfiguration.ts similarity index 100% rename from packages/rum-slim/src/boot/salesforceInitConfiguration.ts rename to packages/salesforce/src/boot/salesforceInitConfiguration.ts diff --git a/packages/rum-slim/src/domain/salesforceViewTracker.spec.ts b/packages/salesforce/src/domain/salesforceViewTracker.spec.ts similarity index 100% rename from packages/rum-slim/src/domain/salesforceViewTracker.spec.ts rename to packages/salesforce/src/domain/salesforceViewTracker.spec.ts diff --git a/packages/rum-slim/src/domain/salesforceViewTracker.ts b/packages/salesforce/src/domain/salesforceViewTracker.ts similarity index 100% rename from packages/rum-slim/src/domain/salesforceViewTracker.ts rename to packages/salesforce/src/domain/salesforceViewTracker.ts diff --git a/packages/rum-slim/src/entries/salesforce.ts b/packages/salesforce/src/entries/main.ts similarity index 75% rename from packages/rum-slim/src/entries/salesforce.ts rename to packages/salesforce/src/entries/main.ts index 54a170b9b0..5be63248e4 100644 --- a/packages/rum-slim/src/entries/salesforce.ts +++ b/packages/salesforce/src/entries/main.ts @@ -1,9 +1,7 @@ import { defineGlobal, getGlobalObject } from '@datadog/browser-core' -import type { RumPublicApi, RumInitConfiguration } from '@datadog/browser-rum-core' -import { makeRumPublicApi } from '@datadog/browser-rum-core' +import { datadogRum as baseDatadogRum } from '@datadog/browser-rum' +import type { RumInitConfiguration, RumPublicApi } from '@datadog/browser-rum-core' import { buildSalesforceInitConfiguration } from '../boot/salesforceInitConfiguration' -import { makeProfilerApiStub } from '../boot/stubProfilerApi' -import { makeRecorderApiStub } from '../boot/stubRecorderApi' import { startSalesforceViewTracking } from '../domain/salesforceViewTracker' export type { @@ -20,8 +18,15 @@ export type { ContextArray, RumInternalContext, } from '@datadog/browser-core' +export { DefaultPrivacyLevel } from '@datadog/browser-core' + +/** + * @deprecated Use {@link DatadogRum} instead + */ +export type RumGlobal = RumPublicApi export type { + RumPublicApi as DatadogRum, RumInitConfiguration, RumBeforeSend, ViewOptions, @@ -35,6 +40,8 @@ export type { PropagatorType, FeatureFlagsForEvents, MatchHeader, + + // Events CommonProperties, RumEvent, RumActionEvent, @@ -43,6 +50,8 @@ export type { RumResourceEvent, RumViewEvent, RumVitalEvent, + + // Events context RumEventDomainContext, RumViewEventDomainContext, RumErrorEventDomainContext, @@ -53,24 +62,27 @@ export type { RumOtherResourceEventDomainContext, RumLongTaskEventDomainContext, } from '@datadog/browser-rum-core' + export { DEFAULT_TRACKED_RESOURCE_HEADERS } from '@datadog/browser-rum-core' -export { DefaultPrivacyLevel } from '@datadog/browser-core' -export type { RumPublicApi as DatadogRum } from '@datadog/browser-rum-core' const salesforceGlobal = getGlobalObject() -export const datadogRum = createSalesforceDatadogRum() - -defineGlobal(salesforceGlobal, 'DD_RUM', datadogRum) +/** + * The global RUM instance. Use this to call RUM methods. + * + * @category Main + * @see {@link DatadogRum} + * @see [RUM Browser Monitoring Setup](https://docs.datadoghq.com/real_user_monitoring/browser/) + */ +export const datadogRum = createSalesforceDatadogRum(baseDatadogRum) interface BrowserWindow extends Window { DD_RUM?: RumPublicApi } -function createSalesforceDatadogRum(): RumPublicApi { - const baseRum = makeRumPublicApi(makeRecorderApiStub(), makeProfilerApiStub(), { - sdkName: 'rum-slim', - }) +defineGlobal(salesforceGlobal, 'DD_RUM', datadogRum) + +function createSalesforceDatadogRum(baseRum: RumPublicApi): RumPublicApi { const baseInit = baseRum.init let stopSalesforceViewTracking: (() => void) | undefined @@ -87,5 +99,3 @@ function createSalesforceDatadogRum(): RumPublicApi { return baseRum } - -// sssr shim \ No newline at end of file diff --git a/packages/salesforce/typedoc.json b/packages/salesforce/typedoc.json new file mode 100644 index 0000000000..002b26a53c --- /dev/null +++ b/packages/salesforce/typedoc.json @@ -0,0 +1,4 @@ +{ + "$schema": "https://typedoc.org/schema.json", + "entryPoints": ["src/entries/main.ts"] +} diff --git a/scripts/build/build-package.ts b/scripts/build/build-package.ts index 3fe681e68d..d2435bc61c 100644 --- a/scripts/build/build-package.ts +++ b/scripts/build/build-package.ts @@ -22,6 +22,9 @@ runMain(async () => { entry: { type: 'string', }, + 'single-bundle': { + type: 'boolean', + }, verbose: { type: 'boolean', default: false, @@ -48,6 +51,7 @@ runMain(async () => { await buildBundle({ entry: values.entry ?? './src/entries/main.ts', filename: values.bundle, + singleBundle: !!values['single-bundle'], verbose: values.verbose, }) } @@ -55,7 +59,17 @@ runMain(async () => { printLog('Done.') }) -async function buildBundle({ entry, filename, verbose }: { entry: string; filename: string; verbose: boolean }) { +async function buildBundle({ + entry, + filename, + singleBundle, + verbose, +}: { + entry: string + filename: string + singleBundle: boolean + verbose: boolean +}) { await fs.rm('./bundle', { recursive: true, force: true }) return new Promise((resolve, reject) => { webpack( @@ -63,6 +77,7 @@ async function buildBundle({ entry, filename, verbose }: { entry: string; filena mode: 'production', entry, filename, + plugins: singleBundle ? [new webpack.optimize.LimitChunkCountPlugin({ maxChunks: 1 })] : undefined, }), (error, stats) => { if (error) { diff --git a/tsconfig.base.json b/tsconfig.base.json index 040b37b41f..0b7affd51f 100644 --- a/tsconfig.base.json +++ b/tsconfig.base.json @@ -25,6 +25,7 @@ "@datadog/browser-rum": ["./packages/rum/src/entries/main"], "@datadog/browser-rum/internal": ["./packages/rum/src/entries/internal"], "@datadog/browser-rum/internal-synthetics": ["./packages/rum/src/entries/internalSynthetics"], + "@datadog/browser-rum-salesforce": ["./packages/salesforce/src/entries/main"], "@datadog/browser-rum-slim": ["./packages/rum-slim/src/entries/main"], diff --git a/yarn.lock b/yarn.lock index 887108dff0..8bdccae3c9 100644 --- a/yarn.lock +++ b/yarn.lock @@ -436,6 +436,21 @@ __metadata: languageName: unknown linkType: soft +"@datadog/browser-rum-salesforce@workspace:packages/salesforce": + version: 0.0.0-use.local + resolution: "@datadog/browser-rum-salesforce@workspace:packages/salesforce" + dependencies: + "@datadog/browser-core": "npm:6.32.0" + "@datadog/browser-rum": "npm:6.32.0" + "@datadog/browser-rum-core": "npm:6.32.0" + peerDependencies: + "@datadog/browser-logs": 6.32.0 + peerDependenciesMeta: + "@datadog/browser-logs": + optional: true + languageName: unknown + linkType: soft + "@datadog/browser-rum-slim@workspace:packages/rum-slim": version: 0.0.0-use.local resolution: "@datadog/browser-rum-slim@workspace:packages/rum-slim" @@ -474,7 +489,7 @@ __metadata: languageName: unknown linkType: soft -"@datadog/browser-rum@workspace:*, @datadog/browser-rum@workspace:packages/rum": +"@datadog/browser-rum@npm:6.32.0, @datadog/browser-rum@workspace:*, @datadog/browser-rum@workspace:packages/rum": version: 0.0.0-use.local resolution: "@datadog/browser-rum@workspace:packages/rum" dependencies: From e6a985415baa607188e3833fbe9fc1573d411f71 Mon Sep 17 00:00:00 2001 From: "beltran.bulbarella" Date: Wed, 29 Apr 2026 15:38:15 +0200 Subject: [PATCH 15/16] Rely on self to access the sandbox global object when available --- packages/core/src/tools/globalObject.spec.ts | 32 +++++++++++++++++--- packages/core/src/tools/globalObject.ts | 11 ++++--- 2 files changed, 35 insertions(+), 8 deletions(-) diff --git a/packages/core/src/tools/globalObject.spec.ts b/packages/core/src/tools/globalObject.spec.ts index 0feb838671..edc4c8b37a 100644 --- a/packages/core/src/tools/globalObject.spec.ts +++ b/packages/core/src/tools/globalObject.spec.ts @@ -1,46 +1,70 @@ import { getGlobalObject } from './globalObject' describe('getGlobalObject', () => { - it('returns window when globalThis is unavailable', () => { + 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() === window).toBe(true) + expect(getGlobalObject()).toBe(fakeSelf) } finally { Object.defineProperty(window, 'globalThis', globalThisDescriptor!) + Object.defineProperty(window, 'self', selfDescriptor!) } }) - it('returns window without relying on the Object.prototype fallback when globalThis is unavailable', () => { + 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() === window).toBe(true) + 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!) } }) }) diff --git a/packages/core/src/tools/globalObject.ts b/packages/core/src/tools/globalObject.ts index 91f0521793..fe97bc8ca0 100644 --- a/packages/core/src/tools/globalObject.ts +++ b/packages/core/src/tools/globalObject.ts @@ -18,14 +18,17 @@ export function getGlobalObject(): T { return globalThis as unknown as T } - if (typeof window === 'object') { - return window as unknown as T - } - + // 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 { From 4935c49845719b4c48ba2da67ce7a4a5155d97bd Mon Sep 17 00:00:00 2001 From: "beltran.bulbarella" Date: Wed, 29 Apr 2026 15:38:36 +0200 Subject: [PATCH 16/16] Use regular rum package for salesforce --- docs/ARCHITECTURE.md | 2 - eslint-local-rules/disallowSideEffects.js | 1 - eslint.config.mjs | 2 +- packages/rum/package.json | 1 + .../salesforce/initConfiguration.spec.ts} | 22 +- .../domain/salesforce/initConfiguration.ts | 12 ++ .../domain/salesforce/viewTracker.spec.ts} | 2 +- .../src/domain/salesforce/viewTracker.ts} | 0 .../main.ts => rum/src/entries/salesforce.ts} | 32 +-- packages/salesforce/LICENSE | 201 ------------------ packages/salesforce/README.md | 17 -- packages/salesforce/package.json | 45 ---- .../src/boot/salesforceInitConfiguration.ts | 18 -- packages/salesforce/typedoc.json | 4 - tsconfig.base.json | 2 - yarn.lock | 17 +- 16 files changed, 46 insertions(+), 332 deletions(-) rename packages/{salesforce/src/boot/salesforceInitConfiguration.spec.ts => rum/src/domain/salesforce/initConfiguration.spec.ts} (62%) create mode 100644 packages/rum/src/domain/salesforce/initConfiguration.ts rename packages/{salesforce/src/domain/salesforceViewTracker.spec.ts => rum/src/domain/salesforce/viewTracker.spec.ts} (97%) rename packages/{salesforce/src/domain/salesforceViewTracker.ts => rum/src/domain/salesforce/viewTracker.ts} (100%) rename packages/{salesforce/src/entries/main.ts => rum/src/entries/salesforce.ts} (78%) delete mode 100644 packages/salesforce/LICENSE delete mode 100644 packages/salesforce/README.md delete mode 100644 packages/salesforce/package.json delete mode 100644 packages/salesforce/src/boot/salesforceInitConfiguration.ts delete mode 100644 packages/salesforce/typedoc.json diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index e18f2dea77..f456c993b0 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -11,7 +11,6 @@ graph TD core["@datadog/browser-core\n(shared utilities)"] rum-core["@datadog/browser-rum-core\n(core RUM)"] rum["@datadog/browser-rum\n(full RUM)"] - salesforce["@datadog/browser-rum-salesforce\n(Salesforce RUM)"] rum-slim["@datadog/browser-rum-slim\n(lightweight RUM)"] rum-react["@datadog/browser-rum-react\n(React integration)"] logs["@datadog/browser-logs"] @@ -20,7 +19,6 @@ graph TD core --> rum-core core --> logs rum-core --> rum - rum --> salesforce rum-core --> rum-slim rum --> rum-react ``` diff --git a/eslint-local-rules/disallowSideEffects.js b/eslint-local-rules/disallowSideEffects.js index 7bb92da22c..890c5852df 100644 --- a/eslint-local-rules/disallowSideEffects.js +++ b/eslint-local-rules/disallowSideEffects.js @@ -28,7 +28,6 @@ const packagesRoot = path.resolve(import.meta.dirname, '..', 'packages') const pathsWithSideEffect = new Set([ `${packagesRoot}/logs/src/entries/main.ts`, `${packagesRoot}/rum/src/entries/main.ts`, - `${packagesRoot}/salesforce/src/entries/main.ts`, `${packagesRoot}/rum-slim/src/entries/main.ts`, ]) diff --git a/eslint.config.mjs b/eslint.config.mjs index 756cc69a42..9156c80230 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -433,7 +433,7 @@ export default tseslint.config( }, { - files: ['packages/{rum,logs,rum-slim,salesforce}/src/entries/*.ts'], + files: ['packages/{rum,logs,rum-slim}/src/entries/*.ts'], rules: { 'local-rules/disallow-enum-exports': 'error', }, diff --git a/packages/rum/package.json b/packages/rum/package.json index f0689c7ee1..a693d74f0f 100644 --- a/packages/rum/package.json +++ b/packages/rum/package.json @@ -18,6 +18,7 @@ "scripts": { "build": "node ../../scripts/build/build-package.ts --modules --bundle datadog-rum.js", "build:bundle": "node ../../scripts/build/build-package.ts --bundle datadog-rum.js", + "build:salesforce": "node ../../scripts/build/build-package.ts --bundle datadog-rum-salesforce.js --entry ./src/entries/salesforce.ts", "prepack": "yarn build" }, "dependencies": { diff --git a/packages/salesforce/src/boot/salesforceInitConfiguration.spec.ts b/packages/rum/src/domain/salesforce/initConfiguration.spec.ts similarity index 62% rename from packages/salesforce/src/boot/salesforceInitConfiguration.spec.ts rename to packages/rum/src/domain/salesforce/initConfiguration.spec.ts index eebcb7da7b..b716ffe4e4 100644 --- a/packages/salesforce/src/boot/salesforceInitConfiguration.spec.ts +++ b/packages/rum/src/domain/salesforce/initConfiguration.spec.ts @@ -1,30 +1,29 @@ import type { RumInitConfiguration } from '@datadog/browser-rum-core' -import { buildSalesforceInitConfiguration } from './salesforceInitConfiguration' +import { buildSalesforceInitConfiguration } from './initConfiguration' describe('salesforce init configuration', () => { - it('forces the salesforce bundle into manual view tracking mode', () => { + it('forces manual view tracking', () => { const initConfiguration = buildSalesforceInitConfiguration({ applicationId: 'app-id', clientToken: 'client-token', trackViewsManually: false, - trackResources: true, - trackUserInteractions: true, - trackLongTasks: true, } as RumInitConfiguration) expect(initConfiguration.trackViewsManually).toBeTrue() - expect(initConfiguration.trackResources).toBeFalse() - expect(initConfiguration.trackUserInteractions).toBeFalse() - expect(initConfiguration.trackLongTasks).toBeFalse() }) - it('preserves customer configuration unrelated to the salesforce view-tracking policy', () => { + it('preserves customer configuration unrelated to view tracking mode', () => { const initConfiguration = buildSalesforceInitConfiguration({ applicationId: 'app-id', clientToken: 'client-token', service: 'browser-sdk-sandbox', env: 'dev', site: 'datadoghq.com', + trackResources: false, + trackUserInteractions: false, + trackLongTasks: false, + sessionReplaySampleRate: 0, + profilingSampleRate: 0, } as RumInitConfiguration) expect(initConfiguration).toEqual( @@ -34,6 +33,11 @@ describe('salesforce init configuration', () => { service: 'browser-sdk-sandbox', env: 'dev', site: 'datadoghq.com', + trackResources: false, + trackUserInteractions: false, + trackLongTasks: false, + sessionReplaySampleRate: 0, + profilingSampleRate: 0, }) ) }) diff --git a/packages/rum/src/domain/salesforce/initConfiguration.ts b/packages/rum/src/domain/salesforce/initConfiguration.ts new file mode 100644 index 0000000000..aa6f89bc7a --- /dev/null +++ b/packages/rum/src/domain/salesforce/initConfiguration.ts @@ -0,0 +1,12 @@ +import type { RumInitConfiguration } from '@datadog/browser-rum-core' + +const SALESFORCE_INIT_DEFAULTS: Pick = { + trackViewsManually: true, +} + +export function buildSalesforceInitConfiguration(initConfiguration: RumInitConfiguration): RumInitConfiguration { + return { + ...initConfiguration, + ...SALESFORCE_INIT_DEFAULTS, + } +} diff --git a/packages/salesforce/src/domain/salesforceViewTracker.spec.ts b/packages/rum/src/domain/salesforce/viewTracker.spec.ts similarity index 97% rename from packages/salesforce/src/domain/salesforceViewTracker.spec.ts rename to packages/rum/src/domain/salesforce/viewTracker.spec.ts index 0b3a484d88..e1bb3bf7c2 100644 --- a/packages/salesforce/src/domain/salesforceViewTracker.spec.ts +++ b/packages/rum/src/domain/salesforce/viewTracker.spec.ts @@ -1,6 +1,6 @@ import type { Clock } from '@datadog/browser-core/test' import { mockClock } from '@datadog/browser-core/test' -import { startSalesforceViewTracking } from './salesforceViewTracker' +import { startSalesforceViewTracking } from './viewTracker' describe('salesforce view tracker', () => { let clock: Clock diff --git a/packages/salesforce/src/domain/salesforceViewTracker.ts b/packages/rum/src/domain/salesforce/viewTracker.ts similarity index 100% rename from packages/salesforce/src/domain/salesforceViewTracker.ts rename to packages/rum/src/domain/salesforce/viewTracker.ts diff --git a/packages/salesforce/src/entries/main.ts b/packages/rum/src/entries/salesforce.ts similarity index 78% rename from packages/salesforce/src/entries/main.ts rename to packages/rum/src/entries/salesforce.ts index 5be63248e4..aeb06978fb 100644 --- a/packages/salesforce/src/entries/main.ts +++ b/packages/rum/src/entries/salesforce.ts @@ -1,8 +1,11 @@ import { defineGlobal, getGlobalObject } from '@datadog/browser-core' -import { datadogRum as baseDatadogRum } from '@datadog/browser-rum' import type { RumInitConfiguration, RumPublicApi } from '@datadog/browser-rum-core' -import { buildSalesforceInitConfiguration } from '../boot/salesforceInitConfiguration' -import { startSalesforceViewTracking } from '../domain/salesforceViewTracker' +import { makeRumPublicApi } from '@datadog/browser-rum-core' +import { makeProfilerApi } from '../boot/profilerApi' +import { makeRecorderApi } from '../boot/recorderApi' +import { createDeflateEncoder, startDeflateWorker } from '../domain/deflate' +import { buildSalesforceInitConfiguration } from '../domain/salesforce/initConfiguration' +import { startSalesforceViewTracking } from '../domain/salesforce/viewTracker' export type { User, @@ -40,8 +43,6 @@ export type { PropagatorType, FeatureFlagsForEvents, MatchHeader, - - // Events CommonProperties, RumEvent, RumActionEvent, @@ -50,8 +51,6 @@ export type { RumResourceEvent, RumViewEvent, RumVitalEvent, - - // Events context RumEventDomainContext, RumViewEventDomainContext, RumErrorEventDomainContext, @@ -67,14 +66,17 @@ export { DEFAULT_TRACKED_RESOURCE_HEADERS } from '@datadog/browser-rum-core' const salesforceGlobal = getGlobalObject() -/** - * The global RUM instance. Use this to call RUM methods. - * - * @category Main - * @see {@link DatadogRum} - * @see [RUM Browser Monitoring Setup](https://docs.datadoghq.com/real_user_monitoring/browser/) - */ -export const datadogRum = createSalesforceDatadogRum(baseDatadogRum) +const recorderApi = makeRecorderApi() + +const profilerApi = makeProfilerApi() + +export const datadogRum = createSalesforceDatadogRum( + makeRumPublicApi(recorderApi, profilerApi, { + startDeflateWorker, + createDeflateEncoder, + sdkName: 'rum', + }) +) interface BrowserWindow extends Window { DD_RUM?: RumPublicApi diff --git a/packages/salesforce/LICENSE b/packages/salesforce/LICENSE deleted file mode 100644 index e6d7fbc979..0000000000 --- a/packages/salesforce/LICENSE +++ /dev/null @@ -1,201 +0,0 @@ - Apache License - Version 2.0, January 2004 - http://www.apache.org/licenses/ - - TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - - 1. Definitions. - - "License" shall mean the terms and conditions for use, reproduction, - and distribution as defined by Sections 1 through 9 of this document. - - "Licensor" shall mean the copyright owner or entity authorized by - the copyright owner that is granting the License. - - "Legal Entity" shall mean the union of the acting entity and all - other entities that control, are controlled by, or are under common - control with that entity. For the purposes of this definition, - "control" means (i) the power, direct or indirect, to cause the - direction or management of such entity, whether by contract or - otherwise, or (ii) ownership of fifty percent (50%) or more of the - outstanding shares, or (iii) beneficial ownership of such entity. - - "You" (or "Your") shall mean an individual or Legal Entity - exercising permissions granted by this License. - - "Source" form shall mean the preferred form for making modifications, - including but not limited to software source code, documentation - source, and configuration files. - - "Object" form shall mean any form resulting from mechanical - transformation or translation of a Source form, including but - not limited to compiled object code, generated documentation, - and conversions to other media types. - - "Work" shall mean the work of authorship, whether in Source or - Object form, made available under the License, as indicated by a - copyright notice that is included in or attached to the work - (an example is provided in the Appendix below). - - "Derivative Works" shall mean any work, whether in Source or Object - form, that is based on (or derived from) the Work and for which the - editorial revisions, annotations, elaborations, or other modifications - represent, as a whole, an original work of authorship. For the purposes - of this License, Derivative Works shall not include works that remain - separable from, or merely link (or bind by name) to the interfaces of, - the Work and Derivative Works thereof. - - "Contribution" shall mean any work of authorship, including - the original version of the Work and any modifications or additions - to that Work or Derivative Works thereof, that is intentionally - submitted to Licensor for inclusion in the Work by the copyright owner - or by an individual or Legal Entity authorized to submit on behalf of - the copyright owner. For the purposes of this definition, "submitted" - means any form of electronic, verbal, or written communication sent - to the Licensor or its representatives, including but not limited to - communication on electronic mailing lists, source code control systems, - and issue tracking systems that are managed by, or on behalf of, the - Licensor for the purpose of discussing and improving the Work, but - excluding communication that is conspicuously marked or otherwise - designated in writing by the copyright owner as "Not a Contribution." - - "Contributor" shall mean Licensor and any individual or Legal Entity - on behalf of whom a Contribution has been received by Licensor and - subsequently incorporated within the Work. - - 2. Grant of Copyright License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - copyright license to reproduce, prepare Derivative Works of, - publicly display, publicly perform, sublicense, and distribute the - Work and such Derivative Works in Source or Object form. - - 3. Grant of Patent License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - (except as stated in this section) patent license to make, have made, - use, offer to sell, sell, import, and otherwise transfer the Work, - where such license applies only to those patent claims licensable - by such Contributor that are necessarily infringed by their - Contribution(s) alone or by combination of their Contribution(s) - with the Work to which such Contribution(s) was submitted. If You - institute patent litigation against any entity (including a - cross-claim or counterclaim in a lawsuit) alleging that the Work - or a Contribution incorporated within the Work constitutes direct - or contributory patent infringement, then any patent licenses - granted to You under this License for that Work shall terminate - as of the date such litigation is filed. - - 4. Redistribution. You may reproduce and distribute copies of the - Work or Derivative Works thereof in any medium, with or without - modifications, and in Source or Object form, provided that You - meet the following conditions: - - (a) You must give any other recipients of the Work or - Derivative Works a copy of this License; and - - (b) You must cause any modified files to carry prominent notices - stating that You changed the files; and - - (c) You must retain, in the Source form of any Derivative Works - that You distribute, all copyright, patent, trademark, and - attribution notices from the Source form of the Work, - excluding those notices that do not pertain to any part of - the Derivative Works; and - - (d) If the Work includes a "NOTICE" text file as part of its - distribution, then any Derivative Works that You distribute must - include a readable copy of the attribution notices contained - within such NOTICE file, excluding those notices that do not - pertain to any part of the Derivative Works, in at least one - of the following places: within a NOTICE text file distributed - as part of the Derivative Works; within the Source form or - documentation, if provided along with the Derivative Works; or, - within a display generated by the Derivative Works, if and - wherever such third-party notices normally appear. The contents - of the NOTICE file are for informational purposes only and - do not modify the License. You may add Your own attribution - notices within Derivative Works that You distribute, alongside - or as an addendum to the NOTICE text from the Work, provided - that such additional attribution notices cannot be construed - as modifying the License. - - You may add Your own copyright statement to Your modifications and - may provide additional or different license terms and conditions - for use, reproduction, or distribution of Your modifications, or - for any such Derivative Works as a whole, provided Your use, - reproduction, and distribution of the Work otherwise complies with - the conditions stated in this License. - - 5. Submission of Contributions. Unless You explicitly state otherwise, - any Contribution intentionally submitted for inclusion in the Work - by You to the Licensor shall be under the terms and conditions of - this License, without any additional terms or conditions. - Notwithstanding the above, nothing herein shall supersede or modify - the terms of any separate license agreement you may have executed - with Licensor regarding such Contributions. - - 6. Trademarks. This License does not grant permission to use the trade - names, trademarks, service marks, or product names of the Licensor, - except as required for reasonable and customary use in describing the - origin of the Work and reproducing the content of the NOTICE file. - - 7. Disclaimer of Warranty. Unless required by applicable law or - agreed to in writing, Licensor provides the Work (and each - Contributor provides its Contributions) on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or - implied, including, without limitation, any warranties or conditions - of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A - PARTICULAR PURPOSE. You are solely responsible for determining the - appropriateness of using or redistributing the Work and assume any - risks associated with Your exercise of permissions under this License. - - 8. Limitation of Liability. In no event and under no legal theory, - whether in tort (including negligence), contract, or otherwise, - unless required by applicable law (such as deliberate and grossly - negligent acts) or agreed to in writing, shall any Contributor be - liable to You for damages, including any direct, indirect, special, - incidental, or consequential damages of any character arising as a - result of this License or out of the use or inability to use the - Work (including but not limited to damages for loss of goodwill, - work stoppage, computer failure or malfunction, or any and all - other commercial damages or losses), even if such Contributor - has been advised of the possibility of such damages. - - 9. Accepting Warranty or Additional Liability. While redistributing - the Work or Derivative Works thereof, You may choose to offer, - and charge a fee for, acceptance of support, warranty, indemnity, - or other liability obligations and/or rights consistent with this - License. However, in accepting such obligations, You may act only - on Your own behalf and on Your sole responsibility, not on behalf - of any other Contributor, and only if You agree to indemnify, - defend, and hold each Contributor harmless for any liability - incurred by, or claims asserted against, such Contributor by reason - of your accepting any such warranty or additional liability. - - END OF TERMS AND CONDITIONS - - APPENDIX: How to apply the Apache License to your work. - - To apply the Apache License to your work, attach the following - boilerplate notice, with the fields enclosed by brackets "[]" - replaced with your own identifying information. (Don't include - the brackets!) The text should be enclosed in the appropriate - comment syntax for the file format. We also recommend that a - file or class name and description of purpose be included on the - same "printed page" as the copyright notice for easier - identification within third-party archives. - - Copyright 2019-Present Datadog, Inc. - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. diff --git a/packages/salesforce/README.md b/packages/salesforce/README.md deleted file mode 100644 index 88e7c7eb60..0000000000 --- a/packages/salesforce/README.md +++ /dev/null @@ -1,17 +0,0 @@ -# RUM Browser Monitoring - Salesforce package - -## Overview - -This package provides a Salesforce-specific bundle built from the regular -[`@datadog/browser-rum`](../rum) SDK. - -The Salesforce bundle is dedicated to Lightning / Experience Cloud wrappers that: - -- keep the standard public API: `DD_RUM.init(...)` and `DD_RUM.startView(...)` -- force manual view tracking -- start and update views automatically by polling `window.location.pathname` -- disable request collection, runtime error collection, and view metrics by design -- emit a single JavaScript bundle for Salesforce static resource loading - -The Salesforce wrapper should only load the bundle and initialize RUM once. The bundle emits the -initial view during `init()` and starts a lightweight pathname watcher to emit route-change views. diff --git a/packages/salesforce/package.json b/packages/salesforce/package.json deleted file mode 100644 index e1273e872c..0000000000 --- a/packages/salesforce/package.json +++ /dev/null @@ -1,45 +0,0 @@ -{ - "name": "@datadog/browser-rum-salesforce", - "version": "6.32.0", - "license": "Apache-2.0", - "main": "cjs/entries/main.js", - "module": "esm/entries/main.js", - "types": "cjs/entries/main.d.ts", - "files": [ - "bundle/**/*.js", - "cjs", - "esm", - "src", - "!src/**/*.spec.ts", - "!src/**/*.specHelper.ts" - ], - "scripts": { - "build": "node ../../scripts/build/build-package.ts --modules --bundle datadog-rum-salesforce.js --single-bundle", - "build:bundle": "node ../../scripts/build/build-package.ts --bundle datadog-rum-salesforce.js --single-bundle", - "prepack": "yarn build" - }, - "dependencies": { - "@datadog/browser-core": "6.32.0", - "@datadog/browser-rum": "6.32.0", - "@datadog/browser-rum-core": "6.32.0" - }, - "peerDependencies": { - "@datadog/browser-logs": "6.32.0" - }, - "peerDependenciesMeta": { - "@datadog/browser-logs": { - "optional": true - } - }, - "repository": { - "type": "git", - "url": "https://github.com/DataDog/browser-sdk.git", - "directory": "packages/salesforce" - }, - "volta": { - "extends": "../../package.json" - }, - "publishConfig": { - "access": "public" - } -} diff --git a/packages/salesforce/src/boot/salesforceInitConfiguration.ts b/packages/salesforce/src/boot/salesforceInitConfiguration.ts deleted file mode 100644 index a9bdd53de9..0000000000 --- a/packages/salesforce/src/boot/salesforceInitConfiguration.ts +++ /dev/null @@ -1,18 +0,0 @@ -import type { RumInitConfiguration } from '@datadog/browser-rum-core' - -const SALESFORCE_VIEW_TRACKING_DEFAULTS: Pick< - RumInitConfiguration, - 'trackViewsManually' | 'trackResources' | 'trackUserInteractions' | 'trackLongTasks' -> = { - trackViewsManually: true, - trackResources: false, - trackUserInteractions: false, - trackLongTasks: false, -} - -export function buildSalesforceInitConfiguration(initConfiguration: RumInitConfiguration): RumInitConfiguration { - return { - ...initConfiguration, - ...SALESFORCE_VIEW_TRACKING_DEFAULTS, - } -} diff --git a/packages/salesforce/typedoc.json b/packages/salesforce/typedoc.json deleted file mode 100644 index 002b26a53c..0000000000 --- a/packages/salesforce/typedoc.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "$schema": "https://typedoc.org/schema.json", - "entryPoints": ["src/entries/main.ts"] -} diff --git a/tsconfig.base.json b/tsconfig.base.json index 0b7affd51f..6add355941 100644 --- a/tsconfig.base.json +++ b/tsconfig.base.json @@ -25,8 +25,6 @@ "@datadog/browser-rum": ["./packages/rum/src/entries/main"], "@datadog/browser-rum/internal": ["./packages/rum/src/entries/internal"], "@datadog/browser-rum/internal-synthetics": ["./packages/rum/src/entries/internalSynthetics"], - "@datadog/browser-rum-salesforce": ["./packages/salesforce/src/entries/main"], - "@datadog/browser-rum-slim": ["./packages/rum-slim/src/entries/main"], "@datadog/browser-rum-react": ["./packages/rum-react/src/entries/main"], diff --git a/yarn.lock b/yarn.lock index 8bdccae3c9..887108dff0 100644 --- a/yarn.lock +++ b/yarn.lock @@ -436,21 +436,6 @@ __metadata: languageName: unknown linkType: soft -"@datadog/browser-rum-salesforce@workspace:packages/salesforce": - version: 0.0.0-use.local - resolution: "@datadog/browser-rum-salesforce@workspace:packages/salesforce" - dependencies: - "@datadog/browser-core": "npm:6.32.0" - "@datadog/browser-rum": "npm:6.32.0" - "@datadog/browser-rum-core": "npm:6.32.0" - peerDependencies: - "@datadog/browser-logs": 6.32.0 - peerDependenciesMeta: - "@datadog/browser-logs": - optional: true - languageName: unknown - linkType: soft - "@datadog/browser-rum-slim@workspace:packages/rum-slim": version: 0.0.0-use.local resolution: "@datadog/browser-rum-slim@workspace:packages/rum-slim" @@ -489,7 +474,7 @@ __metadata: languageName: unknown linkType: soft -"@datadog/browser-rum@npm:6.32.0, @datadog/browser-rum@workspace:*, @datadog/browser-rum@workspace:packages/rum": +"@datadog/browser-rum@workspace:*, @datadog/browser-rum@workspace:packages/rum": version: 0.0.0-use.local resolution: "@datadog/browser-rum@workspace:packages/rum" dependencies: