Skip to content

Commit d32f6ea

Browse files
Add initial Salesforce support.
1 parent 9b6698b commit d32f6ea

7 files changed

Lines changed: 302 additions & 9 deletions

File tree

packages/core/src/domain/report/reportObservable.ts

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -60,11 +60,16 @@ function createReportObservable(reportTypes: ReportType[]) {
6060

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

67-
return stop
69+
return stop
70+
} catch {
71+
return
72+
}
6873
})
6974
}
7075

packages/core/src/tools/instrumentMethod.ts

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -117,14 +117,23 @@ export function instrumentMethod<TARGET extends { [key: string]: any }, METHOD e
117117
return result
118118
}
119119

120-
targetPrototype[method] = instrumentation as TARGET[METHOD]
120+
// 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
121+
try {
122+
targetPrototype[method] = instrumentation as TARGET[METHOD]
123+
} catch {
124+
return { stop: noop }
125+
}
121126

122127
return {
123128
stop: () => {
124129
stopped = true
125130
// If the instrumentation has been removed by a third party, keep the last one
126131
if (targetPrototype[method] === instrumentation) {
127-
targetPrototype[method] = original
132+
try {
133+
targetPrototype[method] = original
134+
} catch {
135+
// Restore can be rejected by sandboxed runtimes; the instrumentation is already stopped.
136+
}
128137
}
129138
},
130139
}

