Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
52 changes: 49 additions & 3 deletions developer-extension/src/content-scripts/main.ts
Original file line number Diff line number Diff line change
@@ -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'
Expand Down Expand Up @@ -102,22 +104,66 @@ function setDebug(global: GlobalInstrumentation) {
})
}

function overrideInitConfiguration(global: GlobalInstrumentation, configurationOverride: object) {
function overrideInitConfiguration(
global: GlobalInstrumentation,
configurationOverride: Partial<RumInitConfiguration | LogsInitConfiguration>
) {
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],
})
}
}
})
}

type SDKInitConfiguration = RumInitConfiguration | LogsInitConfiguration
function restoreFunctions(
original: SDKInitConfiguration,
override: Partial<SDKInitConfiguration>
): Partial<SDKInitConfiguration> {
// Clone the override to avoid mutating the input
const result = (Array.isArray(override) ? [...override] : { ...override }) as Record<string, unknown>

// 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<SDKInitConfiguration>
)
}
}

return result as Partial<SDKInitConfiguration>
}

function loadSdkScriptFromURL(url: string) {
const xhr = new XMLHttpRequest()
try {
Expand Down
20 changes: 20 additions & 0 deletions developer-extension/src/panel/components/json.module.css
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
88 changes: 85 additions & 3 deletions developer-extension/src/panel/components/json.tsx
Original file line number Diff line number Diff line change
@@ -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'
Expand All @@ -14,6 +14,7 @@ interface JsonProps {
defaultCollapseLevel?: number
getMenuItemsForPath?: GetMenuItemsForPath
formatValue?: FormatValue
onRevealFunctionLocation?: (descriptor: JsonValueDescriptor) => void
}

type GetMenuItemsForPath = (path: string, value: unknown) => ReactNode
Expand Down Expand Up @@ -44,27 +45,31 @@ 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
}
| {
parentType: 'object'
parentValue: object
value: unknown
path: string
evaluationPath: string
depth: number
key: string
}
Expand All @@ -76,6 +81,7 @@ export const Json = forwardRef(
defaultCollapseLevel = Infinity,
formatValue = defaultFormatValue,
getMenuItemsForPath,
onRevealFunctionLocation,
...boxProps
}: JsonProps & BoxProps,
ref: ForwardedRef<HTMLDivElement | HTMLSpanElement>
Expand All @@ -89,13 +95,16 @@ export const Json = forwardRef(
component={doesValueHasChildren(value) ? 'div' : 'span'}
className={classes.root}
>
<JsonContext.Provider value={{ defaultCollapseLevel, getMenuItemsForPath, formatValue }}>
<JsonContext.Provider
value={{ defaultCollapseLevel, getMenuItemsForPath, formatValue, onRevealFunctionLocation }}
>
<JsonValue
descriptor={{
parentType: 'root',
value,
depth: 0,
path: '',
evaluationPath: '',
}}
/>
</JsonContext.Provider>
Expand All @@ -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 (
<JsonLine descriptor={descriptor}>
<JsonText color="grape" descriptor={descriptor}>
<Text component="span">{`<function: ${metadata.__name}>`}</Text>
</JsonText>
{metadata.__source && (
<>
<Text
component="span"
size="xs"
c="dimmed"
ml="xs"
className={classes.functionSourceToggle}
onClick={() => setShowSource(!showSource)}
>
{showSource ? '▾ hide source' : '▸ show source'}
</Text>
<Collapse in={showSource}>
<Box
p="xs"
bg={`gray.${colorScheme === 'dark' ? 8 - descriptor.depth : descriptor.depth + 1}`}
className={classes.functionSource}
>
{onRevealFunctionLocation && (
<Text
component="div"
size="xs"
c="blue"
className={classes.functionSourceToggle}
onClick={() => onRevealFunctionLocation?.(descriptor)}
title="Log function to console to reveal source location"
>
<IconSearch size={12} />
Reveal in console
</Text>
)}
{metadata.__source}
</Box>
</Collapse>
</>
)}
</JsonLine>
)
}

function JsonValue({ descriptor }: { descriptor: JsonValueDescriptor }) {
const colorScheme = useColorScheme()
const { formatValue } = useContext(JsonContext)!
Expand All @@ -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,
}}
/>
Expand All @@ -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 <JsonFunctionValue descriptor={descriptor} metadata={descriptor.value} />
}

const entries = Object.entries(descriptor.value)
if (entries.length === 0) {
return <JsonEmptyValue label="{empty object}" descriptor={descriptor} />
Expand All @@ -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,
}}
Expand Down
Loading