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 @@ -509,6 +509,46 @@ describe('remoteConfiguration', () => {
})
})

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 resolve to undefined if the key is missing', () => {
expectAppliedRemoteConfigurationToBe(
{ version: { rcSerializedType: 'dynamic', strategy: 'localStorage', key: 'non_existent_key' } },
{ version: undefined }
)
expect(metrics.get().localStorage).toEqual({ missing: 1 })
})
})

describe('with extractor', () => {
beforeEach(() => {
setCookie(COOKIE_NAME, 'my-version-123', ONE_MINUTE)
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 @@ -149,6 +150,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 +170,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
87 changes: 87 additions & 0 deletions test/e2e/scenario/rum/remoteConfiguration.scenario.ts
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,93 @@ test.describe('remote configuration', () => {
expect(initConfiguration.version).toBe('js-version')
})

createTest('should resolve an option value from localStorage')
.withRum({
remoteConfigurationId: 'e2e',
})
.withRemoteConfiguration({
rum: {
applicationId: 'e2e',
version: { rcSerializedType: 'dynamic', strategy: 'localStorage', key: 'dd_app_version' },
},
})
.withBody(html`
<script>
localStorage.setItem('dd_app_version', 'localStorage-version')
</script>
`)
.run(async ({ page }) => {
const initConfiguration = await page.evaluate(() => window.DD_RUM!.getInitConfiguration()!)
expect(initConfiguration.version).toBe('localStorage-version')
})

createTest('should resolve an option value from localStorage with an extractor')
.withRum({
remoteConfigurationId: 'e2e',
})
.withRemoteConfiguration({
rum: {
applicationId: 'e2e',
version: {
rcSerializedType: 'dynamic',
strategy: 'localStorage',
key: 'dd_app_version',
extractor: { rcSerializedType: 'regex', value: '\\d+\\.\\d+\\.\\d+' },
},
},
})
.withBody(html`
<script>
localStorage.setItem('dd_app_version', 'version-1.2.3-beta')
</script>
`)
.run(async ({ page }) => {
const initConfiguration = await page.evaluate(() => window.DD_RUM!.getInitConfiguration()!)
expect(initConfiguration.version).toBe('1.2.3')
})

createTest('should resolve to undefined when localStorage key is missing')
.withRum({
remoteConfigurationId: 'e2e',
version: 'fallback-version',
})
.withRemoteConfiguration({
rum: {
applicationId: 'e2e',
version: { rcSerializedType: 'dynamic', strategy: 'localStorage', key: 'non_existent_key' },
},
})
.run(async ({ page }) => {
const initConfiguration = await page.evaluate(() => window.DD_RUM!.getInitConfiguration()!)
expect(initConfiguration.version).toBe('fallback-version')
})

createTest('should handle localStorage access failure gracefully')
.withRum({
remoteConfigurationId: 'e2e',
version: 'fallback-version',
})
.withRemoteConfiguration({
rum: {
applicationId: 'e2e',
version: { rcSerializedType: 'dynamic', strategy: 'localStorage', key: 'dd_app_version' },
},
})
.withBody(html`
<script>
Object.defineProperty(window, 'localStorage', {
get: function () {
throw new Error('localStorage is not available')
},
configurable: true,
})
</script>
`)
.run(async ({ page }) => {
const initConfiguration = await page.evaluate(() => window.DD_RUM!.getInitConfiguration()!)
expect(initConfiguration.version).toBe('fallback-version')
})

createTest('should resolve user context')
.withRum({
remoteConfigurationId: 'e2e',
Expand Down