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

⚗️[RUM-4780] Remote configuration #2799

Merged
merged 7 commits into from
Jun 13, 2024
Merged
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
5 changes: 2 additions & 3 deletions packages/core/src/browser/addEventListener.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import { monitor } from '../tools/monitor'
import { getZoneJsOriginalValue } from '../tools/getZoneJsOriginalValue'
import type { Configuration } from '../domain/configuration'
import type { CookieStore, CookieStoreEventMap, VisualViewport, VisualViewportEventMap } from './types'

export type TrustableEvent<E extends Event = Event> = E & { __ddIsTrusted?: boolean }
Expand Down Expand Up @@ -90,7 +89,7 @@ type EventMapFor<T> = T extends Window
* * returns a `stop` function to remove the listener
*/
export function addEventListener<Target extends EventTarget, EventName extends keyof EventMapFor<Target> & string>(
configuration: Configuration,
configuration: { allowUntrustedEvents?: boolean | undefined },
thomas-lebeau marked this conversation as resolved.
Show resolved Hide resolved
eventTarget: Target,
eventName: EventName,
listener: (event: EventMapFor<Target>[EventName] & { type: EventName }) => void,
Expand All @@ -112,7 +111,7 @@ export function addEventListener<Target extends EventTarget, EventName extends k
* * with `once: true`, the listener will be called at most once, even if different events are listened
*/
export function addEventListeners<Target extends EventTarget, EventName extends keyof EventMapFor<Target> & string>(
configuration: Configuration,
configuration: { allowUntrustedEvents?: boolean | undefined },
eventTarget: Target,
eventNames: EventName[],
listener: (event: EventMapFor<Target>[EventName] & { type: EventName }) => void,
Expand Down
1 change: 1 addition & 0 deletions packages/core/src/tools/experimentalFeatures.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ export enum ExperimentalFeature {
TOLERANT_RESOURCE_TIMINGS = 'tolerant_resource_timings',
ENABLE_PRIVACY_FOR_ACTION_NAME = 'enable_privacy_for_action_name',
MICRO_FRONTEND = 'micro_frontend',
REMOTE_CONFIGURATION = 'remote_configuration',
}

const enabledExperimentalFeatures: Set<ExperimentalFeature> = new Set()
Expand Down
3 changes: 2 additions & 1 deletion packages/core/src/transport/eventBridge.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
import { endsWith, includes } from '../tools/utils/polyfills'
import { getGlobalObject } from '../tools/getGlobalObject'
import type { DefaultPrivacyLevel } from '../domain/configuration'

export interface BrowserWindowWithEventBridge extends Window {
DatadogEventBridge?: DatadogEventBridge
}

export interface DatadogEventBridge {
getCapabilities?(): string
getPrivacyLevel?(): string
getPrivacyLevel?(): DefaultPrivacyLevel
getAllowedWebViewHosts(): string
send(msg: string): void
}
Expand Down
2 changes: 2 additions & 0 deletions packages/core/test/requests.ts
Original file line number Diff line number Diff line change
Expand Up @@ -279,6 +279,7 @@ class StubEventEmitter {
class StubXhr extends StubEventEmitter {
public static onSend: (xhr: StubXhr) => void | undefined
public response: string | undefined = undefined
public responseText: string | undefined = undefined
public status: number | undefined = undefined
public readyState: number = XMLHttpRequest.UNSENT
public onreadystatechange: () => void = noop
Expand Down Expand Up @@ -313,6 +314,7 @@ class StubXhr extends StubEventEmitter {
}
this.hasEnded = true
this.response = response
this.responseText = response
this.status = status
this.readyState = XMLHttpRequest.DONE

Expand Down
67 changes: 67 additions & 0 deletions packages/rum-core/src/boot/preStartRum.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,17 @@ import {
TrackingConsent,
createTrackingConsentState,
DefaultPrivacyLevel,
addExperimentalFeatures,
ExperimentalFeature,
resetExperimentalFeatures,
} from '@datadog/browser-core'
import type { Clock } from '@datadog/browser-core/test'
import {
cleanupSyntheticsWorkerValues,
initEventBridgeStub,
interceptRequests,
mockClock,
mockExperimentalFeatures,
mockSyntheticsWorkerValues,
} from '@datadog/browser-core/test'
import type { HybridInitConfiguration, RumConfiguration, RumInitConfiguration } from '../domain/configuration'
Expand Down Expand Up @@ -393,6 +398,48 @@ describe('preStartRum', () => {
})
})
})