packages/rum-core/src/domain/contexts/urlContexts.ts

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,8 +41,9 @@ export function startUrlContexts(
4141
let previousViewUrl: string | undefined
4242

4343
lifeCycle.subscribe(LifeCycleEventType.BEFORE_VIEW_CREATED, ({ startClocks, url }) => {
44-
const locationHref = mockable(location).href
44+
const locationHref = getLocationHref()
4545
const viewUrl = url !== undefined ? buildUrl(url, locationHref).href : locationHref
46+
4647
urlContextHistory.add(
4748
buildUrlContext({
4849
url: viewUrl,
@@ -103,3 +104,12 @@ export function startUrlContexts(
103104
},
104105
}
105106
}
107+
108+
// 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
109+
function getLocationHref() {
110+
try {
111+
return mockable(location).href
112+
} catch {
113+
return mockable(window.location).href
114+
}
115+
}

packages/rum-slim/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
"scripts": {
1717
"build": "node ../../scripts/build/build-package.ts --modules --bundle datadog-rum-slim.js",
1818
"build:bundle": "node ../../scripts/build/build-package.ts --bundle datadog-rum-slim.js",
19+
"build:salesforce": "node ../../scripts/build/build-package.ts --bundle datadog-rum-salesforce.js --entry ./src/entries/salesforce.ts",
1920
"prepack": "yarn build"
2021
},
2122
"dependencies": {
Lines changed: 170 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,170 @@
1+
import { buildUrl, instrumentMethod, noop, setTimeout } from '@datadog/browser-core'
2+
import type { RumPublicApi, ViewOptions } from '@datadog/browser-rum-core'
3+
4+
export interface SalesforceLocation {
5+
pathname?: string
6+
href?: string
7+
}
8+
9+
interface StartSalesforceViewNameTrackingOptions {
10+
getRumPublicApi: () => Pick<RumPublicApi, 'setViewName' | 'startView'> | undefined
11+
getLocation?: () => SalesforceLocation | undefined
12+
}
13+
14+
interface SalesforceView {
15+
key: string
16+
url?: string
17+
}
18+
19+
export function startSalesforceViewNameTracking(options: StartSalesforceViewNameTrackingOptions) {
20+
const getLocation = options.getLocation ?? getNavigationLocation
21+
const initialView = resolveCurrentView(getLocation())
22+
let lastViewKey = initialView?.key
23+
24+
if (initialView) {
25+
setCurrentViewName(initialView)
26+
}
27+
28+
const { stop: stopInstrumentingPushState } = instrumentMethod(
29+
getHistoryInstrumentationTarget('pushState'),
30+
'pushState',
31+
({ onPostCall }) => {
32+
onPostCall(scheduleSetCurrentViewName)
33+
}
34+
)
35+
const { stop: stopInstrumentingReplaceState } = instrumentMethod(
36+
getHistoryInstrumentationTarget('replaceState'),
37+
'replaceState',
38+
({ onPostCall }) => {
39+
onPostCall(scheduleSetCurrentViewName)
40+
}
41+
)
42+
43+
window.addEventListener('popstate', scheduleSetCurrentViewName)
44+
window.addEventListener('hashchange', scheduleSetCurrentViewName)
45+
window.addEventListener('click', scheduleLocationCheckAfterClick, true)
46+
47+
function scheduleLocationCheckAfterClick() {
48+
setTimeout(trackCurrentView, 0)
49+
setTimeout(trackCurrentView, 100)
50+
setTimeout(trackCurrentView, 500)
51+
}
52+
53+
function scheduleSetCurrentViewName() {
54+
setTimeout(trackCurrentView, 0)
55+
}
56+
57+
function trackCurrentView() {
58+
const currentView = resolveCurrentView(getLocation())
59+
60+
if (!currentView) {
61+
return
62+
}
63+
64+
if (!lastViewKey || currentView.key === lastViewKey) {
65+
setCurrentViewName(currentView)
66+
lastViewKey = currentView.key
67+
return
68+
}
69+
70+
options.getRumPublicApi()?.startView(toViewOptions(currentView))
71+
lastViewKey = currentView.key
72+
}
73+
74+
function setCurrentViewName(view: SalesforceView) {
75+
options.getRumPublicApi()?.setViewName(view.key)
76+
}
77+
78+
return {
79+
stop() {
80+
stopInstrumentingPushState()
81+
stopInstrumentingReplaceState()
82+
window.removeEventListener('popstate', scheduleSetCurrentViewName)
83+
window.removeEventListener('hashchange', scheduleSetCurrentViewName)
84+
window.removeEventListener('click', scheduleLocationCheckAfterClick, true)
85+
},
86+
}
87+
}
88+
89+
function getNavigationLocation(): SalesforceLocation | undefined {
90+
try {
91+
return {
92+
href: window.location.href,
93+
pathname: window.location.pathname,
94+
}
95+
} catch {
96+
return undefined
97+
}
98+
}
99+
100+
function resolveCurrentView(location: SalesforceLocation | undefined): SalesforceView | undefined {
101+
if (!location) {
102+
return undefined
103+
}
104+
105+
const url = normalizeLocationHref(location.href)
106+
const key = normalizePathname(location.pathname) ?? getPathnameFromHref(url)
107+
108+
if (!key) {
109+
return undefined
110+
}
111+
112+
return {
113+
key,
114+
url,
115+
}
116+
}
117+
118+
function toViewOptions(view: SalesforceView): ViewOptions {
119+
return view.url ? { name: view.key, url: view.url } : { name: view.key }
120+
}
121+
122+
function getPathnameFromHref(href: string | undefined) {
123+
if (!href) {
124+
return undefined
125+
}
126+
127+
try {
128+
return normalizePathname(buildUrl(href).pathname)
129+
} catch {
130+
return undefined
131+
}
132+
}
133+
134+
function normalizeLocationHref(href: unknown) {
135+
if (typeof href !== 'string' || !href.trim()) {
136+
return undefined
137+
}
138+
139+
try {
140+
return buildUrl(href).href
141+
} catch {
142+
return undefined
143+
}
144+
}
145+
146+
function normalizePathname(pathname: unknown) {
147+
if (typeof pathname !== 'string' || !pathname.trim()) {
148+
return undefined
149+
}
150+
151+
let normalizedPathname = pathname.trim()
152+
153+
if (!normalizedPathname.startsWith('/')) {
154+
normalizedPathname = `/${normalizedPathname}`
155+
}
156+
157+
if (normalizedPathname.length > 1) {
158+
normalizedPathname = normalizedPathname.replace(/\/+$/, '')
159+
}
160+
161+
return normalizedPathname || '/'
162+
}
163+
164+
function getHistoryInstrumentationTarget(methodName: 'pushState' | 'replaceState') {
165+
if (typeof History === 'undefined') {
166+
return { [methodName]: noop }
167+
}
168+
169+
return Object.prototype.hasOwnProperty.call(history, methodName) ? history : History.prototype
170+
}
Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
import { defineGlobal, getGlobalObject } from '@datadog/browser-core'
2+
import type { RumPublicApi } from '@datadog/browser-rum-core'
3+
import { makeRumPublicApi } from '@datadog/browser-rum-core'
4+
import { makeProfilerApiStub } from '../boot/stubProfilerApi'
5+
import { makeRecorderApiStub } from '../boot/stubRecorderApi'
6+
import { startSalesforceViewNameTracking } from '../domain/salesforce/viewNameTracker'
7+
8+
export type {
9+
User,
10+
Account,
11+
TraceContextInjection,
12+
SessionPersistence,
13+
TrackingConsent,
14+
MatchOption,
15+
ProxyFn,
16+
Site,
17+
Context,
18+
ContextValue,
19+
ContextArray,
20+
RumInternalContext,
21+
} from '@datadog/browser-core'
22+
23+
/**
24+
* @deprecated Use {@link DatadogRum} instead
25+
*/
26+
export type RumGlobal = RumPublicApi
27+
28+
export type {
29+
RumPublicApi as DatadogRum,
30+
RumInitConfiguration,
31+
RumBeforeSend,
32+
ViewOptions,
33+
StartRecordingOptions,
34+
AddDurationVitalOptions,
35+
DurationVitalOptions,
36+
FeatureOperationOptions,
37+
FailureReason,
38+
ActionOptions,
39+
ResourceOptions,
40+
ResourceStopOptions,
41+
TracingOption,
42+
RumPlugin,
43+
OnRumStartOptions,
44+
PropagatorType,
45+
FeatureFlagsForEvents,
46+
MatchHeader,
47+
CommonProperties,
48+
RumEvent,
49+
RumActionEvent,
50+
RumErrorEvent,
51+
RumLongTaskEvent,
52+
RumResourceEvent,
53+
RumViewEvent,
54+
RumVitalEvent,
55+
RumEventDomainContext,
56+
RumViewEventDomainContext,
57+
RumErrorEventDomainContext,
58+
RumActionEventDomainContext,
59+
RumVitalEventDomainContext,
60+
RumResourceEventDomainContext,
61+
RumLongTaskEventDomainContext,
62+
} from '@datadog/browser-rum-core'
63+
export { DEFAULT_TRACKED_RESOURCE_HEADERS } from '@datadog/browser-rum-core'
64+
export { DefaultPrivacyLevel } from '@datadog/browser-core'
65+
66+
export const datadogRum = createSalesforceDatadogRum(
67+
makeRumPublicApi(makeRecorderApiStub(), makeProfilerApiStub(), {
68+
sdkName: 'rum-slim',
69+
})
70+
)
71+
72+
interface BrowserWindow extends Window {
73+
DD_RUM?: RumPublicApi
74+
}
75+
76+
defineGlobal(getGlobalObject<BrowserWindow>(), 'DD_RUM', datadogRum)
77+
78+
function createSalesforceDatadogRum(baseRum: RumPublicApi): RumPublicApi {
79+
const baseInit = baseRum.init
80+
let stopSalesforceViewNameTracking: (() => void) | undefined
81+
82+
baseRum.init = (initConfiguration) => {
83+
baseInit(initConfiguration)
84+
85+
if (!stopSalesforceViewNameTracking) {
86+
const salesforceViewNameTracking = startSalesforceViewNameTracking({
87+
getRumPublicApi: () => baseRum,
88+
})
89+
stopSalesforceViewNameTracking = () => salesforceViewNameTracking.stop()
90+
}
91+
}
92+
93+
return baseRum
94+
}

scripts/build/build-package.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,9 @@ runMain(async () => {
1919
bundle: {
2020
type: 'string',
2121
},
22+
entry: {
23+
type: 'string',
24+
},
2225
verbose: {
2326
type: 'boolean',
2427
default: false,
@@ -43,6 +46,7 @@ runMain(async () => {
4346
if (values.bundle) {
4447
printLog('Building bundle...')
4548
await buildBundle({
49+
entry: values.entry ?? './src/entries/main.ts',
4650
filename: values.bundle,
4751
verbose: values.verbose,
4852
})
@@ -51,13 +55,13 @@ runMain(async () => {
5155
printLog('Done.')
5256
})
5357

54-
async function buildBundle({ filename, verbose }: { filename: string; verbose: boolean }) {
58+
async function buildBundle({ entry, filename, verbose }: { entry: string; filename: string; verbose: boolean }) {
5559
await fs.rm('./bundle', { recursive: true, force: true })
5660
return new Promise<void>((resolve, reject) => {
5761
webpack(
5862
webpackBase({
5963
mode: 'production',
60-
entry: './src/entries/main.ts',
64+
entry,
6165
filename,
6266
}),
6367
(error, stats) => {

0 commit comments

Comments
 (0)