Skip to content

Commit 7399fd1

Browse files
committed
Add RC Proxy
1 parent 9747843 commit 7399fd1

File tree

17 files changed

+2299
-73
lines changed

17 files changed

+2299
-73
lines changed

packages/live-debugger/src/domain/probes.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,19 @@ export function getProbes(functionId: string): InitializedProbe[] | undefined {
8585
return activeProbes.get(functionId)
8686
}
8787

88+
/**
89+
* Get all active probes across all functions
90+
*
91+
* @returns Array of all active probes
92+
*/
93+
export function getAllProbes(): InitializedProbe[] {
94+
const allProbes: InitializedProbe[] = []
95+
for (const probes of activeProbes.values()) {
96+
allProbes.push(...probes)
97+
}
98+
return allProbes
99+
}
100+
88101
/**
89102
* Remove a probe from the registry
90103
*
Lines changed: 178 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,178 @@
1+
import { display, setInterval, clearInterval } from '@datadog/browser-core'
2+
import type { LiveDebuggerInitConfiguration } from '../entries/main'
3+
import { addProbe, removeProbe } from './probes'
4+
import type { Probe } from './probes'
5+
6+
/**
7+
* Remote Config Client for Browser Live Debugger
8+
*
9+
* Polls the RC proxy periodically to get probe updates and synchronizes
10+
* the local probe registry.
11+
*
12+
* NOTE: The RC proxy can operate in two modes:
13+
* - agent mode: Polls local Datadog agent (for POC/development, no CORS issues)
14+
* - backend mode: Polls Datadog RC backend directly (requires backend access)
15+
*/
16+
17+
interface ProbeState {
18+
id: string
19+
version: number
20+
}
21+
22+
let pollIntervalId: number | undefined
23+
let currentProbeStates = new Map<string, ProbeState>() // Track probes by ID and version
24+
25+
/**
26+
* Start polling the Remote Config proxy for probe updates
27+
*
28+
* @param config - Live Debugger configuration
29+
*/
30+
export function startRemoteConfigPolling(config: LiveDebuggerInitConfiguration): void {
31+
if (pollIntervalId !== undefined) {
32+
display.warn('Live Debugger: Remote Config polling already started')
33+
return
34+
}
35+
36+
const proxyUrl = config.remoteConfigProxyUrl!
37+
const pollInterval = config.remoteConfigPollInterval || 5000
38+
39+
display.info(`Live Debugger: Starting Remote Config polling (${proxyUrl}, interval: ${pollInterval}ms)`)
40+
41+
// Build query string with service metadata
42+
const params = new URLSearchParams()
43+
params.set('service', config.service!)
44+
if (config.env) {
45+
params.set('env', config.env)
46+
}
47+
if (config.version) {
48+
params.set('version', config.version)
49+
}
50+
51+
const pollUrl = `${proxyUrl}/probes?${params.toString()}`
52+
53+
// Polling function
54+
const poll = async () => {
55+
try {
56+
const response = await fetch(pollUrl)
57+
58+
if (!response.ok) {
59+
display.error(`Live Debugger: RC poll failed with status ${response.status}`)
60+
return
61+
}
62+
63+
const data = await response.json()
64+
const probes: Probe[] = data.probes || []
65+
66+
// Synchronize probes
67+
synchronizeProbes(probes)
68+
} catch (err) {
69+
display.error('Live Debugger: RC poll error:', err as Error)
70+
}
71+
}
72+
73+
// Initial poll
74+
void poll()
75+
76+
// Start polling interval
77+
pollIntervalId = setInterval(() => {
78+
void poll()
79+
}, pollInterval)
80+
}
81+
82+
/**
83+
* Stop Remote Config polling
84+
*/
85+
export function stopRemoteConfigPolling(): void {
86+
if (pollIntervalId !== undefined) {
87+
clearInterval(pollIntervalId)
88+
pollIntervalId = undefined
89+
display.info('Live Debugger: Remote Config polling stopped')
90+
}
91+
}
92+
93+
/**
94+
* Synchronize local probes with probes from RC proxy
95+
*
96+
* - Adds new probes
97+
* - Removes probes no longer in the response
98+
* - Updates probes if version changed
99+
*
100+
* @param probes - Array of probes from RC proxy
101+
*/
102+
function synchronizeProbes(probes: Probe[]): void {
103+
const newProbeStates = new Map<string, ProbeState>()
104+
105+
// Process probes from RC
106+
for (const probe of probes) {
107+
if (!probe.id) {
108+
display.warn('Live Debugger: Received probe without ID, skipping')
109+
continue
110+
}
111+
112+
const probeState: ProbeState = {
113+
id: probe.id,
114+
version: probe.version
115+
}
116+
newProbeStates.set(probe.id, probeState)
117+
118+
const currentState = currentProbeStates.get(probe.id)
119+
120+
if (!currentState) {
121+
// New probe - add it
122+
try {
123+
addProbe(probe)
124+
display.log(`Live Debugger: Added probe ${probe.id} (v${probe.version})`)
125+
} catch (err) {
126+
display.error(`Live Debugger: Failed to add probe ${probe.id}:`, err as Error)
127+
}
128+
} else if (currentState.version !== probe.version) {
129+
// Probe version changed - remove old and add new
130+
try {
131+
removeProbe(probe.id)
132+
addProbe(probe)
133+
display.log(`Live Debugger: Updated probe ${probe.id} (v${currentState.version} -> v${probe.version})`)
134+
} catch (err) {
135+
display.error(`Live Debugger: Failed to update probe ${probe.id}:`, err as Error)
136+
}
137+
}
138+
// If version is the same, probe already exists - no action needed
139+
}
140+
141+
// Remove probes that are no longer in RC response
142+
for (const [probeId, currentState] of currentProbeStates.entries()) {
143+
if (!newProbeStates.has(probeId)) {
144+
try {
145+
removeProbe(probeId)
146+
display.log(`Live Debugger: Removed probe ${probeId} (v${currentState.version})`)
147+
} catch (err) {
148+
display.error(`Live Debugger: Failed to remove probe ${probeId}:`, err as Error)
149+
}
150+
}
151+
}
152+
153+
// Update current state
154+
currentProbeStates = newProbeStates
155+
156+
display.log(`Live Debugger: Synchronized ${newProbeStates.size} probe(s)`)
157+
}
158+
159+
/**
160+
* Get current Remote Config polling status
161+
*
162+
* @returns true if polling is active
163+
*/
164+
export function isRemoteConfigPolling(): boolean {
165+
return pollIntervalId !== undefined
166+
}
167+
168+
/**
169+
* Clear probe state (useful for testing)
170+
*/
171+
export function clearRemoteConfigState(): void {
172+
currentProbeStates.clear()
173+
if (pollIntervalId !== undefined) {
174+
clearInterval(pollIntervalId)
175+
pollIntervalId = undefined
176+
}
177+
}
178+

