diff --git a/developer-extension/src/content-scripts/main.ts b/developer-extension/src/content-scripts/main.ts index 149261212d..cc6c2008d6 100644 --- a/developer-extension/src/content-scripts/main.ts +++ b/developer-extension/src/content-scripts/main.ts @@ -1,3 +1,5 @@ +import type { LogsInitConfiguration } from '@datadog/browser-logs' +import type { RumInitConfiguration } from '@datadog/browser-rum' import type { Settings } from '../common/extension.types' import { EventListeners } from '../common/eventListeners' import { DEV_LOGS_URL, DEV_RUM_SLIM_URL, DEV_RUM_URL } from '../common/packagesUrlConstants' @@ -102,15 +104,18 @@ function setDebug(global: GlobalInstrumentation) { }) } -function overrideInitConfiguration(global: GlobalInstrumentation, configurationOverride: object) { +function overrideInitConfiguration( + global: GlobalInstrumentation, + configurationOverride: Partial +) { 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) => { + sdkInstance.init = (config: RumInitConfiguration | LogsInitConfiguration) => { originalInit({ ...config, - ...configurationOverride, + ...restoreFunctions(config, configurationOverride), allowedTrackingOrigins: [location.origin], }) } @@ -118,6 +123,47 @@ function overrideInitConfiguration(global: GlobalInstrumentation, configurationO }) } +type SDKInitConfiguration = RumInitConfiguration | LogsInitConfiguration +function restoreFunctions( + original: SDKInitConfiguration, + override: Partial +): Partial { + // Clone the override to avoid mutating the input + const result = (Array.isArray(override) ? [...override] : { ...override }) as Record + + // Add back any missing functions from original + for (const key in original) { + if (!Object.prototype.hasOwnProperty.call(original, key)) { + continue + } + + const originalValue = original[key as keyof typeof original] + const resultValue = result[key] + + // If it's a function and missing in result, restore it + if (typeof originalValue === 'function' && !(key in result)) { + result[key] = originalValue + } + // If both are objects, recurse to restore functions at deeper levels + else if ( + key in result && + originalValue && + typeof originalValue === 'object' && + !Array.isArray(originalValue) && + resultValue && + typeof resultValue === 'object' && + !Array.isArray(resultValue) + ) { + result[key] = restoreFunctions( + originalValue as SDKInitConfiguration, + resultValue as Partial + ) + } + } + + return result as Partial +} + function loadSdkScriptFromURL(url: string) { const xhr = new XMLHttpRequest() try { diff --git a/developer-extension/src/panel/components/json.module.css b/developer-extension/src/panel/components/json.module.css index 883f0ac3c8..877b70d5bb 100644 --- a/developer-extension/src/panel/components/json.module.css +++ b/developer-extension/src/panel/components/json.module.css @@ -65,3 +65,23 @@ position: absolute; z-index: var(--dd-json-z-index); } + +.functionSource { + border-radius: 4px; + white-space: pre-wrap; + word-break: break-all; + text-indent: 0; +} + +.functionSourceToggle { + cursor: pointer; +} + +.functionSourceToggle svg { + vertical-align: middle; + margin-right: 4px; +} + +.functionSource > .functionSourceToggle { + margin-bottom: 8px; +} diff --git a/developer-extension/src/panel/components/json.tsx b/developer-extension/src/panel/components/json.tsx index 7b23d5150f..fdf28b2cf9 100644 --- a/developer-extension/src/panel/components/json.tsx +++ b/developer-extension/src/panel/components/json.tsx @@ -1,7 +1,7 @@ import type { BoxProps, MantineColor } from '@mantine/core' import { Box, Collapse, Menu, Text } from '@mantine/core' import { useColorScheme } from '@mantine/hooks' -import { IconCopy } from '@tabler/icons-react' +import { IconCopy, IconSearch } from '@tabler/icons-react' import type { ForwardedRef, ReactNode } from 'react' import React, { forwardRef, useContext, createContext, useState } from 'react' import { copy } from '../copy' @@ -14,6 +14,7 @@ interface JsonProps { defaultCollapseLevel?: number getMenuItemsForPath?: GetMenuItemsForPath formatValue?: FormatValue + onRevealFunctionLocation?: (descriptor: JsonValueDescriptor) => void } type GetMenuItemsForPath = (path: string, value: unknown) => ReactNode @@ -44,20 +45,23 @@ const JsonContext = createContext<{ defaultCollapseLevel: number getMenuItemsForPath?: GetMenuItemsForPath formatValue: FormatValue + onRevealFunctionLocation?: (descriptor: JsonValueDescriptor) => void } | null>(null) -type JsonValueDescriptor = +export type JsonValueDescriptor = | { parentType: 'root' value: unknown depth: 0 path: '' + evaluationPath: '' } | { parentType: 'array' parentValue: unknown[] value: unknown path: string + evaluationPath: string depth: number } | { @@ -65,6 +69,7 @@ type JsonValueDescriptor = parentValue: object value: unknown path: string + evaluationPath: string depth: number key: string } @@ -76,6 +81,7 @@ export const Json = forwardRef( defaultCollapseLevel = Infinity, formatValue = defaultFormatValue, getMenuItemsForPath, + onRevealFunctionLocation, ...boxProps }: JsonProps & BoxProps, ref: ForwardedRef @@ -89,13 +95,16 @@ export const Json = forwardRef( component={doesValueHasChildren(value) ? 'div' : 'span'} className={classes.root} > - + @@ -107,6 +116,72 @@ export function defaultFormatValue(_path: string, value: unknown) { return typeof value === 'number' ? formatNumber(value) : JSON.stringify(value) } +interface FunctionMetadata { + __type: 'function' + __name: string + __source?: string +} + +function isFunctionMetadata(value: unknown): value is FunctionMetadata { + return ( + typeof value === 'object' && + value !== null && + '__type' in value && + (value as any).__type === 'function' && + '__name' in value + ) +} + +function JsonFunctionValue({ descriptor, metadata }: { descriptor: JsonValueDescriptor; metadata: FunctionMetadata }) { + const [showSource, setShowSource] = useState(false) + const colorScheme = useColorScheme() + const { onRevealFunctionLocation } = useContext(JsonContext)! + + return ( + + + {``} + + {metadata.__source && ( + <> + setShowSource(!showSource)} + > + {showSource ? '▾ hide source' : '▸ show source'} + + + + {onRevealFunctionLocation && ( + onRevealFunctionLocation?.(descriptor)} + title="Log function to console to reveal source location" + > + + Reveal in console + + )} + {metadata.__source} + + + + )} + + ) +} + function JsonValue({ descriptor }: { descriptor: JsonValueDescriptor }) { const colorScheme = useColorScheme() const { formatValue } = useContext(JsonContext)! @@ -126,6 +201,7 @@ function JsonValue({ descriptor }: { descriptor: JsonValueDescriptor }) { parentValue: descriptor.value as unknown[], value: child, path: descriptor.path, + evaluationPath: descriptor.evaluationPath ? `${descriptor.evaluationPath}.${i}` : String(i), depth: descriptor.depth + 1, }} /> @@ -135,6 +211,11 @@ function JsonValue({ descriptor }: { descriptor: JsonValueDescriptor }) { } if (typeof descriptor.value === 'object' && descriptor.value !== null) { + // Check if this is a serialized function object + if (isFunctionMetadata(descriptor.value)) { + return + } + const entries = Object.entries(descriptor.value) if (entries.length === 0) { return @@ -150,6 +231,7 @@ function JsonValue({ descriptor }: { descriptor: JsonValueDescriptor }) { parentValue: descriptor.value as object, value: child, path: descriptor.path ? `${descriptor.path}.${key}` : key, + evaluationPath: descriptor.evaluationPath ? `${descriptor.evaluationPath}.${key}` : key, depth: descriptor.depth + 1, key, }} diff --git a/developer-extension/src/panel/components/tabs/infosTab.tsx b/developer-extension/src/panel/components/tabs/infosTab.tsx index 1a613e4c44..71d6ae1155 100644 --- a/developer-extension/src/panel/components/tabs/infosTab.tsx +++ b/developer-extension/src/panel/components/tabs/infosTab.tsx @@ -4,6 +4,7 @@ import React, { useState } from 'react' import { evalInWindow } from '../../evalInWindow' import { useSdkInfos } from '../../hooks/useSdkInfos' import { Columns } from '../columns' +import type { JsonValueDescriptor } from '../json' import { Json } from '../json' import { TabBase } from '../tabBase' import { createLogger } from '../../../common/logger' @@ -12,6 +13,61 @@ import { useSettings } from '../../hooks/useSettings' const logger = createLogger('infosTab') +function buildLogExpression(descriptor: JsonValueDescriptor, sdkType: 'rum' | 'logs'): string { + const evaluationPath = descriptor.evaluationPath + const sdkGlobal = sdkType === 'rum' ? 'DD_RUM' : 'DD_LOGS' + const sdkName = sdkType === 'rum' ? 'RUM' : 'Logs' + + return ` + (function() { + const config = window.${sdkGlobal}?.getInitConfiguration?.(); + if (!config) { + console.warn('[${sdkName}] SDK not found'); + return; + } + + // Navigate the path to get the value + let value = config; + const pathParts = '${evaluationPath}'.split('.'); + + for (const key of pathParts) { + if (!value || typeof value !== 'object') { + console.warn('[${sdkName}] Property not found at path: ${evaluationPath}'); + return; + } + + // Handle array indices (numeric keys) + if (Array.isArray(value)) { + const index = parseInt(key, 10); + if (isNaN(index) || index < 0 || index >= value.length) { + console.warn('[${sdkName}] Invalid array index at path: ${evaluationPath}'); + return; + } + value = value[index]; + } else { + if (!(key in value)) { + console.warn('[${sdkName}] Property not found at path: ${evaluationPath}'); + return; + } + value = value[key]; + } + } + + console.log('[${sdkName}] ${evaluationPath}:', value); + })() + ` +} + +function createRevealFunctionLocation(sdkType: 'rum' | 'logs') { + return (descriptor: JsonValueDescriptor) => { + const logExpression = buildLogExpression(descriptor, sdkType) + + evalInWindow(logExpression).catch((error) => { + logger.error('Failed to log function:', error) + }) + } +} + export function InfosTab() { const infos = useSdkInfos() const [settings, setSetting] = useSettings() @@ -83,6 +139,7 @@ export function InfosTab() { setSetting('rumConfigurationOverride', value) }} isOverridden={!!settings.rumConfigurationOverride} + onRevealFunctionLocation={createRevealFunctionLocation('rum')} /> @@ -114,6 +171,7 @@ export function InfosTab() { setSetting('logsConfigurationOverride', value) }} isOverridden={!!settings.logsConfigurationOverride} + onRevealFunctionLocation={createRevealFunctionLocation('logs')} /> @@ -150,11 +208,13 @@ function Entry({ value, isOverridden = false, onChange, + onRevealFunctionLocation, }: { name: string value: any isOverridden?: boolean onChange?: (value: object | null) => void + onRevealFunctionLocation?: (descriptor: JsonValueDescriptor) => void }) { const [edited, setEdited] = useState(false) const [newValue, setNewValue] = React.useState() @@ -220,7 +280,7 @@ function Entry({ )} {!edited ? ( - + ) : ( - key !== '' && !Array.isArray(val) && typeof val === 'object' && val && !Object.keys(val).length ? undefined : val + // replacer to remove function attributes that have been serialized with metadata by useSdkInfos() (ex: beforeSend) + const replacer = (key: string, val: unknown) => { + // Filter out function metadata objects + if (key !== '' && !Array.isArray(val) && typeof val === 'object' && val && (val as any).__type === 'function') { + return undefined + } + return val + } return JSON.stringify(value, replacer, 2) } diff --git a/developer-extension/src/panel/hooks/useSdkInfos.ts b/developer-extension/src/panel/hooks/useSdkInfos.ts index 096cd68bfc..bd88ced7ba 100644 --- a/developer-extension/src/panel/hooks/useSdkInfos.ts +++ b/developer-extension/src/panel/hooks/useSdkInfos.ts @@ -54,6 +54,20 @@ async function getInfos(): Promise { try { return (await evalInWindow( ` + // Helper to serialize objects while preserving function metadata + function serializeWithFunctions(obj) { + return JSON.parse(JSON.stringify(obj, function(key, value) { + if (typeof value === 'function') { + return { + __type: 'function', + __name: value.name || '(anonymous)', + __source: value.toString() + } + } + return value + })) + } + const cookieRawValue = document.cookie .split(';') .map(cookie => cookie.match(/(\\S*?)=(.*)/)?.slice(1) || []) @@ -65,14 +79,14 @@ async function getInfos(): Promise { ) const rum = window.DD_RUM && { version: window.DD_RUM?.version, - config: window.DD_RUM?.getInitConfiguration?.(), + config: serializeWithFunctions(window.DD_RUM?.getInitConfiguration?.()), internalContext: window.DD_RUM?.getInternalContext?.(), globalContext: window.DD_RUM?.getGlobalContext?.(), user: window.DD_RUM?.getUser?.(), } const logs = window.DD_LOGS && { version: window.DD_LOGS?.version, - config: window.DD_LOGS?.getInitConfiguration?.(), + config: serializeWithFunctions(window.DD_LOGS?.getInitConfiguration?.()), globalContext: window.DD_LOGS?.getGlobalContext?.(), user: window.DD_LOGS?.getUser?.(), }