Skip to content
Closed
Show file tree
Hide file tree
Changes from 3 commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
d32f6ea
Add initial Salesforce support.
BeltranBulbarellaDD May 26, 2026
c02e9bf
Guard session
BeltranBulbarellaDD May 26, 2026
2fcb2ee
Linting
BeltranBulbarellaDD May 26, 2026
0d952b5
Add Resources and APM support
BeltranBulbarellaDD May 27, 2026
d6d525e
Make getGlobalLocationHref mockable, patch plain usage of location
BeltranBulbarellaDD May 27, 2026
363201b
use wire
BeltranBulbarellaDD May 29, 2026
2655dc5
Merge branch 'main' into beltran.bulbarella/salesforce_0
BeltranBulbarellaDD May 29, 2026
6a2eb32
Refactor for comments
BeltranBulbarellaDD May 29, 2026
059c321
Merge branch 'main' into beltran.bulbarella/salesforce_0
BeltranBulbarellaDD May 29, 2026
f7581c2
Fix bundle
BeltranBulbarellaDD May 29, 2026
783c96b
Use globalObject in turn of window
BeltranBulbarellaDD May 29, 2026
a8474b6
Add isCSPEventSupported function
BeltranBulbarellaDD May 29, 2026
59232c7
Create isEventSupported and reuse it
BeltranBulbarellaDD May 29, 2026
4f0711a
linter
BeltranBulbarellaDD May 29, 2026
721c1a2
Remove config parameter from isEventSupported
BeltranBulbarellaDD May 29, 2026
41a66d7
Remove views plugin and try catch debug
BeltranBulbarellaDD May 29, 2026
045bd3c
linter
BeltranBulbarellaDD May 29, 2026
3293b46
Fix compat
BeltranBulbarellaDD May 29, 2026
1f4c882
Revert changes to instrumentMethod.ts
BeltranBulbarellaDD Jun 3, 2026
a52f0d5
Move view tracking to bundle, rename bundle
BeltranBulbarellaDD Jun 3, 2026
a56d75f
Merge branch 'main' into beltran.bulbarella/salesforce_0
BeltranBulbarellaDD Jun 3, 2026
ff6a526
Move SF packages, add README
BeltranBulbarellaDD Jun 3, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 9 additions & 4 deletions packages/core/src/domain/report/reportObservable.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,11 +60,16 @@ function createReportObservable(reportTypes: ReportType[]) {

function createCspViolationReportObservable(configuration: Configuration) {
return new Observable<RawReportError>((observable) => {
const { stop } = addEventListener(configuration, document, DOM_EVENT.SECURITY_POLICY_VIOLATION, (event) => {
observable.notify(buildRawReportErrorFromCspViolation(event))
})
// Salesforce does not allow to add a securitypolicyviolation event listener. https://developer.salesforce.com/tools/lws-distortion-viewer
try {
const { stop } = addEventListener(configuration, document, DOM_EVENT.SECURITY_POLICY_VIOLATION, (event) => {
observable.notify(buildRawReportErrorFromCspViolation(event))
})

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

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { toSessionString, toSessionState } from '../sessionState'
import { Observable } from '../../../tools/observable'
import { mockable } from '../../../tools/mockable'
import { monitorError } from '../../../tools/monitor'
import { noop } from '../../../tools/utils/functionUtils'
import type { CookieAccess } from '../../../browser/cookieAccess'
import {
areCookiesAuthorized,
Expand All @@ -16,9 +17,8 @@ import {
} from '../../../browser/cookieAccess'
import { timeStampNow, dateNow } from '../../../tools/utils/timeUtils'
import { addTelemetryError } from '../../telemetry'

const LOCK_QUERY_TIMEOUT = 1000
import type { CookieStoreWindow } from '../../../browser/browser.types'
import { addEventListener, DOM_EVENT } from '../../../browser/addEventListener'
import { getLifecycleContext } from '../../../browser/lifecycleTracker'
import { clearTimeout, setTimeout } from '../../../tools/timer'
import type { Context } from '../../../tools/serialisation/context'
Expand All @@ -32,6 +32,7 @@ import type {
} from './sessionStoreStrategy'

const SESSION_COOKIE_VERSION = 0
const LOCK_QUERY_TIMEOUT = 1000

export async function selectCookieStrategy(
configuration: Configuration
Expand All @@ -42,7 +43,7 @@ export async function selectCookieStrategy(
}

if (
mockable((window as CookieStoreWindow).cookieStore) &&
canUseCookieStoreStrategy(configuration) &&
(await areCookiesAuthorized(createCookieStoreAccess, cookieOptions, configuration))
) {
return { type: SessionPersistence.COOKIE, cookieOptions, cookieApi: CookieApi.COOKIE_STORE }
Expand Down Expand Up @@ -119,6 +120,20 @@ export function initCookieStrategy(
}
}

function canUseCookieStoreStrategy(configuration: Configuration): boolean {
const cookieStore = mockable((window as CookieStoreWindow).cookieStore)
if (!cookieStore) {
return false
}
try {
const { stop } = addEventListener(configuration, cookieStore, DOM_EVENT.CHANGE, noop)
stop()
return true
} catch {
return false
}
}

interface LockQuerySnapshot {
heldByOthers: number
pendingCount: number
Expand Down
13 changes: 11 additions & 2 deletions packages/core/src/tools/instrumentMethod.ts
Original file line number Diff line number Diff line change
Expand Up @@ -117,14 +117,23 @@ export function instrumentMethod<TARGET extends { [key: string]: any }, METHOD e
return result
}

targetPrototype[method] = instrumentation as TARGET[METHOD]
// Salesforce makes History.prototype.pushState and History.prototype.replaceState readonly. https://help.salesforce.com/s/articleView?id=release-notes.rn_lws_distortions_added.htm&release=238&type=5
try {
targetPrototype[method] = instrumentation as TARGET[METHOD]
} catch {
return { stop: noop }
}

return {
stop: () => {
stopped = true
// If the instrumentation has been removed by a third party, keep the last one
if (targetPrototype[method] === instrumentation) {
targetPrototype[method] = original
try {
targetPrototype[method] = original
} catch {
// Restore can be rejected by sandboxed runtimes; the instrumentation is already stopped.
}
}
},
}
Expand Down
21 changes: 13 additions & 8 deletions packages/rum-core/src/browser/cookieObservable.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,11 +27,14 @@ 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) => {
// Lightning Web Security does not support the change event of CookieStore objects. https://developer.salesforce.com/tools/lws-distortion-viewer
const cookieStore = (window as CookieStoreWindow).cookieStore
if (!cookieStore) {
return watchCookieFallback(cookieName, callback)
}

try {
const listener = addEventListener(configuration, 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 =
Expand All @@ -40,9 +43,11 @@ function listenToCookieStoreChange(configuration: Configuration) {
if (changeEvent) {
callback(changeEvent.value)
}
}
)
return listener.stop
})
return listener.stop
} catch {
return watchCookieFallback(cookieName, callback)
}
}
}

Expand Down
12 changes: 11 additions & 1 deletion packages/rum-core/src/domain/contexts/urlContexts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,8 +41,9 @@ 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({
url: viewUrl,
Expand Down Expand Up @@ -103,3 +104,12 @@ export function startUrlContexts(
},
}
}

// Preserves existing global location behavior but in Salesforce access through explicit window as documented: https://developer.salesforce.com/docs/platform/lightning-components-security/guide/lws-limitations.html#:~:text=The%20location%20property,window.location
function getLocationHref() {
try {
return mockable(location).href
} catch {
return mockable(window.location).href
}
}
1 change: 1 addition & 0 deletions packages/rum-slim/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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-salesforce.js --entry ./src/entries/salesforce.ts",
"prepack": "yarn build"
},
"dependencies": {
Expand Down
187 changes: 187 additions & 0 deletions packages/rum-slim/src/domain/salesforce/viewNameTracker.ts
Comment thread
BeltranBulbarellaDD marked this conversation as resolved.
Outdated
Original file line number Diff line number Diff line change
@@ -0,0 +1,187 @@
import { addEventListener, buildUrl, DOM_EVENT, instrumentMethod, noop, setTimeout } from '@datadog/browser-core'
import type { RumPublicApi, ViewOptions } from '@datadog/browser-rum-core'

export interface SalesforceLocation {
pathname?: string
href?: string
}

interface StartSalesforceViewNameTrackingOptions {
getRumPublicApi: () => Pick<RumPublicApi, 'setViewName' | 'startView'> | undefined
getLocation?: () => SalesforceLocation | undefined
}

interface SalesforceView {
key: string
url?: string
}

export function startSalesforceViewNameTracking(options: StartSalesforceViewNameTrackingOptions) {
const getLocation = options.getLocation ?? getNavigationLocation
const initialView = resolveCurrentView(getLocation())
let lastViewKey = initialView?.key
const eventListenerConfiguration = { allowUntrustedEvents: true }

if (initialView) {
setCurrentViewName(initialView)
}

const { stop: stopInstrumentingPushState } = instrumentMethod(
getHistoryInstrumentationTarget('pushState'),
'pushState',
({ onPostCall }) => {
onPostCall(scheduleSetCurrentViewName)
}
)
const { stop: stopInstrumentingReplaceState } = instrumentMethod(
getHistoryInstrumentationTarget('replaceState'),
'replaceState',
({ onPostCall }) => {
onPostCall(scheduleSetCurrentViewName)
}
)

const { stop: stopListeningPopState } = addEventListener(
eventListenerConfiguration,
window,
DOM_EVENT.POP_STATE,
scheduleSetCurrentViewName
)
const { stop: stopListeningHashChange } = addEventListener(
eventListenerConfiguration,
window,
DOM_EVENT.HASH_CHANGE,
scheduleSetCurrentViewName
)
const { stop: stopListeningClick } = addEventListener(
eventListenerConfiguration,
window,
DOM_EVENT.CLICK,
scheduleLocationCheckAfterClick,
{ capture: true }
)

function scheduleLocationCheckAfterClick() {
setTimeout(trackCurrentView, 0)
setTimeout(trackCurrentView, 100)
setTimeout(trackCurrentView, 500)
}

function scheduleSetCurrentViewName() {
setTimeout(trackCurrentView, 0)
}

function trackCurrentView() {
const currentView = resolveCurrentView(getLocation())

if (!currentView) {
return
}

if (!lastViewKey || currentView.key === lastViewKey) {
setCurrentViewName(currentView)
lastViewKey = currentView.key
return
}

options.getRumPublicApi()?.startView(toViewOptions(currentView))
lastViewKey = currentView.key
}

function setCurrentViewName(view: SalesforceView) {
options.getRumPublicApi()?.setViewName(view.key)
}

return {
stop() {
stopInstrumentingPushState()
stopInstrumentingReplaceState()
stopListeningPopState()
stopListeningHashChange()
stopListeningClick()
},
}
}

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(buildUrl(href).pathname)
} catch {
return undefined
}
}

function normalizeLocationHref(href: unknown) {
if (typeof href !== 'string' || !href.trim()) {
return undefined
}

try {
return buildUrl(href).href
} 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 getHistoryInstrumentationTarget(methodName: 'pushState' | 'replaceState') {
if (typeof History === 'undefined') {
return { [methodName]: noop }
}

return Object.prototype.hasOwnProperty.call(history, methodName) ? history : History.prototype
}
Loading
Loading