Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
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 @@ -643,6 +643,27 @@ describe('getActionNameFromElement', () => {
expectedName: 'foo bar baz',
expectedNameSource: 'text_content',
},
{
html: `
<div data-dd-privacy="mask-unless-allowlisted" aria-label="bar" target>
<span>bar</span>
</div>
`,
defaultPrivacyLevel: NodePrivacyLevel.MASK_UNLESS_ALLOWLISTED,
expectedName: 'Masked Element',
expectedNameSource: 'standard_attribute',
},
{
html: `
<div data-dd-privacy="mask-unless-allowlisted" aria-label="foo" target>
<span>bar</span>
</div>
`,
defaultPrivacyLevel: NodePrivacyLevel.MASK_UNLESS_ALLOWLISTED,
allowlist: ['foo'],
expectedName: 'foo',
expectedNameSource: 'standard_attribute',
},
]
testCases.forEach(({ html, defaultPrivacyLevel, allowlist, expectedName, expectedNameSource }) => {
mockExperimentalFeatures([ExperimentalFeature.USE_TREE_WALKER_FOR_ACTION_NAME])
Expand Down
26 changes: 18 additions & 8 deletions packages/rum-core/src/domain/action/getActionNameFromElement.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { ExperimentalFeature, isExperimentalFeatureEnabled, safeTruncate } from '@datadog/browser-core'
import { getPrivacySelector, NodePrivacyLevel } from '../privacyConstants'
import { getNodePrivacyLevel, shouldMaskNode } from '../privacy'
import { getNodePrivacyLevel, maskDisallowedTextContent, shouldMaskNode } from '../privacy'
import type { NodePrivacyLevelCache } from '../privacy'
import type { RumConfiguration } from '../configuration'
import { isElementNode } from '../../browser/htmlDomUtils'
Expand Down Expand Up @@ -84,7 +84,7 @@ const priorityStrategies: NameStrategy[] = [
return getActionNameFromTextualContent(element, rumConfiguration)
}
},
(element) => getActionNameFromStandardAttribute(element, 'aria-label'),
(element, rumConfiguration) => getActionNameFromStandardAttribute(element, 'aria-label', rumConfiguration),
// associated element text designated by the aria-labelledby attribute
(element, rumConfiguration) => {
const labelledByAttribute = element.getAttribute('aria-labelledby')
Expand All @@ -100,10 +100,10 @@ const priorityStrategies: NameStrategy[] = [
}
}
},
(element) => getActionNameFromStandardAttribute(element, 'alt'),
(element) => getActionNameFromStandardAttribute(element, 'name'),
(element) => getActionNameFromStandardAttribute(element, 'title'),
(element) => getActionNameFromStandardAttribute(element, 'placeholder'),
(element, rumConfiguration) => getActionNameFromStandardAttribute(element, 'alt', rumConfiguration),
(element, rumConfiguration) => getActionNameFromStandardAttribute(element, 'name', rumConfiguration),
(element, rumConfiguration) => getActionNameFromStandardAttribute(element, 'title', rumConfiguration),
(element, rumConfiguration) => getActionNameFromStandardAttribute(element, 'placeholder', rumConfiguration),
// SELECT first OPTION text
(element, rumConfiguration) => {
if ('options' in element && element.options.length > 0) {
Expand Down Expand Up @@ -169,9 +169,19 @@ function getElementById(refElement: Element, id: string) {
return refElement.ownerDocument ? refElement.ownerDocument.getElementById(id) : null
}

function getActionNameFromStandardAttribute(element: Element | HTMLElement, attribute: string): ActionName {
function getActionNameFromStandardAttribute(
element: Element | HTMLElement,
attribute: string,
rumConfiguration: RumConfiguration
): ActionName {
const { enablePrivacyForActionName, defaultPrivacyLevel } = rumConfiguration
const nodeSelfPrivacyLevel = getNodePrivacyLevel(element, defaultPrivacyLevel)
const attributeValue = element.getAttribute(attribute)
return {
name: element.getAttribute(attribute) || '',
name:
enablePrivacyForActionName && nodeSelfPrivacyLevel && shouldMaskNode(element, nodeSelfPrivacyLevel, false)
? maskDisallowedTextContent(attributeValue || '', ACTION_NAME_PLACEHOLDER)
: attributeValue || '',
nameSource: ActionNameSource.STANDARD_ATTRIBUTE,
}
}
Expand Down
5 changes: 4 additions & 1 deletion packages/rum-core/src/domain/privacy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -128,13 +128,16 @@ export function getNodeSelfPrivacyLevel(node: Node): NodePrivacyLevel | undefine
* Other `shouldMaskNode` cases are edge cases that should not matter too much (ex: should we mask a
* node if it is ignored or hidden? it doesn't matter since it won't be serialized).
*/
export function shouldMaskNode(node: Node, privacyLevel: NodePrivacyLevel) {
export function shouldMaskNode(node: Node, privacyLevel: NodePrivacyLevel, isTextContent: boolean = true) {
Copy link
Contributor

Choose a reason for hiding this comment

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

Instead of adding this extra argument to shouldMaskNode(), I would suggest creating a new shouldMaskAttribute() function in this file and migrating as much of the logic as you can from serializeAttribute() in serializeAttribute.ts into shouldMaskAttribute(). This would give us a central place to control privacy for attributes, similar to what we already have for nodes, instead of spreading the logic between multiple places in the code.

I think that you should be able to restructure serializeAttribute() so that it more or less looks like this:

export function serializeAttribute() {
  if (/* HIDDEN */) {
    // Handle HIDDEN case.
    return ...
  }
  if (shouldMaskAttribute(...)) {
    if (/* is one of the image cases */) {
      // Handle image cases.
      return ...
    }
    // We're in a non-image case.
    return CENSORED_STRING_MARK 
  }
  // Handle non-HIDDEN, non-masked cases...
  return
}

With this setup, we'll ensure that action names and session replay handle attribute masking in a consistent way.

switch (privacyLevel) {
case NodePrivacyLevel.MASK:
case NodePrivacyLevel.HIDDEN:
case NodePrivacyLevel.IGNORE:
return true
case NodePrivacyLevel.MASK_UNLESS_ALLOWLISTED:
if (!isTextContent) {
return true
}
if (isTextNode(node)) {
// Always return true if our parent is a form element, like MASK_USER_INPUT.
// Otherwise, decide whether to mask based on the allowlist.
Expand Down