Skip to content

feat: web interaction coverage beyond click + fix fabricated XHR traceparent (v0.1.13) - #2

Merged
nitinstp23 merged 1 commit into
mainfrom
release/v0.1.13
Aug 5, 2026
Merged

feat: web interaction coverage beyond click + fix fabricated XHR traceparent (v0.1.13)#2
nitinstp23 merged 1 commit into
mainfrom
release/v0.1.13

Conversation

@nitinstp23

Copy link
Copy Markdown
Contributor

Prerequisite for wiring scout-react into the logX Grafana app plugin, where the two gaps below would have been immediately visible: logX is a search-and-filter UI, and its config bootstrap uses a synchronous XMLHttpRequest.

Interaction coverage beyond click

Auto-tap tracking listened to a single click event, so a keyboard-submitted search, a <select> change and a form submit all produced nothing.

Now also emitted, all as user_interaction spans distinguished by user_interaction.type:

Type Fires on
change <select>, checkbox, radio, file, date, time, range. Free-text inputs excluded — their change fires on blur, reporting an edit somewhere the user does not associate with it.
submit Form submission, and Enter in a text entry. The latter exists because React handlers routinely swallow the real submit event; user_interaction.trigger tells the two apart.
input Text entries, debounced 500 ms after typing stops. Moving to another field flushes the previous edit immediately, so spans arrive in edit order rather than timer order.

New interactionEvents option narrows the set. Its values are the user_interaction.type vocabulary, so a dashboard filter maps straight onto the config knob controlling it — no mapping table. [] disables interaction tracking without touching enableAutoTapTracking.

Free text never leaves the page

user_interaction.value is set only for controls with a closed value space (selected option label, checked/unchecked). password/email/tel/hidden fields emit no input span at all, and their description reports as redacted rather than falling through to neighbouring text content — the redaction lives inside describeElement's fallthrough chain, so every event type inherits it.

Fix: XMLHttpRequest sent a fabricated traceparent

The header was built from fresh random ids rather than the emitted http.request span's, so XHR calls correlated with neither their own span nor the backend, and the backend saw a parent span id it would never receive.

emitSpan creates and ends a span in one call, so it structurally cannot serve a caller that needs the ids before the work finishes. Added Scout.startTrackedSpan(), which applies the same beforeSend, sampling and view-counter bookkeeping but hands back a handle the caller ends later. It returns null when the span is sampled out or dropped — and callers must respect that, or they inject a traceparent naming a parent that will never be exported.

Moving fetch onto the same helper closed three further gaps:

  • fetch spans were bypassing beforeSend and were not counted in view.resource.count; only XHR was.
  • A failed XHR emitted two http.request spans — both the error and loadend listeners ran the finalizer.
  • XHR spans had a near-zero duration, since the span was created after the request completed. They now span the request.

XHR spans also gained the http.provider.* classification that only fetch had.

Mutable headers contract

Documented and test-locked that headers is read per export rather than snapshotted, so an expiring bearer token is rotated by mutating the object passed to initialize — no re-init, no dropped batches. Two tests guard the reference chain (config.test.ts for resolution, otlp-exporter.test.ts for export), because a defensive copy anywhere along it would strand every exporter on the token the page loaded with.

Verification

make ci green: fmt-check, lint, typecheck, 210 tests (24 files), build, publint + are-the-types-wrong across all four export subpaths.

New coverage: 5 debounce/ordering/redaction cases for text edits, 6 for change/submit/keyboard, 2 for interactionEvents narrowing, and a full XHR suite including the traceparent-matches-span assertion that reproduced the bug.

Note on tagging

v0.1.12 sits on the merge commit on main, so this needs merging before v0.1.13 is tagged — the tag is what triggers the npm publish.

…eparent (v0.1.13)

Auto-tap tracking listened only to `click`, which made search-and-filter UIs
largely invisible: a keyboard-submitted search, a <select> change and a form
submit all produced nothing. It now also emits `user_interaction` spans for
`change`, `submit` (including Enter in a text entry, which React handlers
routinely swallow) and `input` (debounced 500ms, one span per settled edit).
The new `interactionEvents` option narrows the set; its values are exactly the
`user_interaction.type` vocabulary, so a dashboard filter maps directly onto the
config knob that controls it.

Free text never leaves the page. `user_interaction.value` is set only for
controls with a closed value space, sensitive fields emit no `input` span at
all, and their description is reported as `redacted` rather than falling
through to neighbouring text content.

Separately, XMLHttpRequest was sending a `traceparent` built from fresh random
ids rather than the emitted http.request span's, so XHR calls correlated with
neither their own span nor the backend, and the backend saw a parent id it
would never receive. `emitSpan` creates and ends a span in one call and so
cannot serve a caller that needs ids before the work finishes; the new
`Scout.startTrackedSpan()` fills that gap, applying the same beforeSend,
sampling and view-counter bookkeeping. Moving fetch onto it too closed three
more gaps: fetch was bypassing beforeSend and view.resource.count, a failed XHR
emitted two spans because both `error` and `loadend` ran the finalizer, and XHR
spans had a near-zero duration.

Also documents that `headers` is read per export, so an expiring bearer token
can be rotated by mutating the object passed to initialize — locked by tests so
a future defensive copy cannot silently break it.
@nitinstp23 nitinstp23 self-assigned this Aug 5, 2026
@nitinstp23
nitinstp23 requested a review from nimishgj August 5, 2026 09:34
@nitinstp23
nitinstp23 merged commit d2f1403 into main Aug 5, 2026
4 checks passed
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