Skip to content
Draft
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
Original file line number Diff line number Diff line change
Expand Up @@ -229,7 +229,7 @@ describe('remoteConfiguration', () => {
it('should display an error if an unsupported `strategy` is provided', () => {
expectAppliedRemoteConfigurationToBe(
{ version: { rcSerializedType: 'dynamic', strategy: 'foo' as any } as any },
{ version: undefined }
{} // version should not be set when resolution fails
)
expect(displaySpy).toHaveBeenCalledWith('Unsupported remote configuration: "strategy": "foo"')
})
Expand All @@ -248,7 +248,7 @@ describe('remoteConfiguration', () => {
it('should resolve to undefined if the cookie is missing', () => {
expectAppliedRemoteConfigurationToBe(
{ version: { rcSerializedType: 'dynamic', strategy: 'cookie', name: COOKIE_NAME } },
{ version: undefined }
{} // version should not be set when resolution fails
)
expect(metrics.get().cookie).toEqual({ missing: 1 })
})
Expand Down Expand Up @@ -294,7 +294,7 @@ describe('remoteConfiguration', () => {
it('should resolve to undefined and display an error if the selector is invalid', () => {
expectAppliedRemoteConfigurationToBe(
{ version: { rcSerializedType: 'dynamic', strategy: 'dom', selector: '' } },
{ version: undefined }
{} // version should not be set when resolution fails
)
expect(displaySpy).toHaveBeenCalledWith("Invalid selector in the remote configuration: ''")
expect(metrics.get().dom).toEqual({ failure: 1 })
Expand All @@ -303,7 +303,7 @@ describe('remoteConfiguration', () => {
it('should resolve to undefined if the element is missing', () => {
expectAppliedRemoteConfigurationToBe(
{ version: { rcSerializedType: 'dynamic', strategy: 'dom', selector: '#missing' } },
{ version: undefined }
{} // version should not be set when resolution fails
)
expect(metrics.get().dom).toEqual({ missing: 1 })
})
Expand All @@ -320,7 +320,7 @@ describe('remoteConfiguration', () => {
it('should resolve to undefined if the element attribute is missing', () => {
expectAppliedRemoteConfigurationToBe(
{ version: { rcSerializedType: 'dynamic', strategy: 'dom', selector: '#version2', attribute: 'missing' } },
{ version: undefined }
{} // version should not be set when resolution fails
)
expect(metrics.get().dom).toEqual({ missing: 1 })
})
Expand All @@ -329,7 +329,7 @@ describe('remoteConfiguration', () => {
appendElement('<input id="pwd" type="password" value="foo" />')
expectAppliedRemoteConfigurationToBe(
{ version: { rcSerializedType: 'dynamic', strategy: 'dom', selector: '#pwd', attribute: 'value' } },
{ version: undefined }
{} // version should not be set when resolution fails
)
expect(displaySpy).toHaveBeenCalledWith("Forbidden element selected by the remote configuration: '#pwd'")
})
Expand Down Expand Up @@ -443,7 +443,7 @@ describe('remoteConfiguration', () => {
{
version: { rcSerializedType: 'dynamic', strategy: 'js', path: '.' },
},
{ version: undefined }
{} // version should not be set when resolution fails
)
expect(displaySpy).toHaveBeenCalledWith("Invalid JSON path in the remote configuration: '.'")
expect(metrics.get().js).toEqual({ failure: 1 })
Expand All @@ -462,7 +462,7 @@ describe('remoteConfiguration', () => {
{
version: { rcSerializedType: 'dynamic', strategy: 'js', path: 'foo.bar' },
},
{ version: undefined }
{} // version should not be set when resolution fails
)
expect(displaySpy).toHaveBeenCalledWith("Error accessing: 'foo.bar'", new Error('foo'))
expect(metrics.get().js).toEqual({ failure: 1 })
Expand All @@ -473,7 +473,7 @@ describe('remoteConfiguration', () => {
{
version: { rcSerializedType: 'dynamic', strategy: 'js', path: 'missing' },
},
{ version: undefined }
{} // version should not be set when resolution fails
)
expect(metrics.get().js).toEqual({ missing: 1 })
})
Expand All @@ -488,7 +488,7 @@ describe('remoteConfiguration', () => {
{
version: { rcSerializedType: 'dynamic', strategy: 'js', path: 'foo.missing' },
},
{ version: undefined }
{} // version should not be set when resolution fails
)
expect(metrics.get().js).toEqual({ missing: 1 })
})
Expand All @@ -503,12 +503,69 @@ describe('remoteConfiguration', () => {
{
version: { rcSerializedType: 'dynamic', strategy: 'js', path: 'foo[0]' },
},
{ version: undefined }
{} // version should not be set when resolution fails
)
expect(metrics.get().js).toEqual({ missing: 1 })
})
})

describe('localStorage strategy', () => {
const LOCAL_STORAGE_KEY = 'dd_test_version'

beforeEach(() => {
localStorage.setItem(LOCAL_STORAGE_KEY, 'my-version')
registerCleanupTask(() => localStorage.removeItem(LOCAL_STORAGE_KEY))
})

it('should resolve a configuration value from localStorage', () => {
expectAppliedRemoteConfigurationToBe(
{ version: { rcSerializedType: 'dynamic', strategy: 'localStorage', key: LOCAL_STORAGE_KEY } },
{ version: 'my-version' }
)
expect(metrics.get().localStorage).toEqual({ success: 1 })
})

it('should resolve a configuration value from localStorage with an extractor', () => {
localStorage.setItem(LOCAL_STORAGE_KEY, 'version-123')
expectAppliedRemoteConfigurationToBe(
{
version: {
rcSerializedType: 'dynamic',
strategy: 'localStorage',
key: LOCAL_STORAGE_KEY,
extractor: { rcSerializedType: 'regex', value: '\\d+' },
},
},
{ version: '123' }
)
})

it('should not override init config when localStorage key is missing', () => {
expectAppliedRemoteConfigurationToBe(
{ version: { rcSerializedType: 'dynamic', strategy: 'localStorage', key: 'non_existent_key' } },
{} // version should not be set at all when resolution fails
)
expect(metrics.get().localStorage).toEqual({ missing: 1 })
})

it('should preserve init config value when localStorage key is missing', () => {
const initConfigWithVersion = { ...DEFAULT_INIT_CONFIGURATION, version: 'init-version' }
const rumRemoteConfiguration: RumRemoteConfiguration = {
applicationId: 'yyy',
version: { rcSerializedType: 'dynamic', strategy: 'localStorage', key: 'non_existent_key' },
}

expect(
applyRemoteConfiguration(initConfigWithVersion, rumRemoteConfiguration, supportedContextManagers, metrics)
).toEqual({
...initConfigWithVersion,
applicationId: 'yyy',
// version should remain 'init-version', not undefined
})
expect(metrics.get().localStorage).toEqual({ missing: 1 })
})
})

describe('with extractor', () => {
beforeEach(() => {
setCookie(COOKIE_NAME, 'my-version-123', ONE_MINUTE)
Expand Down Expand Up @@ -556,7 +613,7 @@ describe('remoteConfiguration', () => {
extractor: { rcSerializedType: 'regex', value: 'foo' },
},
},
{ version: undefined }
{} // version should not be set when resolution fails
)
})

Expand All @@ -570,7 +627,7 @@ describe('remoteConfiguration', () => {
extractor: { rcSerializedType: 'regex', value: 'Hello(?|!)' },
},
},
{ version: undefined }
{} // version should not be set when resolution fails
)
expect(displaySpy).toHaveBeenCalledWith("Invalid regex in the remote configuration: 'Hello(?|!)'")
})
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ export interface RemoteConfigurationMetrics extends Context {
cookie?: RemoteConfigurationMetricCounters
dom?: RemoteConfigurationMetricCounters
js?: RemoteConfigurationMetricCounters
localStorage?: RemoteConfigurationMetricCounters
}

interface RemoteConfigurationMetricCounters {
Expand Down Expand Up @@ -91,7 +92,10 @@ export function applyRemoteConfiguration(
const appliedConfiguration = { ...initConfiguration } as RumInitConfiguration & { [key: string]: unknown }
SUPPORTED_FIELDS.forEach((option: string) => {
if (option in rumRemoteConfiguration) {
appliedConfiguration[option] = resolveConfigurationProperty(rumRemoteConfiguration[option])
const resolvedValue = resolveConfigurationProperty(rumRemoteConfiguration[option])
if (resolvedValue !== undefined) {
appliedConfiguration[option] = resolvedValue
}
}
})
;(Object.keys(supportedContextManagers) as Array<keyof SupportedContextManagers>).forEach((context) => {
Expand Down Expand Up @@ -149,6 +153,9 @@ export function applyRemoteConfiguration(
case 'js':
resolvedValue = resolveJsValue(property)
break
case 'localStorage':
resolvedValue = resolveLocalStorageValue(property)
break
default:
display.error(`Unsupported remote configuration: "strategy": "${strategy as string}"`)
return
Expand All @@ -166,6 +173,18 @@ export function applyRemoteConfiguration(
return value
}

function resolveLocalStorageValue({ key }: { key: string }) {
let value: string | null
try {
value = localStorage.getItem(key)
} catch {
metrics.increment('localStorage', 'failure')
return
}
metrics.increment('localStorage', value !== null ? 'success' : 'missing')
return value ?? undefined
}

function resolveDomValue({ selector, attribute }: { selector: string; attribute?: string }) {
let element: Element | null
try {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,13 @@ export type DynamicOption =
extractor?: SerializedRegex
[k: string]: unknown
}
| {
rcSerializedType: 'dynamic'
strategy: 'localStorage'
key: string
extractor?: SerializedRegex
[k: string]: unknown
}

/**
* RUM Browser & Mobile SDKs Remote Configuration properties
Expand Down Expand Up @@ -73,6 +80,13 @@ export interface RumSdkConfig {
extractor?: SerializedRegex
[k: string]: unknown
}
| {
rcSerializedType: 'dynamic'
strategy: 'localStorage'
key: string
extractor?: SerializedRegex
[k: string]: unknown
}
/**
* The percentage of sessions tracked
*/
Expand Down
22 changes: 22 additions & 0 deletions packages/rum-core/src/rumEvent.types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,10 @@ export type RumActionEvent = CommonProperties &
* CSS selector path of the target element
*/
readonly selector?: string
/**
* Mobile-only: a globally unique and stable identifier for this UI element, computed as the hash of the element's path (32 lowercase hex characters). Used to correlate actions with mobile session replay wireframes.
*/
readonly permanent_id?: string
/**
* Width of the target element (in pixels)
*/
Expand Down Expand Up @@ -2028,6 +2032,24 @@ export interface ViewPerformanceData {
* URL of the largest contentful paint element
*/
resource_url?: string
/**
* Sub-parts of the LCP
*/
sub_parts?: {
/**
* Time between first_byte and the loading start of the resource associated with the LCP
*/
readonly load_delay: number
/**
* Time to takes to load the resource attached to the LCP
*/
readonly load_time: number
/**
* Time between the LCP resource finishes loading and the LCP element is fully rendered
*/
readonly render_delay: number
[k: string]: unknown
}
[k: string]: unknown
}
/**
Expand Down
20 changes: 20 additions & 0 deletions remote-configuration/rum-sdk-config.json
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,26 @@
"$ref": "#/$defs/SerializedRegex"
}
}
},
{
"type": "object",
"required": ["rcSerializedType", "strategy", "key"],
"properties": {
"rcSerializedType": {
"type": "string",
"const": "dynamic"
},
"strategy": {
"type": "string",
"const": "localStorage"
},
"key": {
"type": "string"
},
"extractor": {
"$ref": "#/$defs/SerializedRegex"
}
}
}
]
},
Expand Down
2 changes: 1 addition & 1 deletion rum-events-format
Loading