describe('remote configuration', () => {
let interceptor: ReturnType<typeof interceptRequests>

beforeEach(() => {
interceptor = interceptRequests()
})

afterEach(() => {
interceptor.restore()
})

describe('when remote_configuration ff is enabled', () => {
it('should start with the remote configuration when a remoteConfigurationId is provided', (done) => {
mockExperimentalFeatures([ExperimentalFeature.REMOTE_CONFIGURATION])

interceptor.withStubXhr((xhr) => {
xhr.complete(200, '{"sessionSampleRate":50}')

expect(doStartRumSpy.calls.mostRecent().args[0].sessionSampleRate).toEqual(50)
done()
})

const strategy = createPreStartStrategy({}, getCommonContextSpy, createTrackingConsentState(), doStartRumSpy)
strategy.init({
...DEFAULT_INIT_CONFIGURATION,
remoteConfigurationId: '123',
})
})
})

describe('when remote_configuration ff is disabled', () => {
it('should start without the remote configuration when a remoteConfigurationId is provided', () => {
const strategy = createPreStartStrategy({}, getCommonContextSpy, createTrackingConsentState(), doStartRumSpy)
strategy.init({
...DEFAULT_INIT_CONFIGURATION,
remoteConfigurationId: '123',
})
expect(doStartRumSpy.calls.mostRecent().args[0].sessionSampleRate).toEqual(100)
})
})
})
})

