1- import { safeTruncate } from '@datadog/browser-core'
2- import { NodePrivacyLevel , getPrivacySelector } from '../privacy'
1+ import { ExperimentalFeature , isExperimentalFeatureEnabled , safeTruncate } from '@datadog/browser-core'
2+ import { getNodeSelfPrivacyLevel , getPrivacySelector , NodePrivacyLevel , shouldMaskNode } from '../privacy'
33import type { RumConfiguration } from '../configuration'
4+ import { isElementNode } from '../../browser/htmlDomUtils'
45
56/**
67 * Get the action name from the attribute 'data-dd-action-name' on the element or any of its parent.
@@ -23,7 +24,7 @@ interface ActionName {
2324export function getActionNameFromElement (
2425 element : Element ,
2526 { enablePrivacyForActionName, actionNameAttribute : userProgrammaticAttribute } : RumConfiguration ,
26- nodePrivacyLevel ? : NodePrivacyLevel
27+ nodePrivacyLevel : NodePrivacyLevel = NodePrivacyLevel . ALLOW
2728) : ActionName {
2829 // Proceed to get the action name in two steps:
2930 // * first, get the name programmatically, explicitly defined by the user.
@@ -73,14 +74,14 @@ function getActionNameFromElementProgrammatically(targetElement: Element, progra
7374type NameStrategy = (
7475 element : Element | HTMLElement | HTMLInputElement | HTMLSelectElement ,
7576 userProgrammaticAttribute : string | undefined ,
76- privacyEnabledActionName ? : boolean
77+ privacyEnabledActionName : boolean
7778) => ActionName | undefined | null
7879
7980const priorityStrategies : NameStrategy [ ] = [
8081 // associated LABEL text
81- ( element , userProgrammaticAttribute ) => {
82+ ( element , userProgrammaticAttribute , privacyEnabledActionName ) => {
8283 if ( 'labels' in element && element . labels && element . labels . length > 0 ) {
83- return getActionNameFromTextualContent ( element . labels [ 0 ] , userProgrammaticAttribute )
84+ return getActionNameFromTextualContent ( element . labels [ 0 ] , userProgrammaticAttribute , privacyEnabledActionName )
8485 }
8586 } ,
8687 // INPUT button (and associated) value
@@ -120,9 +121,9 @@ const priorityStrategies: NameStrategy[] = [
120121 ( element ) => getActionNameFromStandardAttribute ( element , 'title' ) ,
121122 ( element ) => getActionNameFromStandardAttribute ( element , 'placeholder' ) ,
122123 // SELECT first OPTION text
123- ( element , userProgrammaticAttribute ) => {
124+ ( element , userProgrammaticAttribute , privacyEnabledActionName ) => {
124125 if ( 'options' in element && element . options . length > 0 ) {
125- return getActionNameFromTextualContent ( element . options [ 0 ] , userProgrammaticAttribute )
126+ return getActionNameFromTextualContent ( element . options [ 0 ] , userProgrammaticAttribute , privacyEnabledActionName )
126127 }
127128 } ,
128129]
@@ -141,7 +142,7 @@ function getActionNameFromElementForStrategies(
141142 targetElement : Element ,
142143 userProgrammaticAttribute : string | undefined ,
143144 strategies : NameStrategy [ ] ,
144- privacyEnabledActionName ? : boolean
145+ privacyEnabledActionName : boolean
145146) {
146147 let element : Element | null = targetElement
147148 let recursionCounter = 0
@@ -196,7 +197,7 @@ function getActionNameFromStandardAttribute(element: Element | HTMLElement, attr
196197function getActionNameFromTextualContent (
197198 element : Element | HTMLElement ,
198199 userProgrammaticAttribute : string | undefined ,
199- privacyEnabledActionName ? : boolean
200+ privacyEnabledActionName : boolean
200201) : ActionName {
201202 return {
202203 name : getTextualContent ( element , userProgrammaticAttribute , privacyEnabledActionName ) || '' ,
@@ -205,16 +206,20 @@ function getActionNameFromTextualContent(
205206}
206207
207208function getTextualContent (
208- element : Element | HTMLElement ,
209+ element : Element ,
209210 userProgrammaticAttribute : string | undefined ,
210- privacyEnabledActionName ? : boolean
211+ privacyEnabledActionName : boolean
211212) {
212213 if ( ( element as HTMLElement ) . isContentEditable ) {
213214 return
214215 }
215216
217+ if ( isExperimentalFeatureEnabled ( ExperimentalFeature . USE_TREE_WALKER_FOR_ACTION_NAME ) ) {
218+ return getTextualContentWithTreeWalker ( element , userProgrammaticAttribute , privacyEnabledActionName )
219+ }
220+
216221 if ( 'innerText' in element ) {
217- let text = element . innerText
222+ let text = ( element as HTMLElement ) . innerText
218223
219224 const removeTextFromElements = ( query : string ) => {
220225 const list = element . querySelectorAll < Element | HTMLElement > ( query )
@@ -248,3 +253,60 @@ function getTextualContent(
248253
249254 return element . textContent
250255}
256+
257+ function getTextualContentWithTreeWalker (
258+ element : Element ,
259+ userProgrammaticAttribute : string | undefined ,
260+ privacyEnabledActionName : boolean
261+ ) {
262+ const walker = document . createTreeWalker (
263+ element ,
264+ // eslint-disable-next-line no-bitwise
265+ NodeFilter . SHOW_ELEMENT | NodeFilter . SHOW_TEXT ,
266+ rejectInvisibleOrMaskedElementsFilter
267+ )
268+
269+ let text = ''
270+
271+ while ( walker . nextNode ( ) ) {
272+ const node = walker . currentNode
273+ if ( isElementNode ( node ) ) {
274+ if (
275+ // Following InnerText rendering spec https://html.spec.whatwg.org/multipage/dom.html#rendered-text-collection-steps
276+ node . nodeName === 'BR' ||
277+ node . nodeName === 'P' ||
278+ [ 'block' , 'flex' , 'grid' , 'list-item' , 'table' , 'table-caption' ] . includes ( getComputedStyle ( node ) . display )
279+ ) {
280+ text += ' '
281+ }
282+ continue // skip element nodes
283+ }
284+
285+ text += node . textContent || ''
286+ }
287+
288+ return text . replace ( / \s + / g, ' ' ) . trim ( )
289+
290+ function rejectInvisibleOrMaskedElementsFilter ( node : Node ) {
291+ if ( isElementNode ( node ) ) {
292+ const nodeSelfPrivacyLevel = getNodeSelfPrivacyLevel ( node )
293+ if (
294+ node . hasAttribute ( DEFAULT_PROGRAMMATIC_ACTION_NAME_ATTRIBUTE ) ||
295+ ( userProgrammaticAttribute && node . hasAttribute ( userProgrammaticAttribute ) ) ||
296+ ( privacyEnabledActionName && nodeSelfPrivacyLevel && shouldMaskNode ( node , nodeSelfPrivacyLevel ) )
297+ ) {
298+ return NodeFilter . FILTER_REJECT
299+ }
300+ const style = getComputedStyle ( node )
301+ if (
302+ style . visibility !== 'visible' ||
303+ style . display === 'none' ||
304+ ( style . contentVisibility && style . contentVisibility !== 'visible' )
305+ // contentVisibility is not supported in all browsers, so we need to check it
306+ ) {
307+ return NodeFilter . FILTER_REJECT
308+ }
309+ }
310+ return NodeFilter . FILTER_ACCEPT
311+ }
312+ }
0 commit comments