Skip to content

Conversation

@cy-moi
Copy link
Contributor

@cy-moi cy-moi commented Oct 10, 2025

Motivation

Following up on the new mask-unless-allowlisted behavior, in which case we mask with allowlists when shouldMaskNode is true and enablePrivacyForActionName turned on. However, it seems like we did not cover standard attributes.

Changes

Make masking for standard attributes the same as the textual content.

Test instructions

Should pass CI
Note: This PR has a dependency on https://github.com/DataDog/web-ui/pull/237816 when testing on staging.

Checklist

  • Tested locally
  • Tested on staging
  • Added unit tests for this change.
  • Added e2e/integration tests for this change.

@cy-moi cy-moi force-pushed the congyao/fix-mask-unless-allowlist-for-standard-attr branch from ebfde81 to 1ad9e51 Compare October 10, 2025 15:29
@cit-pr-commenter
Copy link

cit-pr-commenter bot commented Oct 10, 2025

Bundles Sizes Evolution

📦 Bundle Name Base Size Local Size 𝚫 𝚫% Status
Rum 162.08 KiB 162.68 KiB +616 B +0.37%
Rum Recorder 19.78 KiB 19.56 KiB -233 B -1.15%
Rum Profiler 4.89 KiB 4.89 KiB 0 B 0.00%
Logs 55.77 KiB 55.77 KiB 0 B 0.00%
Flagging 944 B 944 B 0 B 0.00%
Rum Slim 119.06 KiB 119.71 KiB +666 B +0.55%
Worker 23.60 KiB 23.60 KiB 0 B 0.00%
🚀 CPU Performance
Action Name Base CPU Time (ms) Local CPU Time (ms) 𝚫%
RUM - add global context 0.0045 0.0041 -8.89%
RUM - add action 0.014 0.0119 -15.00%
RUM - add error 0.0122 0.0124 +1.64%
RUM - add timing 0.003 0.0027 -10.00%
RUM - start view 0.0037 0.0032 -13.51%
RUM - start/stop session replay recording 0.001 0.0007 -30.00%
Logs - log message 0.0176 0.0133 -24.43%
🧠 Memory Performance
Action Name Base Memory Consumption Local Memory Consumption 𝚫
RUM - add global context 25.46 KiB 25.88 KiB +429 B
RUM - add action 45.97 KiB 46.44 KiB +481 B
RUM - add timing 24.34 KiB 24.95 KiB +625 B
RUM - add error 51.44 KiB 50.61 KiB -846 B
RUM - start/stop session replay recording 24.89 KiB 23.84 KiB -1.05 KiB
RUM - start view 422.83 KiB 428.81 KiB +5.98 KiB
Logs - log message 43.14 KiB 42.09 KiB -1.05 KiB

🔗 RealWorld

@datadog-official
Copy link

datadog-official bot commented Oct 10, 2025

✅ Tests

🎉 All green!

❄️ No new flaky tests detected
🧪 All tests passed

🎯 Code Coverage
Patch Coverage: 100.00%
Total Coverage: 92.69% (+0.05%)

View detailed report

This comment will be updated automatically if new data arrives.
🔗 Commit SHA: 16c8de1 | Docs | Datadog PR Page | Was this helpful? Give us feedback!

@cy-moi cy-moi force-pushed the congyao/fix-mask-unless-allowlist-for-standard-attr branch 2 times, most recently from 0913df3 to 168b5d1 Compare October 13, 2025 10:59
@cy-moi cy-moi changed the title Add mask-unless-allowlisted privacy level support for standard attr 🐛 Add mask-unless-allowlisted privacy level support for standard attr Oct 13, 2025
@cy-moi cy-moi force-pushed the congyao/fix-mask-unless-allowlist-for-standard-attr branch from 168b5d1 to 71674a9 Compare October 13, 2025 11:00
@cy-moi
Copy link
Contributor Author

cy-moi commented Oct 13, 2025

/to-staging

@dd-devflow-routing-codex
Copy link

dd-devflow-routing-codex bot commented Oct 13, 2025

View all feedbacks in Devflow UI.

2025-10-13 11:18:14 UTC ℹ️ Start processing command /to-staging


2025-10-13 11:18:20 UTC ℹ️ Branch Integration: starting soon, merge expected in approximately 0s (p90)

Commit 71674a9ec7 will soon be integrated into staging-42.


2025-10-13 11:29:20 UTC ℹ️ Branch Integration: this commit was successfully integrated

Commit 71674a9ec7 has been merged into staging-42 in merge commit 58ffb86f27.

Check out the triggered pipeline on Gitlab 🦊

If you need to revert this integration, you can use the following command: /code revert-integration -b staging-42

dd-mergequeue bot added a commit that referenced this pull request Oct 13, 2025
… into staging-42

Integrated commit sha: 71674a9

