Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

✨[Developer extension] npm setup override support #2304

Merged
merged 10 commits into from Jan 17, 2024
2 changes: 2 additions & 0 deletions developer-extension/src/common/constants.ts
Expand Up @@ -25,3 +25,5 @@ export const enum PanelTabs {
}

export const DEFAULT_PANEL_TAB = PanelTabs.Events

export const SESSION_STORAGE_SETTINGS_KEY = '__ddBrowserSdkExtensionSettings'
14 changes: 14 additions & 0 deletions developer-extension/src/common/types.ts
Expand Up @@ -40,3 +40,17 @@ export type SdkMessage =
segment: BrowserSegmentMetadata
}
}

export type EventCollectionStrategy = 'sdk' | 'requests'

export interface Settings {
useDevBundles: boolean
injectDevBundles: boolean
useRumSlim: boolean
blockIntakeRequests: boolean
autoFlush: boolean
preserveEvents: boolean
eventCollectionStrategy: EventCollectionStrategy
rumConfigurationOverride: object | null
logsConfigurationOverride: object | null
}
162 changes: 152 additions & 10 deletions developer-extension/src/content-scripts/main.ts
@@ -1,11 +1,153 @@
// This script is executed in the "main" execution world, the same world as the webpage. Thus, it
// can define a global callback variable to listen to SDK events.

