Skip to content

⚗️ Collect click-target attributes as a facetable key-value map - #5002

Draft
LFBarreto wants to merge 11 commits into
mainfrom
luiz.barreto/composed-path-selector-attributes
Draft

⚗️ Collect click-target attributes as a facetable key-value map#5002
LFBarreto wants to merge 11 commits into
mainfrom
luiz.barreto/composed-path-selector-attributes

Conversation

@LFBarreto

@LFBarreto LFBarreto commented Aug 31, 2026

Copy link
Copy Markdown
Collaborator

Motivation

_dd.action.target.composed_path_selector identifies the clicked element for auto-collected click actions. It is a single CSS-like string. Customers cannot filter or group RUM click actions by one attribute value, for example href or aria-label.

This change adds a key-value map of click-target attributes. Customers can use it as a facet to filter and group click actions.

Design rationale, prior-art comparison (Heap, PostHog, FullStory, Amplitude, GA4), and customer motivation are written up in RFC: Collecting Click-Target Attributes as a Facetable Key-Value Map.

JIRA: PANA-8907

This field is kept under _dd.* for now. Promoting it to the public action.target schema needs a companion PR in rum-events-format: DataDog/rum-events-format#439 (draft).

Changes

  • Add _dd.action.target.attributes, a key-value map collected from a click's composedPath(). The map can contain href, aria-label, name, title, alt, id, role, and data-* attributes.
  • Elements are visited target-first. The closest value for each key wins. Farther ancestors are ignored for that key once it is set.
  • href is collected only from <a> and <area> elements, and only after the same structural sanitization already used in composed_path_selector (origin plus path, generated segments replaced with ?, query values dropped, query names kept, hash dropped). This sanitization now lives in a shared urlSanitizer.ts, with its own unit tests.
  • aria-label, name, title, and alt go through the existing privacy masking pipeline (defaultPrivacyLevel, data-dd-privacy, $DD_ALLOW), independently of the enablePrivacyForActionName option, which only controls masking of the action name.
  • data-* attributes go through the same masking pipeline, except for a small allowlist of test-id-style attributes (data-testid, data-cy, ...), which stay unmasked. The SDK's own data-dd-privacy attribute is excluded from this wildcard collection.
  • id and role are collected as-is. They are not part of the masked set.
  • Every collected value is dropped if it contains an email address, regardless of privacy level or attribute type. aria-label, name, title, and alt values are also dropped if they contain a digit, since that free-form text can carry a raw identifier (e.g. a phone number) when unmasked. href, id, role, and data-* are exempt from the digit check: digits are the common, wanted case there (product ids, SKUs, test ids).
  • The map is capped at 20 keys per click action, and each value is truncated to 100 characters.
  • The map is gated behind a new composed_path_selector_attributes_map experimental flag.
  • href and aria-label are removed from composed_path_selector. They stay in the new attributes map only, so the SDK does not collect the same PII-sensitive data in two fields.

See getComposedPathAttributes.ts for the implementation.

Test instructions

  • Unit tests:
    yarn test:unit --spec packages/browser-rum-core/src/domain/getComposedPathAttributes.spec.ts --spec packages/browser-rum-core/src/domain/getComposedPathSelector.spec.ts --spec packages/browser-rum-core/src/domain/urlSanitizer.spec.ts --spec packages/browser-rum-core/src/domain/action/trackClickActions.spec.ts --spec packages/browser-rum-core/src/domain/action/actionCollection.spec.ts
    
  • E2E test: yarn test:e2e -g "composed path" covers the attribute map and the selector string, with the correct experimental flag enabled.
  • Manual check: run yarn dev, add enableExperimentalFeatures: ['composed_path_selector_attributes_map'] to the sandbox DD_RUM.init() call in sandbox/index.html, click a link with a query string and an icon button with an aria-label, and confirm _dd.action.target.attributes on the intake payload contains the sanitized href and the masked aria-label, with no digits or email addresses in any value.

Checklist

  • Tested locally
  • Tested on staging
  • Added unit tests for this change.
  • Added e2e/integration tests for this change.
  • Updated documentation and/or relevant AGENTS.md file

Add sanitized href and aria-label to the click action composed_path_selector.
This helps identify the clicked element without exposing PII.

The href value keeps only the origin and the path. The path removes
segments that look like generated identifiers. The href value drops
the query string and the hash. A non-http(s) href keeps only the scheme.

The aria-label value follows the same privacy rules as action names.

This feature is behind the composed_path_selector_attributes
experimental flag.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@datadog-official

datadog-official Bot commented Aug 31, 2026

Copy link
Copy Markdown

Pipelines  Tests

Unblock PR with BitsAI

⚠️ Warnings

Your PR has failed checks. Please review the issues below and take necessary action before merging.

🚦 1 Pipeline job failed

DataDog/browser-sdk | unit — ❌ 1 test failed · 🔧 Needs a code fix, caused by this PR

View more details · View in GitLab

❌ getComposedPathAttributes when the experimental flag is enabled caps the number of collected keys and keeps collecting nothing further past the cap from Chrome Headless 152.0.0.0 (Linux 0.0.0)
Test has failed

ℹ️ Info

No other issues found (see more)

❄️ No new flaky tests detected