describe('getInternalContext', () => {
Expand All @@ -417,14 +464,18 @@ describe('preStartRum', () => {
describe('initConfiguration', () => {
let strategy: Strategy
let initConfiguration: RumInitConfiguration
let interceptor: ReturnType<typeof interceptRequests>

beforeEach(() => {
interceptor = interceptRequests()
strategy = createPreStartStrategy({}, getCommonContextSpy, createTrackingConsentState(), doStartRumSpy)
initConfiguration = { ...DEFAULT_INIT_CONFIGURATION, service: 'my-service', version: '1.4.2', env: 'dev' }
})

afterEach(() => {
cleanupSyntheticsWorkerValues()
interceptor.restore()
resetExperimentalFeatures()
})

it('is undefined before init', () => {
Expand Down Expand Up @@ -452,6 +503,22 @@ describe('preStartRum', () => {

expect(strategy.initConfiguration).toEqual(initConfiguration)
})

it('returns the initConfiguration with the remote configuration when a remoteConfigurationId is provided', (done) => {
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

I'm not sure about this behaviour. Basically DD_RUM.getInitConfiguration() will also have the remote configuration values. It has the advantage to make the browser extension reflect the remote config without any change.
Wdyt?

Copy link
Collaborator

Choose a reason for hiding this comment

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

💭 thought: ‏ Currently DD_RUM.getInitConfiguration() returns exactly what the customer called init() with. I wouldn't mind it to return what the SDK was actually initialised with, this means the combination of what the customer called init() with, the remote config if applicable and the defaults.

I think it would be useful information to debug rum, both for customers, for us through telemetry and even for support.

I'm not sure if it is safe to do that or if this will be a breaking change.

Copy link
Member

Choose a reason for hiding this comment

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

We could review our usages of getInitConfiguration https://github.com/search?q=repo%3ADataDog%2Fsynthetics-worker%20getInitConfiguration&type=code

To me it doesn't matter much, but we'll maybe need a onInitConfigurationReady(cb) API in the future if we have use-cases where we need to access the resolved config.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

From what I identifyed in Datadog repos, we use the API for

Clearly if we get one of fields applicationId, service, env or version, from the RC we will have some issues. But luckily for now they won't be available in RC :). So the question is, should we anticipate by introducing a new API right away and defining the getInitConfiguration() has only returning whats passed to the init() API
I'll create an RFC

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Seen with @BenoitZugmeyer, since the feature is under FF, lets keep the behaviour as is and revisite later when needed.

Copy link
Collaborator

Choose a reason for hiding this comment

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

perfect! Sounds good!

addExperimentalFeatures([ExperimentalFeature.REMOTE_CONFIGURATION])
interceptor.withStubXhr((xhr) => {
xhr.complete(200, '{"sessionSampleRate":50}')

expect(strategy.initConfiguration?.sessionSampleRate).toEqual(50)
done()
})

const strategy = createPreStartStrategy({}, getCommonContextSpy, createTrackingConsentState(), doStartRumSpy)
strategy.init({
...DEFAULT_INIT_CONFIGURATION,
remoteConfigurationId: '123',
})
})
})

describe('buffers API calls before starting RUM', () => {
Expand Down
97 changes: 56 additions & 41 deletions packages/rum-core/src/boot/preStartRum.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,10 @@ import {
clocksNow,
assign,
getEventBridge,
addTelemetryConfiguration,
ExperimentalFeature,
isExperimentalFeatureEnabled,
initFeatureFlags,
addTelemetryConfiguration,
} from '@datadog/browser-core'
import type { TrackingConsentState, DeflateWorker } from '@datadog/browser-core'
import {
Expand All @@ -20,7 +22,7 @@ import {
} from '../domain/configuration'
import type { CommonContext } from '../domain/contexts/commonContext'
import type { ViewOptions } from '../domain/view/trackViews'
import { serializeRumConfiguration } from '../domain/configuration'
import { fetchAndApplyRemoteConfiguration, serializeRumConfiguration } from '../domain/configuration'
import type { RumPublicApiOptions, Strategy } from './rumPublicApi'
import type { StartRumResult } from './startRum'

Expand Down Expand Up @@ -73,29 +75,62 @@ export function createPreStartStrategy(
bufferApiCalls.drain(startRumResult)
}

function doInit(initConfiguration: RumInitConfiguration) {
const eventBridgeAvailable = canUseEventBridge()
if (eventBridgeAvailable) {
initConfiguration = overrideInitConfigurationForBridge(initConfiguration)
}

// Update the exposed initConfiguration to reflect the bridge and remote configuration overrides
cachedInitConfiguration = initConfiguration
addTelemetryConfiguration(serializeRumConfiguration(initConfiguration))

if (cachedConfiguration) {
displayAlreadyInitializedError('DD_RUM', initConfiguration)
return
}

const configuration = validateAndBuildRumConfiguration(initConfiguration)
if (!configuration) {
return
}

if (!eventBridgeAvailable && !configuration.sessionStoreStrategyType) {
display.warn('No storage available for session. We will not send any data.')
return
}

if (configuration.compressIntakeRequests && !eventBridgeAvailable && startDeflateWorker) {
deflateWorker = startDeflateWorker(
configuration,
'Datadog RUM',
// Worker initialization can fail asynchronously, especially in Firefox where even CSP
// issues are reported asynchronously. For now, the SDK will continue its execution even if
// data won't be sent to Datadog. We could improve this behavior in the future.
noop
)
if (!deflateWorker) {
// `startDeflateWorker` should have logged an error message explaining the issue
return
}
}

cachedConfiguration = configuration
trackingConsentState.tryToInit(configuration.trackingConsent)
tryStartRum()
}

return {
init(initConfiguration) {
if (!initConfiguration) {
display.error('Missing configuration')
return
}

// Set the experimental feature flags as early as possible, so we can use them in most places
initFeatureFlags(initConfiguration.enableExperimentalFeatures)

const eventBridgeAvailable = canUseEventBridge()
if (eventBridgeAvailable) {
initConfiguration = overrideInitConfigurationForBridge(initConfiguration)
}

// Expose the initial configuration regardless of initialization success.
cachedInitConfiguration = initConfiguration
addTelemetryConfiguration(serializeRumConfiguration(initConfiguration))

if (cachedConfiguration) {
displayAlreadyInitializedError('DD_RUM', initConfiguration)
return
}

// If we are in a Synthetics test configured to automatically inject a RUM instance, we want
// to completely discard the customer application RUM instance by ignoring their init() call.
Expand All @@ -105,34 +140,14 @@ export function createPreStartStrategy(
return
}

const configuration = validateAndBuildRumConfiguration(initConfiguration)
if (!configuration) {
return
}

if (!eventBridgeAvailable && !configuration.sessionStoreStrategyType) {
display.warn('No storage available for session. We will not send any data.')
return
}

if (configuration.compressIntakeRequests && !eventBridgeAvailable && startDeflateWorker) {
deflateWorker = startDeflateWorker(
configuration,
'Datadog RUM',
// Worker initialization can fail asynchronously, especially in Firefox where even CSP
// issues are reported asynchronously. For now, the SDK will continue its execution even if
// data won't be sent to Datadog. We could improve this behavior in the future.
noop
)
if (!deflateWorker) {
// `startDeflateWorker` should have logged an error message explaining the issue
return
}
if (
initConfiguration.remoteConfigurationId &&
isExperimentalFeatureEnabled(ExperimentalFeature.REMOTE_CONFIGURATION)
) {
fetchAndApplyRemoteConfiguration(initConfiguration, doInit)
} else {
doInit(initConfiguration)
}

cachedConfiguration = configuration
trackingConsentState.tryToInit(configuration.trackingConsent)
tryStartRum()
},

get initConfiguration() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,12 @@ import {
EXHAUSTIVE_INIT_CONFIGURATION,
mockExperimentalFeatures,
SERIALIZED_EXHAUSTIVE_INIT_CONFIGURATION,
} from '../../../core/test'
import type { ExtractTelemetryConfiguration, CamelToSnakeCase, MapInitConfigurationKey } from '../../../core/test'
} from '@datadog/browser-core/test'
import type {
ExtractTelemetryConfiguration,
CamelToSnakeCase,
MapInitConfigurationKey,
} from '@datadog/browser-core/test'
import type { RumInitConfiguration } from './configuration'
import { DEFAULT_PROPAGATOR_TYPES, serializeRumConfiguration, validateAndBuildRumConfiguration } from './configuration'

Expand Down Expand Up @@ -471,6 +475,7 @@ describe('serializeRumConfiguration', () => {
trackViewsManually: true,
trackResources: true,
trackLongTasks: true,
remoteConfigurationId: '123',
}

type MapRumInitConfigurationKey<Key extends string> = Key extends keyof InitConfiguration
Expand All @@ -479,7 +484,7 @@ describe('serializeRumConfiguration', () => {
? `use_${CamelToSnakeCase<Key>}`
: Key extends 'trackLongTasks'
? 'track_long_task' // oops
: Key extends 'applicationId' | 'subdomain'
: Key extends 'applicationId' | 'subdomain' | 'remoteConfigurationId'
thomas-lebeau marked this conversation as resolved.
Show resolved Hide resolved
? never
: CamelToSnakeCase<Key>

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,10 @@ import {
isExperimentalFeatureEnabled,
ExperimentalFeature,
} from '@datadog/browser-core'
import type { RumEventDomainContext } from '../domainContext.types'
import type { RumEvent } from '../rumEvent.types'
import { isTracingOption } from './tracing/tracer'
import type { PropagatorType, TracingOption } from './tracing/tracer.types'
import type { RumEventDomainContext } from '../../domainContext.types'
import type { RumEvent } from '../../rumEvent.types'
import { isTracingOption } from '../tracing/tracer'
import type { PropagatorType, TracingOption } from '../tracing/tracer.types'

export const DEFAULT_PROPAGATOR_TYPES: PropagatorType[] = ['tracecontext', 'datadog']

Expand Down Expand Up @@ -52,6 +52,7 @@ export interface RumInitConfiguration extends InitConfiguration {
* See [Content Security Policy guidelines](https://docs.datadoghq.com/integrations/content_security_policy_logs/?tab=firefox#use-csp-with-real-user-monitoring-and-session-replay) for further information.
*/
compressIntakeRequests?: boolean | undefined
remoteConfigurationId?: string | undefined

// tracing options
/**
Expand Down
2 changes: 2 additions & 0 deletions packages/rum-core/src/domain/configuration/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export * from './configuration'
export * from './remoteConfiguration'
Loading