Skip to content

Commit 6c3e757

Browse files
♻️ [PANA-5105] Serialize all DOM attribute values as strings (#3999)
1 parent 1f994b9 commit 6c3e757

File tree

4 files changed

+22
-45
lines changed

4 files changed

+22
-45
lines changed

packages/rum/src/domain/record/serialization/htmlAst.specHelper.ts

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1438,7 +1438,7 @@ export const AST_ALLOW = {
14381438
type: 2,
14391439
tagName: 'option',
14401440
attributes: {
1441-
selected: true,
1441+
selected: '',
14421442
'aria-label': 'A',
14431443
value: 'private option A',
14441444
},
@@ -1526,7 +1526,7 @@ export const AST_ALLOW = {
15261526
type: 'checkbox',
15271527
name: 'inputFoo',
15281528
value: 'on',
1529-
checked: true,
1529+
checked: '',
15301530
},
15311531
childNodes: [],
15321532
},
@@ -1558,7 +1558,6 @@ export const AST_ALLOW = {
15581558
type: 'radio',
15591559
name: 'radioGroup',
15601560
value: 'bar-private',
1561-
checked: false,
15621561
},
15631562
childNodes: [],
15641563
},

packages/rum/src/domain/record/serialization/serializeAttributes.spec.ts

Lines changed: 6 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -61,19 +61,9 @@ describe('serializeDOMAttributes', () => {
6161
// serialized at all. Used for boolean form element attributes like `<option selected>`.
6262
// - 'maskable-image': Like 'maskable', except that we expect the masked
6363
// representation to be a data URL image.
64-
// - 'maskable-option-selected': Like 'maskable-boolean', in that when unmasked the
65-
// representation is a boolean value. However, when the value is masked, instead of
66-
// not being serialized, its underlying string value is used. The effect is a weird
67-
// hybrid of 'always-unmasked' and 'maskable-boolean'.
68-
// TODO: Reduce the complexity here by aligning 'maskable-form',
69-
// 'maskable-form-boolean', and 'maskable-option-selected'.
70-
expectedBehavior:
71-
| 'always-unmasked'
72-
| 'maskable'
73-
| 'maskable-form'
74-
| 'maskable-form-boolean'
75-
| 'maskable-image'
76-
| 'maskable-option-selected'
64+
// TODO: Reduce the complexity here by aligning 'maskable-form' and
65+
// 'maskable-form-boolean'.
66+
expectedBehavior: 'always-unmasked' | 'maskable' | 'maskable-form' | 'maskable-form-boolean' | 'maskable-image'
7767

7868
// How to treat the IGNORE privacy level. The default is 'unmasked'.
7969
// TODO: Eliminate this inconsistency by always masking for IGNORE.
@@ -225,10 +215,7 @@ describe('serializeDOMAttributes', () => {
225215
// when masked, the entire attribute should not be serialized.
226216
{
227217
html: '<option selected>',
228-
// TODO: This is a bug! If the <option> is masked, we don't set the value of
229-
// 'selected' based on the property, but we still allow the DOM attribute to be
230-
// recorded. We should fix this; this should be 'maskable-boolean'.
231-
expectedBehavior: 'maskable-option-selected',
218+
expectedBehavior: 'maskable-form-boolean',
232219
ignoreBehavior: 'masked',
233220
},
234221
{
@@ -285,13 +272,7 @@ describe('serializeDOMAttributes', () => {
285272
attribute: { name: string; value: string | undefined },
286273
privacyLevel: NodePrivacyLevel
287274
): boolean | string | undefined {
288-
let unmaskedValue: boolean | string | undefined = testCase.attrValue ?? attribute.value
289-
if (
290-
testCase.expectedBehavior === 'maskable-form-boolean' ||
291-
testCase.expectedBehavior === 'maskable-option-selected'
292-
) {
293-
unmaskedValue = attribute.value === ''
294-
}
275+
const unmaskedValue: boolean | string | undefined = testCase.attrValue ?? attribute.value
295276

296277
let maskedValue: boolean | string | undefined
297278
if (testCase.expectedBehavior === 'maskable-form') {
@@ -300,8 +281,6 @@ describe('serializeDOMAttributes', () => {
300281
maskedValue = undefined
301282
} else if (testCase.expectedBehavior === 'maskable-image') {
302283
maskedValue = CENSORED_IMG_MARK
303-
} else if (testCase.expectedBehavior === 'maskable-option-selected') {
304-
maskedValue = attribute.value
305284
} else {
306285
maskedValue = CENSORED_STRING_MARK
307286
}
@@ -346,7 +325,7 @@ describe('serializeDOMAttributes', () => {
346325
// behavior; it shouldn't behave differently because Element#tagName is lowercase.
347326
;(element as any).selected = true
348327
const attributes = serializeDOMAttributes(element, NodePrivacyLevel.ALLOW, transaction)
349-
expect(attributes['selected']).toBe(true)
328+
expect(attributes['selected']).toBe('')
350329
})
351330
})
352331

packages/rum/src/domain/record/serialization/serializeAttributes.ts

Lines changed: 11 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ export function serializeAttributes(
1010
element: Element,
1111
nodePrivacyLevel: NodePrivacyLevel,
1212
transaction: SerializationTransaction
13-
): Record<string, boolean | number | string> {
13+
): Record<string, number | string> {
1414
return {
1515
...serializeDOMAttributes(element, nodePrivacyLevel, transaction),
1616
...serializeVirtualAttributes(element, nodePrivacyLevel, transaction),
@@ -21,12 +21,12 @@ export function serializeDOMAttributes(
2121
element: Element,
2222
nodePrivacyLevel: NodePrivacyLevel,
2323
transaction: SerializationTransaction
24-
): Record<string, boolean | string> {
24+
): Record<string, string> {
2525
if (nodePrivacyLevel === NodePrivacyLevel.HIDDEN) {
2626
return {}
2727
}
2828

29-
const attrs: Record<string, string | boolean> = {}
29+
const attrs: Record<string, string> = {}
3030
const tagName = normalizedTagName(element)
3131

3232
for (let i = 0; i < element.attributes.length; i += 1) {
@@ -51,11 +51,12 @@ export function serializeDOMAttributes(
5151
/**
5252
* <Option> can be selected, which occurs if its `value` matches ancestor `<Select>.value`
5353
*/
54-
if (tagName === 'option' && nodePrivacyLevel === NodePrivacyLevel.ALLOW) {
55-
// For privacy=`MASK`, all the values would be the same, so skip.
54+
if (tagName === 'option') {
5655
const optionElement = element as HTMLOptionElement
57-
if (optionElement.selected) {
58-
attrs.selected = optionElement.selected
56+
if (optionElement.selected && !shouldMaskNode(optionElement, nodePrivacyLevel)) {
57+
attrs.selected = ''
58+
} else {
59+
delete attrs.selected
5960
}
6061
}
6162

@@ -69,9 +70,9 @@ export function serializeDOMAttributes(
6970
*/
7071
const inputElement = element as HTMLInputElement
7172
if (tagName === 'input' && (inputElement.type === 'radio' || inputElement.type === 'checkbox')) {
72-
if (nodePrivacyLevel === NodePrivacyLevel.ALLOW) {
73-
attrs.checked = !!inputElement.checked
74-
} else if (shouldMaskNode(inputElement, nodePrivacyLevel)) {
73+
if (inputElement.checked && !shouldMaskNode(inputElement, nodePrivacyLevel)) {
74+
attrs.checked = ''
75+
} else {
7576
delete attrs.checked
7677
}
7778
}

packages/rum/src/domain/record/serialization/serializeNode.spec.ts

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -239,7 +239,7 @@ describe('serializeNodeWithId', () => {
239239
jasmine.objectContaining({
240240
attributes: {
241241
value: 'bar',
242-
selected: true,
242+
selected: '',
243243
},
244244
}),
245245
],
@@ -271,7 +271,6 @@ describe('serializeNodeWithId', () => {
271271
attributes: {
272272
type: 'checkbox',
273273
value: 'on',
274-
checked: false,
275274
},
276275
})
277276
)
@@ -283,7 +282,7 @@ describe('serializeNodeWithId', () => {
283282
attributes: {
284283
type: 'checkbox',
285284
value: 'on',
286-
checked: true,
285+
checked: '',
287286
},
288287
})
289288
)
@@ -295,15 +294,14 @@ describe('serializeNodeWithId', () => {
295294
expect(serializeNodeWithId(radio, NodePrivacyLevel.ALLOW, transaction)?.attributes).toEqual({
296295
type: 'radio',
297296
value: 'on',
298-
checked: false,
299297
})
300298

301299
radio.checked = true
302300

303301
expect(serializeNodeWithId(radio, NodePrivacyLevel.ALLOW, transaction)?.attributes).toEqual({
304302
type: 'radio',
305303
value: 'on',
306-
checked: true,
304+
checked: '',
307305
})
308306
})
309307

0 commit comments

Comments
 (0)