Co-authored-by: cy-moi <[email protected]>
@cy-moi cy-moi force-pushed the congyao/fix-mask-unless-allowlist-for-standard-attr branch from 9932c44 to 06574d5 Compare October 13, 2025 13:30
@cy-moi cy-moi marked this pull request as ready for review October 14, 2025 09:40
@cy-moi cy-moi requested a review from a team as a code owner October 14, 2025 09:40
rumConfiguration: RumConfiguration
): ActionName {
const { enablePrivacyForActionName, defaultPrivacyLevel } = rumConfiguration
const nodeSelfPrivacyLevel = getNodePrivacyLevel(element, defaultPrivacyLevel)
Copy link
Contributor

Choose a reason for hiding this comment

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

Nit: I would avoid calling this nodeSelfPrivacyLevel, because that phrase has a particular meaning in the code (it's the privacy level of a node ignoring its ancestors), and this variable has a different meaning (it contains the privacy level of element considering its ancestors).

Suggested change
const nodeSelfPrivacyLevel = getNodePrivacyLevel(element, defaultPrivacyLevel)
const nodePrivacyLevel = getNodePrivacyLevel(element, defaultPrivacyLevel)

return {
name: element.getAttribute(attribute) || '',
name:
enablePrivacyForActionName && nodeSelfPrivacyLevel && shouldMaskNode(element, nodeSelfPrivacyLevel, false)
Copy link
Contributor

Choose a reason for hiding this comment

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

I don't think it makes sense to check if nodeSelfPrivacyLevel is truthy, because getNodePrivacyLevel always returns a valid privacy level:

Suggested change
enablePrivacyForActionName && nodeSelfPrivacyLevel && shouldMaskNode(element, nodeSelfPrivacyLevel, false)
enablePrivacyForActionName && shouldMaskNode(element, nodeSelfPrivacyLevel, false)

Looking at the bigger picture, it's unfortunate that we do all of the work of calling getNodePrivacyLevel() (which can be a bit expensive) and then ignore the result if enablePrivacyForActionName is false. It would be better to restructure this function so that you check enablePrivacyForActionName first, and then only call getNodePrivacyLevel() if enablePrivacyForActionName is true.

* 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.

rumConfiguration: RumConfiguration
): ActionName {
const { enablePrivacyForActionName, defaultPrivacyLevel } = rumConfiguration
const nodeSelfPrivacyLevel = getNodePrivacyLevel(element, defaultPrivacyLevel)
Copy link
Member

Choose a reason for hiding this comment

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

🔨 warning: ‏It's very inefficient to call getNodePrivacyLevel over and over again for each attribute / element ancestors. You could use the cache argument to make it faster.

name:
enablePrivacyForActionName && nodeSelfPrivacyLevel && shouldMaskNode(element, nodeSelfPrivacyLevel, false)
? maskDisallowedTextContent(attributeValue || '', ACTION_NAME_PLACEHOLDER)
: attributeValue || '',
Copy link
Member

Choose a reason for hiding this comment

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

💬 suggestion: ‏maybe don't do the expensive operations if the attribute is missing

let attributeValue = element.getAttribute(attribute)
if (attributeValue) {
  const nodePrivacyLevel = ...
  etc.
} else {
  attributeValue = ''
}
return { name: attributeValue, nameSource: ActionNameSource.STANDARD_ATTRIBUTE }

@cy-moi cy-moi force-pushed the congyao/fix-mask-unless-allowlist-for-standard-attr branch from 40849d8 to 16c8de1 Compare October 29, 2025 14:33
Comment on lines +190 to +192
let nodeSelfPrivacyLevel
if (attributeValue) {
nodeSelfPrivacyLevel = getNodePrivacyLevel(element, defaultPrivacyLevel, nodePrivacyLevelCache)
Copy link
Member

Choose a reason for hiding this comment

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

Suggestion:

  • It should be nodePrivacyLevel, not nodeSelfPrivacyLevel
  • You can use const since you only define it once
  • You can shortcut the whole block with && enablePrivacyForActionName
  if (attributeValue && enablePrivacyForActionName) {
    const nodePrivacyLevel = getNodePrivacyLevel(element, defaultPrivacyLevel, nodePrivacyLevelCache)

Comment on lines +169 to +188
switch (attributeName) {
case 'title':
case 'alt':
case 'name':
case 'placeholder':
case 'aria-label':
return true
}
if (tagName === 'A' && attributeName === 'href') {
return true
}
if (tagName === 'IFRAME' && attributeName === 'srcdoc') {
return true
}
if (attributeValue && attributeName.startsWith('data-')) {
return true
}
if (tagName === 'IMG' || tagName === 'SOURCE') {
return true
}
Copy link
Member

Choose a reason for hiding this comment

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

🔨 warning: ‏you changed the logic while moving the code: it makes it hard to catch the exact changes here. Don't hesitate to use multiple commits.

The logic changed and now:

  • name and aria-label attributes are masked. Could you add it to the "change" section of the PR? And explain the motivation.
  • all attributes of img and source elements are masked. This sounds quite problematic, because attributes like class or id will be masked and it will break the replay.

Copy link
Contributor

Choose a reason for hiding this comment

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

+1. @cy-moi , it might make sense to make the name and aria-label changes in a separate PR, since this is a policy change and not just a refactor. I do think it's a good idea! But making it in a separate PR could have several advantages:

  • Easier to review.
  • Easier to back out the change if we discover there's a reason to do so.
  • It'd be clearly announced in the changelog.

Re: the img and source changes, I think that's probably just a bug, right? The original code in serializeAttribute.ts masks the src and srcset on img and source elements, but it doesn't mask all attributes on those elements.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants