-
Notifications
You must be signed in to change notification settings - Fork 163
🐛 Add mask-unless-allowlisted privacy level support for standard attr #3907
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
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' | ||||||
|
@@ -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') | ||||||
|
@@ -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) { | ||||||
|
@@ -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) | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🔨 warning: It's very inefficient to call |
||||||
const attributeValue = element.getAttribute(attribute) | ||||||
return { | ||||||
name: element.getAttribute(attribute) || '', | ||||||
name: | ||||||
enablePrivacyForActionName && nodeSelfPrivacyLevel && shouldMaskNode(element, nodeSelfPrivacyLevel, false) | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I don't think it makes sense to check if
Suggested change
Looking at the bigger picture, it's unfortunate that we do all of the work of calling |
||||||
? maskDisallowedTextContent(attributeValue || '', ACTION_NAME_PLACEHOLDER) | ||||||
: attributeValue || '', | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 } |
||||||
nameSource: ActionNameSource.STANDARD_ATTRIBUTE, | ||||||
} | ||||||
} | ||||||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Instead of adding this extra argument to I think that you should be able to restructure 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. | ||
|
There was a problem hiding this comment.
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 ofelement
considering its ancestors).