Skip to content

feat: add opt-in interaction instrumentation with click-to-request correlation - #98

Draft
parker-edwards wants to merge 25 commits into
dash0hq:mainfrom
parker-edwards:feat/interaction-instrumentation
Draft

feat: add opt-in interaction instrumentation with click-to-request correlation#98
parker-edwards wants to merge 25 commits into
dash0hq:mainfrom
parker-edwards:feat/interaction-instrumentation

Conversation

@parker-edwards

@parker-edwards parker-edwards commented Jul 22, 2026

Copy link
Copy Markdown
Contributor

What

Adds opt-in interaction instrumentation (@dash0/interactions): automatic browser.interaction events for clicks (plus individually opt-in scroll / key-press / change capture), with click-to-request correlation onto HTTP spans. This replaces #93 (closed), split per feature at the reviewing team's request — this is part 3 of 3.

Stacked on the XHR PR (#97): this branch is based on the @dash0/xhr branch, because interaction attribution stamps user_interaction.* onto both fetch and XHR spans. Until that PR merges, the diff here includes its commits — the interaction-specific changes start at "feat: add browser.interaction event name and interaction attribute keys". I'll rebase onto main once the XHR PR lands.

Why

Teams migrating from other RUM products (Datadog RUM's trackUserInteractions in particular) expect click analytics with human-readable action names, and the ability to tie a user action to the HTTP requests it triggered.

How

  • Opt-in by default: interactionInstrumentation.enabled defaults to false. Scroll (captureScrolls), key-press (captureKeyPresses), and change (captureChanges) capture are each additionally opt-in.
  • A single capture-phase window listener (no per-element wiring, no listener leakage) emits one browser.interaction log per interaction: a plain-string human-readable body (e.g. Click "Save Settings" on /settings) plus namespaced interaction.* attributes (type, name, name_source, target.tag, target.id, target.selector, id).
  • Action-name derivation walks the ancestor chain with a strict priority — configured attribute (data-dash0-action-name by default, configurable via actionNameAttribute) → standard attributes (aria-label, aria-labelledby, alt, title, placeholder, button-ish value) → visible text → blank. Each event's name_source reflects which phase produced the name.
  • Privacy rules (not configurable): values of text/password/select fields are never read; the text-content fallback never applies to form controls; change capture reports only value length for text fields and selected count for selects, and nothing for password fields; key-press capture records only navigation/activation keys, never printable characters. Names are whitespace-normalized and truncated to 100 characters; selectors capped at 128.
  • Click-to-request correlation: the click registers an active interaction before application handlers run (capture phase), so HTTP spans triggered by the click are stamped with user_interaction.id/user_interaction.name. Key presses attribute only for activation keys (Enter/Space).
  • Documented in INSTALL.md.

Testing

  • pnpm run ci (lint, prettier, unit tests, build): green — 365 unit tests across 26 files (~90 new across action-name derivation, click/scroll/keypress/change capture, emit, attribution, and config plumbing).
  • New e2e spec 10-interactions (custom attribute naming, text/aria-label derivation, input-value privacy, default-disabled): green locally, as is the full suite (10/10 spec files) via pnpm run test:e2e:local.

Notes for reviewers

This feature was built together with @seanpeterkin (the scroll/key-press/change capture and click-to-request correlation are his work; commit authorship is preserved). It's our own initiative rather than a scheduled roadmap item — happy to adjust naming, semantics, or scope to fit your conventions.

🤖 Generated with Claude Code

parker-edwards and others added 25 commits July 3, 2026 17:05
…hared utils

Pulls determinePropagatorTypes, addTraceContextHttpHeaders, endSpanOnError,
and endSpanOnAbort out of fetch.ts into the transport-agnostic
instrumentations/http/utils.ts module so xhr.ts (ENG-4822) can reuse them
without duplicating logic. Pure refactor, no behavior change.

Ref: ENG-4822
Registers the new instrumentation name and enablement gate ahead of the
full XMLHttpRequest instrumentation implementation (ENG-4822). Defaults
to enabled, matching @dash0/fetch, since axios/XHR-based consumers need
this on by default to get traceparent propagation.

Ref: ENG-4822
Adds instrumentXhr() covering open()/setRequestHeader()/send() up through
span creation, common/url/method attributes, propagator header injection
(mirrors fetch.ts's determinePropagatorTypes/addTraceContextHttpHeaders),
request header capture, and starting resource-timing correlation. Request
completion (success/error/timeout/abort) lands in the next commit.

Ref: ENG-4822
…ort)

Wires loadend/error/timeout/abort listeners to classify failures, mirrors
fetch.ts's endSpanOnAbort/endSpanOnError semantics, captures response
headers via getAllResponseHeaders(), and routes span completion through
the shared resource-timing correlation (observeResourcePerformance) so
XHR spans get the same network timing events as fetch spans.

Unit tests await the async resource-timing completion path (maxWait=0 in
tests) rather than assuming synchronous span delivery.

Ref: ENG-4822
Prevents unbounded listener and closure accumulation on reused
XMLHttpRequest instances; cleanup happens on loadend, which fires for
every request outcome.

Ref: ENG-4822
…d axios

Adds test/e2e/spec/09-xhr-instrumentation covering same-origin XHR, cross-
origin traceparent injection, X-Ray on a same-origin XHR request, ignored
URLs, synchronous XHR, and an axios GET request (axios defaults to its XHR
adapter in browsers, the setup that motivated ENG-4822). Serves
axios's UMD bundle from the local e2e test server.

Ref: ENG-4822
README and INSTALL previously only mentioned fetch for HTTP instrumentation
and trace propagation; several INSTALL.md option descriptions already
referenced XMLHttpRequest in anticipation of this feature (ENG-4822) and
are now accurate.

Ref: ENG-4822
Add InteractionInstrumentationSettings type with enabled/actionNameAttribute
fields, wire it through InitOptions and the merge-based init() pipeline with
a double gate (enabledInstrumentations name check AND
interactionInstrumentation.enabled), and add a stub
startInteractionInstrumentation entry point fleshed out in a later task.
…or boundaries

The id fast path in buildSelector skipped MAX_SELECTOR_LENGTH; both
branches now share one truncation.
…ents

Four changes shaped by validating the preview against an Angular 20 +
ASP.NET Core 9 stack:

1. Action names: remove the target.textContent catch-all. Text is only
   read from clickable elements (button/link/label/[role=button]), so a
   click on an unnamed container yields a blank name with target
   metadata instead of the container's entire visible text.

2. Readable event bodies: browser.interaction bodies are now plain
   human-readable strings ('Click "Save Part" on /inventory/parts');
   structured fields moved to namespaced interaction.* log attributes.
   UIs without a dedicated renderer for this event type display a
   sentence instead of stringified JSON.

3. Click-to-request correlation: interactions register a short-lived
   (2s) active interaction; XHR/fetch spans started in that window are
   stamped with user_interaction.id/name and the event carries the same
   interaction.id, joining a click to the requests it triggered.

4. New interaction types behind the existing opt-in: scroll (one event
   per burst with net direction), key_press (allow-listed control keys
   only, printable characters never), change (value length / selected
   count only, never the value; password fields report neither). Each
   type has its own opt-out flag (captureScrolls, captureKeyPresses,
   captureChanges).

375 unit tests passing (about 50 new), lint/prettier/build clean.
Bundle cost of all four changes: 22.5 -> 23.7 kB gzip (ESM).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Enabling interaction instrumentation now captures only clicks, as before
this PR. The new capture types each require an explicit opt-in
(captureScrolls / captureKeyPresses / captureChanges) because they add
event volume and capture additional -- if carefully minimized -- data.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Navigation keys (arrows, Tab, Escape, paging keys) still emit key_press
events but no longer register as the active interaction: they don't
cause requests, so letting them claim the attribution slot could stamp
unrelated background requests or steal attribution from a real click
within the 2s window.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… interaction.* attributes

The interactions e2e spec still asserted the pre-#4 kvlist body shape.
PR #4 moved the structured fields to namespaced interaction.* log
attributes and made the body a plain-string human-readable title, but
only updated the unit tests. Align the e2e expectations with the
ratified shape.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
thiemok added a commit that referenced this pull request Jul 24, 2026
…on (#99)

What

Adds XMLHttpRequest instrumentation (@dash0/xhr) — HTTP client spans and trace-context propagation for XHR, matching the existing fetch instrumentation. This replaces #93 (closed), split per feature at the reviewing team's request — this is part 2 of 3 and independent of the startView PR (#96); the interaction-instrumentation PR (#98) is stacked on top of this one.
Why

axios's default browser adapter uses XMLHttpRequest, so today axios/XHR-based applications get no HTTP spans and no traceparent header — their frontend telemetry never joins backend traces.
How

    New @dash0/xhr instrumentation name, enabled by default alongside @dash0/fetch.
    First commit extracts the propagator/error helpers from fetch.ts into http/utils.ts, so fetch and XHR share one propagation code path (traceparent, X-Ray, propagators config, CORS URL allow-lists) — no behavior change for fetch.
    XHR spans cover the full lifecycle: success, error, timeout, and abort all end the span, and per-request event listeners are removed on completion to avoid leaks.
    Documented in INSTALL.md and the README feature list.
    axios is added as a devDependency only, for the e2e spec.

---------

Co-authored-by: Parker Hyland <parker.hyland@dash0.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
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