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 improvements #2516

Merged
merged 18 commits into from
Dec 7, 2023
Merged
Show file tree
Hide file tree
Changes from 16 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
1 change: 1 addition & 0 deletions LICENSE-3rdparty.csv
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ file,tracekit,MIT,Copyright 2013 Onur Can Cakmak and all TraceKit contributors
file,web-vitals,Apache-2.0,Copyright 2020 Google LLC
prod,@mantine/core,MIT,Copyright (c) 2021 Vitaly Rtishchev
prod,@mantine/hooks,MIT,Copyright (c) 2021 Vitaly Rtishchev
prod,@tabler/icons-react,MIT,Copyright (c) 2020-2023 Paweł Kuna
prod,clsx,MIT,Copyright (c) Luke Edwards <[email protected]> (lukeed.com)
prod,react,MIT,Copyright (c) Facebook, Inc. and its affiliates.
prod,react-dom,MIT,Copyright (c) Facebook, Inc. and its affiliates.
Expand Down
2 changes: 2 additions & 0 deletions developer-extension/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
"dev": "webpack --mode development --watch"
},
"devDependencies": {
"@tabler/icons-react": "2.42.0",
"@types/chrome": "0.0.251",
"@types/react": "18.2.37",
"@types/react-dom": "18.2.15",
Expand All @@ -18,6 +19,7 @@
"webpack": "5.89.0"
},
"dependencies": {
"@datadog/browser-core": "workspace:*",
"@datadog/browser-logs": "workspace:*",
"@datadog/browser-rum": "workspace:*",
"@mantine/core": "7.2.2",
Expand Down
2 changes: 2 additions & 0 deletions developer-extension/src/panel/components/json.module.css
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@

.valueChildrenIndent {
position: absolute;
z-index: var(--dd-json-z-index);
left: 0px;
width: var(--indent);

Expand Down Expand Up @@ -62,4 +63,5 @@

.jsonLine[data-floating] {
position: absolute;
z-index: var(--dd-json-z-index);
}
13 changes: 3 additions & 10 deletions developer-extension/src/panel/components/json.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
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 type { ForwardedRef, ReactNode } from 'react'
import React, { forwardRef, useContext, createContext, useState } from 'react'
import { copy } from '../copy'
import { formatNumber } from '../formatNumber'

import classes from './json.module.css'
Expand Down Expand Up @@ -314,22 +316,13 @@ function CopyMenuItem({ value, children }: { value: unknown; children: ReactNode
onClick={() => {
copy(JSON.stringify(value, null, 2))
}}
leftSection={<IconCopy size={14} />}
>
{children}
</Menu.Item>
)
}

function copy(text: string) {
// Unfortunately, navigator.clipboard.writeText does not seem to work in extensions
const container = document.createElement('textarea')
container.innerHTML = text
document.body.appendChild(container)
container.select()
document.execCommand('copy')
document.body.removeChild(container)
}

function doesValueHasChildren(value: unknown) {
if (Array.isArray(value)) {
return value.length > 0
Expand Down
5 changes: 5 additions & 0 deletions developer-extension/src/panel/components/tabBase.module.css
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,13 @@
.contentContainer {
flex: 1;
overflow-y: auto;
overflow-x: hidden;
padding: 0;
margin: 0;

scroll-timeline:
--dd-tab-scroll-timeline,
--dd-tab-scroll-x-timeline x;
}

.horizontalContainer {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
.addFieldColumn {
display: flex;
align-items: flex-end;
gab: var(--mantine-spacing-md);
}

.addFieldAutocomplete {
flex: 1;
}

.addFilterAutocompleteHighlight {
text-decoration: underline;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
import type { OptionsFilter } from '@mantine/core'
import { Autocomplete, Button, Flex, Popover, Text } from '@mantine/core'
import { IconColumnInsertRight } from '@tabler/icons-react'
import React, { useMemo, useState } from 'react'
import type { FacetRegistry } from '../../../hooks/useEvents'
import type { EventListColumn } from './columnUtils'
import { getColumnTitle, includesColumn, DEFAULT_COLUMNS } from './columnUtils'
import { RowButton } from './rowButton'
import classes from './addColumnPopover.module.css'

export function AddColumnPopover({
columns,
onColumnsChange,
facetRegistry,
}: {
columns: EventListColumn[]
onColumnsChange: (columns: EventListColumn[]) => void
facetRegistry: FacetRegistry
}) {
return (
<Popover width={300} trapFocus position="bottom" withArrow shadow="md">
<Popover.Target>
<RowButton title="Add column" icon={IconColumnInsertRight} />
</Popover.Target>
<Popover.Dropdown>
<Flex direction="column" gap="sm">
{DEFAULT_COLUMNS.map((column) => (
<AddDefaultColumnButton
key={column.type}
column={column}
columns={columns}
onColumnsChange={onColumnsChange}
/>
))}
<AddFieldColumn columns={columns} onColumnsChange={onColumnsChange} facetRegistry={facetRegistry} />
</Flex>
</Popover.Dropdown>
</Popover>
)
}

function AddDefaultColumnButton({
column,
columns,
onColumnsChange,
}: {
column: EventListColumn
columns: EventListColumn[]
onColumnsChange: (columns: EventListColumn[]) => void
}) {
if (includesColumn(columns, column)) {
return null
}
return (
<Flex justify="space-between" align="center" gap="sm">
<Text>{getColumnTitle(column)}</Text>
<Button
onClick={() => {
onColumnsChange(columns.concat(column))
}}
>
Add
</Button>
</Flex>
)
}

function AddFieldColumn({
columns,
onColumnsChange,
facetRegistry,
}: {
columns: EventListColumn[]
onColumnsChange: (columns: EventListColumn[]) => void
facetRegistry: FacetRegistry
}) {
const [input, setInput] = useState('')

function addFieldColumn(path: string) {
const newColumn: EventListColumn = { path, type: 'field' }
if (!includesColumn(columns, newColumn)) {
onColumnsChange(columns.concat(newColumn))
}
}

const allPaths = useMemo(
() =>
Array.from(facetRegistry.getAllFieldPaths()).sort((a, b) => {
// Sort private fields last
if (a.startsWith('_dd') !== b.startsWith('_dd')) {
if (a.startsWith('_dd')) {
return 1
}
if (b.startsWith('_dd')) {
return -1
}
}
return a < b ? -1 : 1
}),
[]
)

return (
<form
onSubmit={(event) => {
event.preventDefault()
addFieldColumn(input)
}}
className={classes.addFieldColumn}
>
<Autocomplete
className={classes.addFieldAutocomplete}
value={input}
label="Field"
onChange={setInput}
data={allPaths}
filter={filterColumns}
placeholder="foo.bar"
onOptionSubmit={addFieldColumn}
/>
<Button type="submit">Add</Button>
</form>
)
}

function filterColumns(filterOptions: Parameters<OptionsFilter>[0]): ReturnType<OptionsFilter> {
if (!filterOptions.search) {
return filterOptions.options
}
const filteredOptions = filterOptions.options.flatMap((option) => {
if (!('value' in option)) {
return []
}

const inputIndex = option.value.indexOf(filterOptions.search)
if (inputIndex < 0) {
return []
}

return [
{
value: option.value,
label: (
<span>
{option.value.slice(0, inputIndex)}
<span className={classes.addFilterAutocompleteHighlight}>
{option.value.slice(inputIndex, inputIndex + filterOptions.search.length)}
</span>
{option.value.slice(inputIndex + filterOptions.search.length)}
</span>
) as unknown as string,
// Mantime types expect a string as label, but to support highlighting we need to return a
// ReactNode. This is the simplest way to achieve this, but it might break in the future
},
]
})
return filteredOptions
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@
--vertical-padding: 7px;

position: fixed;

z-index: var(--dd-column-drag-ghost-z-index);

opacity: 0.5;
border-radius: var(--mantine-radius-sm);
top: var(--drag-target-top);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
import type { TelemetryEvent } from '../../../../../../packages/core/src/domain/telemetry'
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

praise:
kudos 🎉

import type { LogsEvent } from '../../../../../../packages/logs/src/logsEvent.types'
import type { RumEvent } from '../../../../../../packages/rum-core/src/rumEvent.types'
import { getIntakeUrlForEvent, escapeShellParameter } from './copyEvent'

const RUM_ERROR_EVENT = { type: 'error' } as RumEvent
const TELEMETRY_EVENT = {
type: 'telemetry',
telemetry: { type: 'log' },
} as TelemetryEvent
const LOG_EVENT = {
status: 'info',
} as LogsEvent

describe('getIntakeUrlForEvent', () => {
it('should return undefined when RUM is not present', () => {
expect(getIntakeUrlForEvent({} as any, RUM_ERROR_EVENT)).toBeUndefined()
})

it('should return undefined when no RUM config', () => {
expect(getIntakeUrlForEvent({ rum: {} } as any, RUM_ERROR_EVENT)).toBeUndefined()
})

it('should return undefined when no RUM version', () => {
expect(getIntakeUrlForEvent({ rum: { config: {} } } as any, RUM_ERROR_EVENT)).toBeUndefined()
})

it('should return the URL with the right parameters', () => {
const url = new URL(
getIntakeUrlForEvent(
{
rum: {
config: {
clientToken: 'client-token',
},
version: '1.2.3',
},
} as any,
RUM_ERROR_EVENT
)!
)

expect(url.host).toBe('browser-intake-datadoghq.com')
expect(url.pathname).toBe('/api/v2/rum')
expect(url.searchParams.get('ddsource')).toBe('browser')
expect(url.searchParams.get('ddtags')).toBe('sdk_version:1.2.3,api:manual')
expect(url.searchParams.get('dd-api-key')).toBe('client-token')
expect(url.searchParams.get('dd-evp-origin-version')).toBe('1.2.3')
expect(url.searchParams.get('dd-evp-origin')).toBe('browser')
expect(url.searchParams.get('dd-request-id')).toMatch(/[a-f0-9-]+/)
expect(url.searchParams.get('batch_time')).toMatch(/[0-9]+/)
})

it('should escape the version URL parameter', () => {
const url = new URL(
getIntakeUrlForEvent(
{
rum: {
config: {
clientToken: 'client-token',
},
version: '1.2.3&4',
},
} as any,
RUM_ERROR_EVENT
)!
)

expect(url.searchParams.get('ddtags')).toBe('sdk_version:1.2.3&4,api:manual')
expect(url.searchParams.get('dd-evp-origin-version')).toBe('1.2.3&4')
})

it('should use the RUM intake for telemetry events', () => {
const url = new URL(
getIntakeUrlForEvent(
{
rum: {
config: {
clientToken: 'client-token',
},
version: '1.2.3',
},
} as any,
TELEMETRY_EVENT
)!
)

expect(url.pathname).toBe('/api/v2/rum')
})

it('should use the Logs intake for Log events', () => {
const url = new URL(
getIntakeUrlForEvent(
{
logs: {
config: {
clientToken: 'client-token',
},
version: '1.2.3',
},
} as any,
LOG_EVENT
)!
)

expect(url.pathname).toBe('/api/v2/logs')
})
})

describe('escapeShellParameter', () => {
it('should escape simple strings', () => {
expect(escapeShellParameter('foo bar')).toBe("$'foo bar'")
})

it('should escape backslashes', () => {
expect(escapeShellParameter('foo\\bar')).toBe("$'foo\\\\bar'")
})

it('should escape single quotes', () => {
expect(escapeShellParameter("foo'bar")).toBe("$'foo\\'bar'")
})
})
Loading