11import { ExperimentalFeature , isExperimentalFeatureEnabled , safeTruncate } from '@datadog/browser-core'
22import { getPrivacySelector , NodePrivacyLevel } from '../privacyConstants'
3- import { getNodePrivacyLevel , shouldMaskNode } from '../privacy'
3+ import { getNodePrivacyLevel , maskDisallowedTextContent , shouldMaskNode , shouldMaskAttribute } from '../privacy'
44import type { NodePrivacyLevelCache } from '../privacy'
55import type { RumConfiguration } from '../configuration'
66import { isElementNode } from '../../browser/htmlDomUtils'
@@ -16,6 +16,8 @@ export function getActionNameFromElement(
1616 rumConfiguration : RumConfiguration ,
1717 nodePrivacyLevel : NodePrivacyLevel = NodePrivacyLevel . ALLOW
1818) : ActionName {
19+ const nodePrivacyLevelCache : NodePrivacyLevelCache = new Map ( )
20+
1921 const { actionNameAttribute : userProgrammaticAttribute } = rumConfiguration
2022
2123 // Proceed to get the action name in two steps:
@@ -36,8 +38,8 @@ export function getActionNameFromElement(
3638 }
3739
3840 return (
39- getActionNameFromElementForStrategies ( element , priorityStrategies , rumConfiguration ) ||
40- getActionNameFromElementForStrategies ( element , fallbackStrategies , rumConfiguration ) || {
41+ getActionNameFromElementForStrategies ( element , priorityStrategies , rumConfiguration , nodePrivacyLevelCache ) ||
42+ getActionNameFromElementForStrategies ( element , fallbackStrategies , rumConfiguration , nodePrivacyLevelCache ) || {
4143 name : '' ,
4244 nameSource : ActionNameSource . BLANK ,
4345 }
@@ -58,14 +60,15 @@ function getActionNameFromElementProgrammatically(targetElement: Element, progra
5860
5961type NameStrategy = (
6062 element : Element | HTMLElement | HTMLInputElement | HTMLSelectElement ,
61- rumConfiguration : RumConfiguration
63+ rumConfiguration : RumConfiguration ,
64+ nodePrivacyLevelCache : NodePrivacyLevelCache
6265) => ActionName | undefined | null
6366
6467const priorityStrategies : NameStrategy [ ] = [
6568 // associated LABEL text
66- ( element , rumConfiguration ) => {
69+ ( element , rumConfiguration , nodePrivacyLevelCache ) => {
6770 if ( 'labels' in element && element . labels && element . labels . length > 0 ) {
68- return getActionNameFromTextualContent ( element . labels [ 0 ] , rumConfiguration )
71+ return getActionNameFromTextualContent ( element . labels [ 0 ] , rumConfiguration , nodePrivacyLevelCache )
6972 }
7073 } ,
7174 // INPUT button (and associated) value
@@ -79,41 +82,47 @@ const priorityStrategies: NameStrategy[] = [
7982 }
8083 } ,
8184 // BUTTON, LABEL or button-like element text
82- ( element , rumConfiguration ) => {
85+ ( element , rumConfiguration , nodePrivacyLevelCache ) => {
8386 if ( element . nodeName === 'BUTTON' || element . nodeName === 'LABEL' || element . getAttribute ( 'role' ) === 'button' ) {
84- return getActionNameFromTextualContent ( element , rumConfiguration )
87+ return getActionNameFromTextualContent ( element , rumConfiguration , nodePrivacyLevelCache )
8588 }
8689 } ,
87- ( element ) => getActionNameFromStandardAttribute ( element , 'aria-label' ) ,
90+ ( element , rumConfiguration , nodePrivacyLevelCache ) =>
91+ getActionNameFromStandardAttribute ( element , 'aria-label' , rumConfiguration , nodePrivacyLevelCache ) ,
8892 // associated element text designated by the aria-labelledby attribute
89- ( element , rumConfiguration ) => {
93+ ( element , rumConfiguration , nodePrivacyLevelCache ) => {
9094 const labelledByAttribute = element . getAttribute ( 'aria-labelledby' )
9195 if ( labelledByAttribute ) {
9296 return {
9397 name : labelledByAttribute
9498 . split ( / \s + / )
9599 . map ( ( id ) => getElementById ( element , id ) )
96100 . filter ( ( label ) : label is HTMLElement => Boolean ( label ) )
97- . map ( ( element ) => getTextualContent ( element , rumConfiguration ) )
101+ . map ( ( element ) => getTextualContent ( element , rumConfiguration , nodePrivacyLevelCache ) )
98102 . join ( ' ' ) ,
99103 nameSource : ActionNameSource . TEXT_CONTENT ,
100104 }
101105 }
102106 } ,
103- ( element ) => getActionNameFromStandardAttribute ( element , 'alt' ) ,
104- ( element ) => getActionNameFromStandardAttribute ( element , 'name' ) ,
105- ( element ) => getActionNameFromStandardAttribute ( element , 'title' ) ,
106- ( element ) => getActionNameFromStandardAttribute ( element , 'placeholder' ) ,
107+ ( element , rumConfiguration , nodePrivacyLevelCache ) =>
108+ getActionNameFromStandardAttribute ( element , 'alt' , rumConfiguration , nodePrivacyLevelCache ) ,
109+ ( element , rumConfiguration , nodePrivacyLevelCache ) =>
110+ getActionNameFromStandardAttribute ( element , 'name' , rumConfiguration , nodePrivacyLevelCache ) ,
111+ ( element , rumConfiguration , nodePrivacyLevelCache ) =>
112+ getActionNameFromStandardAttribute ( element , 'title' , rumConfiguration , nodePrivacyLevelCache ) ,
113+ ( element , rumConfiguration , nodePrivacyLevelCache ) =>
114+ getActionNameFromStandardAttribute ( element , 'placeholder' , rumConfiguration , nodePrivacyLevelCache ) ,
107115 // SELECT first OPTION text
108- ( element , rumConfiguration ) => {
116+ ( element , rumConfiguration , nodePrivacyLevelCache ) => {
109117 if ( 'options' in element && element . options . length > 0 ) {
110- return getActionNameFromTextualContent ( element . options [ 0 ] , rumConfiguration )
118+ return getActionNameFromTextualContent ( element . options [ 0 ] , rumConfiguration , nodePrivacyLevelCache )
111119 }
112120 } ,
113121]
114122
115123const fallbackStrategies : NameStrategy [ ] = [
116- ( element , rumConfiguration ) => getActionNameFromTextualContent ( element , rumConfiguration ) ,
124+ ( element , rumConfiguration , nodePrivacyLevelCache ) =>
125+ getActionNameFromTextualContent ( element , rumConfiguration , nodePrivacyLevelCache ) ,
117126]
118127
119128/**
@@ -124,7 +133,8 @@ const MAX_PARENTS_TO_CONSIDER = 10
124133function getActionNameFromElementForStrategies (
125134 targetElement : Element ,
126135 strategies : NameStrategy [ ] ,
127- rumConfiguration : RumConfiguration
136+ rumConfiguration : RumConfiguration ,
137+ nodePrivacyLevelCache : NodePrivacyLevelCache
128138) {
129139 let element : Element | null = targetElement
130140 let recursionCounter = 0
@@ -136,7 +146,7 @@ function getActionNameFromElementForStrategies(
136146 element . nodeName !== 'HEAD'
137147 ) {
138148 for ( const strategy of strategies ) {
139- const actionName = strategy ( element , rumConfiguration )
149+ const actionName = strategy ( element , rumConfiguration , nodePrivacyLevelCache )
140150 if ( actionName ) {
141151 const { name, nameSource } = actionName
142152 const trimmedName = name && name . trim ( )
@@ -169,24 +179,45 @@ function getElementById(refElement: Element, id: string) {
169179 return refElement . ownerDocument ? refElement . ownerDocument . getElementById ( id ) : null
170180}
171181
172- function getActionNameFromStandardAttribute ( element : Element | HTMLElement , attribute : string ) : ActionName {
182+ function getActionNameFromStandardAttribute (
183+ element : Element | HTMLElement ,
184+ attribute : string ,
185+ rumConfiguration : RumConfiguration ,
186+ nodePrivacyLevelCache : NodePrivacyLevelCache
187+ ) : ActionName {
188+ const { enablePrivacyForActionName, defaultPrivacyLevel } = rumConfiguration
189+ let attributeValue = element . getAttribute ( attribute )
190+ if ( attributeValue && enablePrivacyForActionName ) {
191+ const nodePrivacyLevel = getNodePrivacyLevel ( element , defaultPrivacyLevel , nodePrivacyLevelCache )
192+ if ( shouldMaskAttribute ( element . tagName , attribute , attributeValue , nodePrivacyLevel , rumConfiguration ) ) {
193+ attributeValue = maskDisallowedTextContent ( attributeValue , ACTION_NAME_PLACEHOLDER )
194+ }
195+ } else if ( ! attributeValue ) {
196+ attributeValue = ''
197+ }
198+
173199 return {
174- name : element . getAttribute ( attribute ) || '' ,
200+ name : attributeValue ,
175201 nameSource : ActionNameSource . STANDARD_ATTRIBUTE ,
176202 }
177203}
178204
179205function getActionNameFromTextualContent (
180206 element : Element | HTMLElement ,
181- rumConfiguration : RumConfiguration
207+ rumConfiguration : RumConfiguration ,
208+ nodePrivacyLevelCache : NodePrivacyLevelCache
182209) : ActionName {
183210 return {
184- name : getTextualContent ( element , rumConfiguration ) || '' ,
211+ name : getTextualContent ( element , rumConfiguration , nodePrivacyLevelCache ) || '' ,
185212 nameSource : ActionNameSource . TEXT_CONTENT ,
186213 }
187214}
188215
189- function getTextualContent ( element : Element , rumConfiguration : RumConfiguration ) {
216+ function getTextualContent (
217+ element : Element ,
218+ rumConfiguration : RumConfiguration ,
219+ nodePrivacyLevelCache : NodePrivacyLevelCache
220+ ) {
190221 if ( ( element as HTMLElement ) . isContentEditable ) {
191222 return
192223 }
@@ -202,7 +233,8 @@ function getTextualContent(element: Element, rumConfiguration: RumConfiguration)
202233 element ,
203234 userProgrammaticAttribute ,
204235 enablePrivacyForActionName ,
205- defaultPrivacyLevel
236+ defaultPrivacyLevel ,
237+ nodePrivacyLevelCache
206238 )
207239 }
208240
@@ -246,10 +278,9 @@ function getTextualContentWithTreeWalker(
246278 element : Element ,
247279 userProgrammaticAttribute : string | undefined ,
248280 privacyEnabledActionName : boolean ,
249- defaultPrivacyLevel : NodePrivacyLevel
281+ defaultPrivacyLevel : NodePrivacyLevel ,
282+ nodePrivacyLevelCache : NodePrivacyLevelCache
250283) {
251- const nodePrivacyLevelCache : NodePrivacyLevelCache = new Map ( )
252-
253284 const walker = document . createTreeWalker (
254285 element ,
255286 // eslint-disable-next-line no-bitwise
0 commit comments