Skip to content

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

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

LFBarreto wants to merge 17 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

The field is public: action.target.attributes. This needs a companion schema change in rum-events-format: DataDog/rum-events-format#439 (draft).

Changes

  • Add action.target.attributes, a key-value map (href, aria-label, name, title, alt, id, role, data-*) collected target-first from a click's composedPath(), capped at 20 keys / 100 chars per value.
  • Reuse the action-name privacy pipeline (shouldMaskAttribute / maskAttributeIfNeeded) for every masked attribute — no attribute-specific sanitization is added.
  • Gate the map behind a new click_target_attributes_map experimental flag; href/aria-label are no longer duplicated in composed_path_selector.
  • Implementation: getClickTargetAttributes.ts.

Test instructions

  • Unit tests:
    yarn test:unit --spec packages/browser-rum-core/src/domain/getClickTargetAttributes.spec.ts --spec packages/browser-rum-core/src/domain/getComposedPathSelector.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, open the sandbox at http://localhost:8080 (the port increments if 8080 is busy — check the terminal output for the actual URL), and update the DD_RUM.init() call in sandbox/index.html to enable the flag:
    {
      "clientToken": "xxx",
      "applicationId": "xxx",
      "sessionReplaySampleRate": 100,
      "profilingSampleRate": 100,
      "trackResources": true,
      "trackLongTasks": true,
      "telemetrySampleRate": 100,
      "telemetryConfigurationSampleRate": 100,
      "telemetryUsageSampleRate": 100,
      "enableExperimentalFeatures": ["click_target_attributes_map"],
      "proxy": "/proxy"
    }
    Click a link with a query string and an icon button with an aria-label, then confirm action.target.attributes on the intake payload contains the raw href and aria-label values at the default privacy level.

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

🤖 Generated with Claude Code

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

❌ Errors

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

🚦 2 Pipeline jobs failed

DataDog/browser-sdk | e2e: [firefox-pinned] — 🔄 Retry may pass, looks flaky

View more details · View in GitLab

DataDog/browser-sdk | e2e: [firefox] — 🔄 Retry may pass, looks flaky

View more details · View in GitLab

⚠️ Warnings

❄️ 2 New flaky tests detected

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
getComposedPathAttributes when the experimental flag is enabled keeps the closest (target-first) value when the same key appears on target and ancestor from Chrome Headless 152.0.0.0 (Linux 0.0.0)
Test has failed

View in Flaky Test Management

ℹ️ Info

No other issues found (see more)

🧪 All tests passed

🔄 Datadog auto-retried 3 jobs - 0 passed on retry View in Datadog

🎯 Code Coverage (details)
Patch Coverage: 73.02%
Overall Coverage: 77.06% (-0.02%)

Useful? React with 👍 / 👎

This comment will be updated automatically if new data arrives.
🔗 Commit SHA: d80f6dd | 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 185.26 KiB 186.75 KiB +1.49 KiB +0.80%
Rum Profiler 8.38 KiB 8.38 KiB +1 B +0.01%
Rum Recorder 28.13 KiB 28.13 KiB +1 B +0.00%
Logs 58.81 KiB 58.87 KiB +60 B +0.10%
Rum Salesforce N/A 144.79 KiB N/A N/A N/A
Rum Slim 143.30 KiB 144.78 KiB +1.48 KiB +1.03%
Worker 22.96 KiB 22.96 KiB 0 B 0.00%
Rum Shopify N/A 214.42 KiB N/A N/A N/A
Rum-shopify Profiler N/A 8.38 KiB N/A N/A N/A
Rum-shopify Recorder N/A 3.69 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>
LFBarreto and others added 6 commits September 4, 2026 10:51
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.
…tization