;(window as any).__ddBrowserSdkExtensionCallback = (message: unknown) => {
// Relays any message to the "isolated" content-script via a custom event.
window.dispatchEvent(
new CustomEvent('__ddBrowserSdkMessage', {
detail: message,
})
)
import type { Settings } from '../common/types'
import { EventListeners } from '../common/eventListeners'
import { DEV_LOGS_URL, DEV_RUM_URL, SESSION_STORAGE_SETTINGS_KEY } from '../common/constants'

declare global {
interface Window extends EventTarget {
DD_RUM?: SdkPublicApi
DD_LOGS?: SdkPublicApi
__ddBrowserSdkExtensionCallback?: (message: unknown) => void
}
}

type SdkPublicApi = { [key: string]: (...args: any[]) => unknown }

function main() {
// Prevent multiple executions when the devetools are reconnecting
if (window.__ddBrowserSdkExtensionCallback) {
return
}

sendEventsToExtension()

const settings = getSettings()

if (
settings &&
// Avoid instrumenting SDK global variables if the SDKs are already loaded.
// This happens when the page is loaded and then the devtools are opened.
noBrowserSdkLoaded()
) {
const ddRumGlobal = instrumentGlobal('DD_RUM')
const ddLogsGlobal = instrumentGlobal('DD_LOGS')

if (settings.rumConfigurationOverride) {
overrideInitConfiguration(ddRumGlobal, settings.rumConfigurationOverride)
}

if (settings.logsConfigurationOverride) {
overrideInitConfiguration(ddLogsGlobal, settings.logsConfigurationOverride)
}

if (settings.injectDevBundles) {
injectDevBundle(DEV_RUM_URL, ddRumGlobal)
injectDevBundle(DEV_LOGS_URL, ddLogsGlobal)
}
}
}

main()

function sendEventsToExtension() {
// This script is executed in the "main" execution world, the same world as the webpage. Thus, it
// can define a global callback variable to listen to SDK events.
window.__ddBrowserSdkExtensionCallback = (message: unknown) => {
// Relays any message to the "isolated" content-script via a custom event.
window.dispatchEvent(
new CustomEvent('__ddBrowserSdkMessage', {
detail: message,
})
)
}
}

function getSettings() {
try {
// sessionStorage access throws in sandboxed iframes
const stringSettings = sessionStorage.getItem(SESSION_STORAGE_SETTINGS_KEY)
// JSON.parse throws if the stringSettings is not a valid JSON
return JSON.parse(stringSettings || 'null') as Settings
amortemousque marked this conversation as resolved.
Show resolved Hide resolved
} catch (error) {
// eslint-disable-next-line no-console
console.error('Error getting settings', error)
}
}

function noBrowserSdkLoaded() {
return !window.DD_RUM && !window.DD_LOGS
}

function injectDevBundle(url: string, global: GlobalInstrumentation) {
loadSdkScriptFromURL(url)
const devInstance = global.get() as SdkPublicApi

if (devInstance) {
global.onSet((sdkInstance) => proxySdk(sdkInstance, devInstance))
global.returnValue(devInstance)
}
}

function overrideInitConfiguration(global: GlobalInstrumentation, configurationOverride: object) {
global.onSet((sdkInstance) => {
// Ensure the sdkInstance has an 'init' method, excluding async stubs.
if ('init' in sdkInstance) {
const originalInit = sdkInstance.init
sdkInstance.init = (config: any) => {
originalInit({ ...config, ...configurationOverride })
}
}
})
}

function loadSdkScriptFromURL(url: string) {
const xhr = new XMLHttpRequest()
try {
xhr.open('GET', url, false) // `false` makes the request synchronous
xhr.send()
} catch (error) {
// eslint-disable-next-line no-console
console.error(`[DD Browser SDK extension] Error while loading ${url}:`, error)
return
}
if (xhr.status === 200) {
const script = document.createElement('script')
script.type = 'text/javascript'
script.text = xhr.responseText

document.documentElement.prepend(script)
}
}

type GlobalInstrumentation = ReturnType<typeof instrumentGlobal>
function instrumentGlobal(global: 'DD_RUM' | 'DD_LOGS') {
const eventListeners = new EventListeners<SdkPublicApi>()
let returnedInstance: SdkPublicApi | undefined
let lastInstance: SdkPublicApi | undefined
Object.defineProperty(window, global, {
set(sdkInstance: SdkPublicApi) {
eventListeners.notify(sdkInstance)
lastInstance = sdkInstance
},
get(): SdkPublicApi | undefined {
return returnedInstance ?? lastInstance
},
})

return {
get: () => window[global],
onSet: (callback: (sdkInstance: SdkPublicApi) => void) => {
eventListeners.subscribe(callback)
},
returnValue: (sdkInstance: SdkPublicApi) => {
returnedInstance = sdkInstance
},
}
}

function proxySdk(target: SdkPublicApi, root: SdkPublicApi) {
for (const key in root) {
if (Object.prototype.hasOwnProperty.call(root, key)) {
target[key] = root[key]
}
}
amortemousque marked this conversation as resolved.
Show resolved Hide resolved
}
19 changes: 16 additions & 3 deletions developer-extension/src/panel/components/panel.tsx
Expand Up @@ -5,9 +5,9 @@ import { datadogRum } from '@datadog/browser-rum'
import { useEvents } from '../hooks/useEvents'
import { useAutoFlushEvents } from '../hooks/useAutoFlushEvents'
import { useNetworkRules } from '../hooks/useNetworkRules'
import type { Settings } from '../hooks/useSettings'
import { useSettings } from '../hooks/useSettings'
import { DEFAULT_PANEL_TAB, PanelTabs } from '../../common/constants'
import type { Settings } from '../../common/types'
import { SettingsTab } from './tabs/settingsTab'
import { InfosTab } from './tabs/infosTab'
import { EventsTab, DEFAULT_COLUMNS } from './tabs/eventsTab'
Expand Down Expand Up @@ -35,7 +35,16 @@ export function Panel() {
<Tabs color="violet" value={activeTab} className={classes.tabs} onChange={updateActiveTab}>
<Tabs.List className="dd-privacy-allow">
<Tabs.Tab value={PanelTabs.Events}>Events</Tabs.Tab>
<Tabs.Tab value={PanelTabs.Infos}>
<Tabs.Tab
value={PanelTabs.Infos}
rightSection={
isOverridingInitConfiguration(settings) && (
<Text c="orange" fw="bold" title="Overriding init configuration">
</Text>
)
}
>
<Text>Infos</Text>
</Tabs.Tab>
<Tabs.Tab value={PanelTabs.Replay}>
Expand Down Expand Up @@ -79,5 +88,9 @@ export function Panel() {
}

function isInterceptingNetworkRequests(settings: Settings) {
return settings.blockIntakeRequests || settings.useDevBundles || settings.useRumSlim
return settings.blockIntakeRequests || settings.useDevBundles || settings.injectDevBundles || settings.useRumSlim
}

function isOverridingInitConfiguration(settings: Settings) {
return settings.rumConfigurationOverride || settings.logsConfigurationOverride
}