packages/live-debugger/src/entries/main.ts

Lines changed: 40 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,12 @@
66
* @see [Live Debugger Documentation](https://docs.datadoghq.com/dynamic_instrumentation/)
77
*/
88

9-
import { defineGlobal, getGlobalObject, makePublicApi } from '@datadog/browser-core'
9+
import { defineGlobal, getGlobalObject, makePublicApi, display } from '@datadog/browser-core'
1010
import type { PublicApi, Site } from '@datadog/browser-core'
1111
import { onEntry, onReturn, onThrow, sendDebuggerSnapshot } from '../domain/api'
12-
import { addProbe, getProbes, removeProbe, clearProbes } from '../domain/probes'
12+
import { addProbe, getProbes, getAllProbes, removeProbe, clearProbes } from '../domain/probes'
1313
import type { Probe } from '../domain/probes'
14+
import { startRemoteConfigPolling } from '../domain/remoteConfig'
1415

1516
export type { Probe, ProbeWhere, ProbeWhen, ProbeSampling, InitializedProbe } from '../domain/probes'
1617
export type { CaptureOptions, CapturedValue } from '../domain/capture'
@@ -56,6 +57,22 @@ export interface LiveDebuggerInitConfiguration {
5657
* @category Data Collection
5758
*/
5859
version?: string
60+
61+
/**
62+
* URL of the Remote Config proxy for dynamic probe management
63+
*
64+
* @category Remote Config
65+
* @example 'http://localhost:3030'
66+
*/
67+
remoteConfigProxyUrl?: string
68+
69+
/**
70+
* Remote Config polling interval in milliseconds
71+
*
72+
* @category Remote Config
73+
* @defaultValue 5000
74+
*/
75+
remoteConfigPollInterval?: number
5976
}
6077

6178
/**
@@ -104,6 +121,14 @@ export interface LiveDebuggerPublicApi extends PublicApi {
104121
*/
105122
clearProbes: () => void
106123

124+
/**
125+
* Get all currently active probes across all instrumented functions
126+
*
127+
* @category Probes
128+
* @returns Array of all active probes
129+
*/
130+
getProbes: () => Probe[]
131+
107132
/**
108133
* Send a debugger snapshot to Datadog logs.
109134
*
@@ -121,15 +146,24 @@ export interface LiveDebuggerPublicApi extends PublicApi {
121146
*/
122147
function makeLiveDebuggerPublicApi(): LiveDebuggerPublicApi {
123148
return makePublicApi<LiveDebuggerPublicApi>({
124-
init: () => {
125-
// TODO: Support configuration argument
149+
init: (initConfiguration: LiveDebuggerInitConfiguration) => {
126150
// Expose internal hooks on globalThis for instrumented code
127151
if (typeof globalThis !== 'undefined') {
128152
;(globalThis as any).$dd_entry = onEntry
129153
;(globalThis as any).$dd_return = onReturn
130154
;(globalThis as any).$dd_throw = onThrow
131155
;(globalThis as any).$dd_probes = getProbes
132156
}
157+
158+
// Start Remote Config polling if proxy URL is provided
159+
if (initConfiguration.remoteConfigProxyUrl) {
160+
if (!initConfiguration.service) {
161+
display.error('Live Debugger: service is required when using remoteConfigProxyUrl')
162+
return
163+
}
164+
165+
startRemoteConfigPolling(initConfiguration)
166+
}
133167
},
134168

135169
addProbe: (probe: Probe) => {
@@ -144,6 +178,8 @@ function makeLiveDebuggerPublicApi(): LiveDebuggerPublicApi {
144178
clearProbes()
145179
},
146180

181+
getProbes: () => getAllProbes(),
182+
147183
sendDebuggerSnapshot: (logger: any, dd: any, snapshot: any, message?: string) => {
148184
sendDebuggerSnapshot(logger, dd, snapshot, message)
149185
},

0 commit comments

Comments
 (0)