Move the field to action.target.attributes (public schema) instead of
_dd.action.target.attributes, per RFC feedback. Remove the specialized
digit/email content filter and href structural sanitization: every
attribute (including href) now goes through the same masking pipeline
already used for action names (shouldMaskAttribute/maskAttributeIfNeeded).
composed_path_selector already excluded href/aria-label, so it needed no
change beyond dropping the now-unused urlSanitizer import.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Continuation of 2de9037: update rawRumEvent.types.ts and
actionCollection.ts to place attributes under the public action.target
instead of _dd.action.target, rewrite getComposedPathAttributes.ts to
drop custom href/digit sanitization in favor of the existing
maskAttributeIfNeeded pipeline, and update the affected unit/e2e tests.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
The mask-unless-allowlisted test cases set window.$DD_ALLOW per
iteration but never reset it, leaving the last case's value
(['foo']) leaked globally for the rest of the Karma run. This made
CI's randomized test order intermittently unmask attributes equal to
'foo' in unrelated specs (e.g. getComposedPathAttributes.spec.ts).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@LFBarreto
LFBarreto force-pushed the luiz.barreto/composed-path-selector-attributes branch 5 times, most recently from 57ec69a to ce8aac5 Compare September 16, 2026 13:26
@LFBarreto
LFBarreto force-pushed the luiz.barreto/composed-path-selector-attributes branch from 111d4e8 to ecc658d Compare September 16, 2026 13:47
@LFBarreto
LFBarreto marked this pull request as ready for review September 16, 2026 13:47
@LFBarreto
LFBarreto requested a review from a team as a code owner September 16, 2026 13:47
@LFBarreto LFBarreto changed the title ⚗️ Collect click-target attributes as a facetable key-value map Collect click-target attributes as a facetable key-value map Sep 16, 2026
@LFBarreto LFBarreto changed the title Collect click-target attributes as a facetable key-value map ⚗️ Collect click-target attributes as a facetable key-value map Sep 16, 2026
@LFBarreto

Copy link
Copy Markdown
Collaborator Author

@codex review

@chatgpt-codex-connector

Copy link
Copy Markdown

To use Codex here, create a Codex account and connect to github.

@BeltranBulbarellaDD

Copy link
Copy Markdown
Contributor

@codex review

@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Sep 16, 2026

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review Completed 2026-09-16T14:11:05.025545Z ecc658d Manual request
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: ecc658d29f

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread packages/browser-rum-core/src/domain/privacy.ts Outdated
Comment thread packages/browser-rum-core/src/domain/getComposedPathSelector.ts Outdated
Comment thread packages/browser-rum-core/src/domain/action/actionCollection.ts
resource?: Count
target: {
name: string
attributes?: Record<string, string>

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Regenerate the public action type before exposing attributes

issue: Adding the field only to RawRumActionEvent leaves the generated public RumActionEvent.action.target type unchanged, where this property is inferred through the index signature as unknown. Consequently, TypeScript customers cannot type-safely inspect or redact action.target.attributes in beforeSend, despite this being introduced as a public field; merge the companion schema change, update the schema dependency, and regenerate rumEvent.types.ts.

AGENTS.md reference: AGENTS.md:L124-L128

Useful? React with 👍 / 👎.

Comment thread packages/browser-rum-core/src/domain/getClickTargetAttributes.ts Outdated
- Enforce MASK without consulting $DD_ALLOW: only MASK_UNLESS_ALLOWLISTED
  should honor the allowlist, so an explicitly masked value can no longer
  leak raw just because it's allowlisted.
- Only exclude href/aria-label from composed_path_selector once the
  click_target_attributes_map flag is enabled, so customers using
  actionNameAttribute without the flag keep seeing the value there.
- Add action.target.attributes to the modifiable field paths so beforeSend
  can redact it, matching the existing header-map pattern.
- Compare tagName case-insensitively when collecting/masking href, so SVG
  <a href> elements (lowercase tagName) are no longer skipped.

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

Copy link
Copy Markdown
Collaborator Author

Left the rawRumEvent.types.ts finding unresolved: RumActionEvent.action.target can only be regenerated from the rum-events-format schema once DataDog/rum-events-format#439 merges — hand-editing that generated file isn't an option (see AGENTS.md). Will regenerate once the schema PR lands.

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.

2 participants