Skip to content

Commit e739e7e

Browse files
authored
Add Live Debugger package (#4449)
Introduce the `@datadog/browser-debugger` package, which provides Live Debugger support in browser applications. Frontend developers can attach log probes to running applications, evaluate conditions, and inspect runtime state without redeploying or modifying source code. The package is published privately for now (excluded from generated docs via `typedoc.json`). The probe execution pipeline lives entirely on the client: instrumented code calls into hooks exposed on `globalThis` (`$dd_entry`, `$dd_return`, `$dd_throw`, `$dd_probes`), and the SDK is responsible for evaluating probe conditions, capturing snapshots of arguments, locals, return values, and thrown errors, rendering template messages against runtime context, applying per-probe and global rate limits, and shipping the resulting events to the logs intake. To keep that hot path predictable, expressions, conditions, and message templates are compiled once at probe registration and the capture logic enforces explicit depth, collection-size, and string-length bounds. Probes themselves are pulled from a Delivery API via a cursor-based polling client, so probe lifecycle (add/remove/clear) is driven by the backend rather than the page. Event delivery reuses `@datadog/browser-core`'s batch/flush transport infrastructure, which required a small core change: the transport layer now accepts `'dd_debugger'` as a URL-only source override so events route correctly in the intake without widening the public RUM `source` surface. To validate behavior and guard against regressions, the change also introduces E2E scenarios covering snapshot delivery, value capture, exception handling, template and condition evaluation, and RUM correlation, plus a dedicated instrumentation-overhead benchmark that stress-tests the hooks against the real built bundle (rather than stubs) so the per-call cost we track is the cost customers will actually pay.
1 parent 5f1d903 commit e739e7e

66 files changed

Lines changed: 8164 additions & 49 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/CODEOWNERS

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,3 +12,10 @@ test/e2e/scenario/recorder/** @Datadog/rum-browser @Datadog/se
1212

1313
# Docs
1414
/README.md @Datadog/rum-browser @DataDog/documentation
15+
16+
# Debugger
17+
packages/debugger @Datadog/rum-browser @DataDog/debugger
18+
packages/debugger/README.md @Datadog/rum-browser @DataDog/debugger @DataDog/documentation
19+
test/apps/instrumentation-overhead @Datadog/rum-browser @DataDog/debugger
20+
test/e2e/scenario/debugger.scenario.ts @Datadog/rum-browser @DataDog/debugger
21+
test/performance/scenarios/instrumentationOverhead.scenario.ts @Datadog/rum-browser @DataDog/debugger

LICENSE-3rdparty.csv

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ dev,@vitejs/plugin-react,MIT,Copyright (c) 2019-present Evan You & Vite Contribu
3838
dev,@vitejs/plugin-vue,MIT,Copyright (c) 2019-present, Yuxi (Evan) You and Vite contributors
3939
dev,@module-federation/enhanced,MIT, Copyright (c) 2020 ScriptedAlchemy LLC (Zack Jackson) Zhou Shaw (zhouxiao)
4040
dev,@vue/test-utils,MIT,Copyright (c) 2021-present vuejs
41+
dev,acorn,MIT,Copyright (C) 2012-2022 by various contributors (see AUTHORS)
4142
dev,ajv,MIT,Copyright 2015-2017 Evgeny Poberezkin
4243
dev,babel-loader,MIT,Copyright (c) 2014-2019 Luís Couto <hello@luiscouto.pt>
4344
dev,browserstack-local,MIT,Copyright 2016 BrowserStack

eslint-local-rules/disallowSideEffects.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ const pathsWithSideEffect = new Set([
2929
`${packagesRoot}/logs/src/entries/main.ts`,
3030
`${packagesRoot}/rum/src/entries/main.ts`,
3131
`${packagesRoot}/rum-slim/src/entries/main.ts`,
32+
`${packagesRoot}/debugger/src/entries/main.ts`,
3233
])
3334

3435
// Those packages are known to have no side effects when evaluated

packages/core/src/domain/configuration/configuration.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -311,6 +311,8 @@ export interface ReplicaUserConfiguration {
311311
clientToken: string
312312
}
313313

314+
export type SdkSource = 'browser' | 'flutter' | 'unity'
315+
314316
export interface Configuration extends TransportConfiguration {
315317
// Built from init configuration
316318
beforeSend: GenericBeforeSendCallback | undefined
@@ -331,7 +333,6 @@ export interface Configuration extends TransportConfiguration {
331333

332334
// internal
333335
sdkVersion: string | undefined
334-
source: 'browser' | 'flutter' | 'unity'
335336
variant: string | undefined
336337
}
337338

packages/core/src/domain/configuration/endpointBuilder.ts

Lines changed: 21 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,18 +8,34 @@ import type { InitConfiguration } from './configuration'
88
// replaced at build time
99
declare const __BUILD_ENV__SDK_VERSION__: string
1010

11-
export type TrackType = 'logs' | 'rum' | 'replay' | 'profile' | 'exposures' | 'flagevaluation'
11+
export type TrackType = 'logs' | 'rum' | 'replay' | 'profile' | 'exposures' | 'flagevaluation' | 'debugger'
1212
export type ApiType =
1313
| 'fetch'
1414
| 'beacon'
1515
// 'manual' reflects that the request have been sent manually, outside of the SDK (ex: via curl or
1616
// a Node.js script).
1717
| 'manual'
1818

19+
/**
20+
* Source values supported by the transport layer for the `ddsource` URL parameter.
21+
*
22+
* `'dd_debugger'` is internal to the Live Debugger SDK and is not part of
23+
* `InitConfiguration.source`. It can only be supplied by passing it as the
24+
* `sourceOverride` argument of `computeTransportConfiguration`.
25+
*/
26+
export type TransportSource = 'browser' | 'flutter' | 'unity' | 'dd_debugger'
27+
28+
// Internal: the endpoint builder accepts a wider `source` than the public
29+
// `InitConfiguration.source` so that `computeTransportConfiguration` can pass
30+
// the validated/overridden transport source through to URL building.
31+
type EndpointBuilderInitConfiguration = Omit<InitConfiguration, 'source'> & {
32+
source?: TransportSource
33+
}
34+
1935
export type EndpointBuilder = ReturnType<typeof createEndpointBuilder>
2036

2137
export function createEndpointBuilder(
22-
initConfiguration: InitConfiguration,
38+
initConfiguration: EndpointBuilderInitConfiguration,
2339
trackType: TrackType,
2440
extraParameters?: string[]
2541
) {
@@ -40,7 +56,7 @@ export function createEndpointBuilder(
4056
* request, as only parameters are changing.
4157
*/
4258
function createEndpointUrlWithParametersBuilder(
43-
initConfiguration: InitConfiguration,
59+
initConfiguration: EndpointBuilderInitConfiguration,
4460
trackType: TrackType
4561
): (parameters: string) => string {
4662
const path = `/api/v2/${trackType}`
@@ -58,7 +74,7 @@ function createEndpointUrlWithParametersBuilder(
5874

5975
export function buildEndpointHost(
6076
trackType: TrackType,
61-
initConfiguration: InitConfiguration & { usePciIntake?: boolean }
77+
initConfiguration: Omit<InitConfiguration, 'source'> & { usePciIntake?: boolean }
6278
) {
6379
const { site = INTAKE_SITE_US1, internalAnalyticsSubdomain } = initConfiguration
6480

@@ -84,7 +100,7 @@ export function buildEndpointHost(
84100
* request, as they change randomly.
85101
*/
86102
function buildEndpointParameters(
87-
{ clientToken, internalAnalyticsSubdomain, source = 'browser' }: InitConfiguration,
103+
{ clientToken, internalAnalyticsSubdomain, source = 'browser' }: EndpointBuilderInitConfiguration,
88104
trackType: TrackType,
89105
api: ApiType,
90106
{ retry, encoding }: Payload,

packages/core/src/domain/configuration/transportConfiguration.ts

Lines changed: 38 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import type { Site } from '../intakeSites'
22
import { INTAKE_SITE_US1, INTAKE_URL_PARAMETERS } from '../intakeSites'
3-
import type { InitConfiguration } from './configuration'
4-
import type { EndpointBuilder } from './endpointBuilder'
3+
import type { InitConfiguration, SdkSource } from './configuration'
4+
import type { EndpointBuilder, TransportSource } from './endpointBuilder'
55
import { createEndpointBuilder } from './endpointBuilder'
66

77
export interface TransportConfiguration {
@@ -11,23 +11,48 @@ export interface TransportConfiguration {
1111
profilingEndpointBuilder: EndpointBuilder
1212
exposuresEndpointBuilder: EndpointBuilder
1313
flagEvaluationEndpointBuilder: EndpointBuilder
14+
debuggerEndpointBuilder: EndpointBuilder
1415
datacenter?: string | undefined
1516
replica?: ReplicaConfiguration
1617
site: Site
17-
source: 'browser' | 'flutter' | 'unity'
18+
source: SdkSource
1819
}
1920

2021
export interface ReplicaConfiguration {
2122
logsEndpointBuilder: EndpointBuilder
2223
rumEndpointBuilder: EndpointBuilder
2324
}
2425

25-
export function computeTransportConfiguration(initConfiguration: InitConfiguration): TransportConfiguration {
26+
// Internal: init configuration once the transport source has been resolved
27+
// (validated user value or `sourceOverride` from `computeTransportConfiguration`).
28+
type ResolvedSourceInitConfiguration = Omit<InitConfiguration, 'source'> & {
29+
source: TransportSource
30+
}
31+
32+
/**
33+
* Compute the transport configuration (endpoint builders, replica, etc.) for an
34+
* SDK init configuration.
35+
*
36+
* `sourceOverride` (if provided) is only used to build the `ddsource` URL
37+
* parameter on outgoing requests; it does not appear on the returned
38+
* `TransportConfiguration.source`, which is always the validated `SdkSource`
39+
* derived from `initConfiguration.source`.
40+
*/
41+
export function computeTransportConfiguration(
42+
initConfiguration: InitConfiguration,
43+
sourceOverride?: TransportSource
44+
): TransportConfiguration {
2645
const site = initConfiguration.site || INTAKE_SITE_US1
2746
const source = validateSource(initConfiguration.source)
47+
const transportSource: TransportSource = sourceOverride ?? source
2848

29-
const endpointBuilders = computeEndpointBuilders({ ...initConfiguration, site, source })
30-
const replicaConfiguration = computeReplicaConfiguration({ ...initConfiguration, site, source })
49+
const resolvedConfiguration: ResolvedSourceInitConfiguration = {
50+
...initConfiguration,
51+
site,
52+
source: transportSource,
53+
}
54+
const endpointBuilders = computeEndpointBuilders(resolvedConfiguration)
55+
const replicaConfiguration = computeReplicaConfiguration(resolvedConfiguration)
3156

3257
return {
3358
replica: replicaConfiguration,
@@ -37,30 +62,33 @@ export function computeTransportConfiguration(initConfiguration: InitConfigurati
3762
}
3863
}
3964

40-
function validateSource(source: string | undefined) {
65+
function validateSource(source: string | undefined): SdkSource {
4166
if (source === 'flutter' || source === 'unity') {
4267
return source
4368
}
4469
return 'browser'
4570
}
4671

47-
function computeEndpointBuilders(initConfiguration: InitConfiguration) {
72+
function computeEndpointBuilders(initConfiguration: ResolvedSourceInitConfiguration) {
4873
return {
4974
logsEndpointBuilder: createEndpointBuilder(initConfiguration, 'logs'),
5075
rumEndpointBuilder: createEndpointBuilder(initConfiguration, 'rum'),
5176
profilingEndpointBuilder: createEndpointBuilder(initConfiguration, 'profile'),
5277
sessionReplayEndpointBuilder: createEndpointBuilder(initConfiguration, 'replay'),
5378
exposuresEndpointBuilder: createEndpointBuilder(initConfiguration, 'exposures'),
5479
flagEvaluationEndpointBuilder: createEndpointBuilder(initConfiguration, 'flagevaluation'),
80+
debuggerEndpointBuilder: createEndpointBuilder(initConfiguration, 'debugger'),
5581
}
5682
}
5783

58-
function computeReplicaConfiguration(initConfiguration: InitConfiguration): ReplicaConfiguration | undefined {
84+
function computeReplicaConfiguration(
85+
initConfiguration: ResolvedSourceInitConfiguration
86+
): ReplicaConfiguration | undefined {
5987
if (!initConfiguration.replica) {
6088
return
6189
}
6290

63-
const replicaConfiguration: InitConfiguration = {
91+
const replicaConfiguration: ResolvedSourceInitConfiguration = {
6492
...initConfiguration,
6593
site: INTAKE_SITE_US1,
6694
clientToken: initConfiguration.replica.clientToken,

packages/core/src/index.ts

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ export {
77
isSampleRate,
88
buildEndpointHost,
99
isIntakeUrl,
10+
computeTransportConfiguration,
1011
} from './domain/configuration'
1112
export * from './domain/intakeSites'
1213
export type { TrackingConsentState } from './domain/trackingConsent'
@@ -57,7 +58,15 @@ export {
5758
SESSION_NOT_TRACKED,
5859
SessionPersistence,
5960
} from './domain/session/sessionConstants'
60-
export type { BandwidthStats, HttpRequest, HttpRequestEvent, Payload, FlushEvent, FlushReason } from './transport'
61+
export type {
62+
Batch,
63+
BandwidthStats,
64+
HttpRequest,
65+
HttpRequestEvent,
66+
Payload,
67+
FlushEvent,
68+
FlushReason,
69+
} from './transport'
6170
export {
6271
createHttpRequest,
6372
canUseEventBridge,

packages/core/src/transport/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ export type { BandwidthStats, HttpRequest, HttpRequestEvent, Payload, RetryInfo
22
export { createHttpRequest } from './httpRequest'
33
export type { BrowserWindowWithEventBridge, DatadogEventBridge } from './eventBridge'
44
export { canUseEventBridge, bridgeSupports, getEventBridge, BridgeCapability } from './eventBridge'
5+
export type { Batch } from './batch'
56
export { createBatch } from './batch'
67
export type { FlushController, FlushEvent, FlushReason } from './flushController'
78
export { createFlushController, FLUSH_DURATION_LIMIT } from './flushController'

0 commit comments

Comments
 (0)