🎯 Code Coverage (details)
Patch Coverage: 70.93%
Overall Coverage: 77.00% (-0.03%)

Useful? React with 👍 / 👎

This comment will be updated automatically if new data arrives.
🔗 Commit SHA: 2ebdcb0 | Docs | View more details | Give us feedback!

@cit-pr-commenter-54b7da

cit-pr-commenter-54b7da Bot commented Aug 31, 2026

Copy link
Copy Markdown

Bundles Sizes Evolution

📦 Bundle Name Base Size Local Size 𝚫 𝚫% Status
Rum 181.63 KiB 183.54 KiB +1.92 KiB +1.05%
Rum Profiler 8.43 KiB 8.43 KiB 0 B 0.00%
Rum Recorder 25.32 KiB 25.32 KiB 0 B 0.00%
Logs 57.93 KiB 58.01 KiB +80 B +0.13%
Rum Salesforce N/A 141.57 KiB N/A N/A N/A
Rum Slim 139.68 KiB 141.56 KiB +1.88 KiB +1.34%
Worker 22.96 KiB 22.96 KiB 0 B 0.00%
Rum Shopify N/A 207.90 KiB N/A N/A N/A
Rum-shopify Profiler N/A 8.43 KiB N/A N/A N/A
Rum-shopify Recorder N/A 3.74 KiB N/A N/A N/A

LFBarreto and others added 7 commits August 31, 2026 14:35
Run Prettier to fix a formatting issue.
Fix the e2e assertion to match CSS.escape output for spaces and slashes.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Add a preceding sibling in the test body so the link's nth-child
position stays fixed. Without it, the async and npm CI test setups
append an init script after </body>. Browsers reparent that script
into <body> as a sibling, which changed the number of children and
added an unexpected nth-child to the selector.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Change the href sanitization to keep the (deduplicated) query
parameter names while still dropping their values. Parameter names
are typically static field names, not user data, so keeping them
makes the selector more useful without exposing what was in the
query string.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Do not apply the privacy-level masking to href. Its own sanitization
(dropping query values, the hash, and any non-http(s) payload) is
already the intended protection for this attribute, regardless of
the privacy level in effect. aria-label keeps the existing masking
behavior.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Fix bugs found by code review:
- Exclude href and aria-label from the safe-attributes allowlist,
  so setting actionNameAttribute to either no longer leaks the raw,
  unsanitized value alongside the sanitized one.
- Detect the href's non-http(s) scheme from the parsed URL instead
  of a regex on the raw string, so a leading whitespace or control
  character can no longer bypass the scheme-only shortcut and leak
  the full payload (an email address, a script...).
- Mask aria-label at the hidden and ignore privacy levels too, not
  only mask and mask-unless-allowlisted, matching how session replay
  already handles them.
- Compare the resolved origin against the current page's origin to
  decide whether to show it, instead of pattern-matching the raw
  href, so a backslash-led href cannot hide a real cross-origin
  navigation.
- Decode a path segment before checking if it looks generated, so a
  percent-encoded non-ASCII segment (ex: an accented word) is not
  redacted by mistake.
- Resolve an empty href to the current document instead of treating
  it as absent.

Also extract the aria-label masking logic into a shared, exported
maskAttributeIfNeeded in privacy.ts, and fix a test cleanup that used
an inline delete instead of registerCleanupTask.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
The backslash-led href test relied on old Chrome/Edge (BrowserStack
legacy devices) resolving "\\evil.example/..." as a file: URL, unlike
current browsers, which resolve it as http:. Replace it with a
<base> element pointing to a different origin, which resolves
consistently across browsers and still exercises the same
origin-comparison code path.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Add `_dd.action.target.attributes`, a key-value map of href, aria-label,
name, title, alt, id, role, and data-* attributes collected from a click's
composedPath, behind a new `composed_path_selector_attributes_map`
experimental flag. This lets customers filter and group RUM click actions
by these values, which the existing `composed_path_selector` CSS-like
string does not support.

href and aria-label are no longer collected in `composed_path_selector`,
to avoid collecting the same PII-sensitive data in two fields. Every
collected value is dropped if it contains an email address or a digit,
regardless of privacy level, so the map never carries a numeric
identifier or an email address.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@LFBarreto LFBarreto changed the title ✨ Add href and aria-label to composed_path_selector ⚗️ Collect click-target attributes as a facetable key-value map Sep 2, 2026
`maskAttributeIfNeeded` was gated behind `enablePrivacyForActionName`, so
disabling that action-name-only option would also disable masking for the
new attributes facet map. `data-dd-privacy` was leaking into the map as a
regular data-* attribute, and `placeholder` was missing from the masked-text
set despite the code claiming parity with `shouldMaskAttribute`. The new e2e
test also enabled the wrong experimental-flag string, so it silently ran
against unflagged behavior.

Also dedupes `FILTERED_TAGNAMES` into a shared export, fixes a stale
comment, and adds dedicated `urlSanitizer.spec.ts` coverage.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Not asked for in the original request; removing to keep the collected
attribute set limited to what customers actually need.
id, role, and data-* commonly carry digits customers want (product ids,
SKUs, test ids). The blanket digit-drop now applies only to
aria-label/name/title/alt, which are the free-form text attributes most
likely to leak a raw identifier when unmasked. The email check still
applies to every attribute.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant