Skip to content

fix(search-modal): count NoResults per settled query, not per keystroke - #13114

Merged
mekarpeles merged 1 commit into
internetarchive:masterfrom
lokesh:fix/search-modal-noresults-tracking
Jul 3, 2026
Merged

fix(search-modal): count NoResults per settled query, not per keystroke#13114
mekarpeles merged 1 commit into
internetarchive:masterfrom
lokesh:fix/search-modal-noresults-tracking

Conversation

@lokesh

@lokesh lokesh commented Jul 3, 2026

Copy link
Copy Markdown
Collaborator

Closes #

fix β€” Corrects the NoResults analytics event on the new search modal, which was massively over-counting.

What's the problem, in plain terms?

We recently added tracking to the search modal to learn how people use it. One of the events, NoResults, is meant to answer: "How often does someone search and find nothing?" Early numbers showed it firing far more than expected β€” roughly as often as the modal is even opened.

That number is misleading, because of when the event fired. The modal shows results as you type. The old code sent a NoResults event every single time the results area happened to be empty β€” and there are lots of harmless, temporary moments where that's true but the person is perfectly happy:

  • While you're still typing. If you're searching for chamber of secrets, you briefly pass through cham, chamber o, chamber of sec… and one of those in-between fragments might momentarily match nothing before your full query matches plenty. The old code counted each of those blips as a "no results" β€” even though your search ultimately succeeded.
  • When you adjust a filter. Toggling "Readable only" or picking a language re-runs the search. Someone fiddling with filters to find something could rack up several NoResults events in a single sitting.
  • No deduplication. The same empty search settling again (e.g. deleting a typo and retyping it) counted every time.

So NoResults wasn't measuring "searches that ended with nothing" β€” it was measuring "empty split-seconds," which is a much bigger, much noisier number. It can't be meaningfully compared against the other events (like Open, which fires once per session).

How is it fixed?

The event now only counts a search the person has actually settled on:

  1. Wait for them to stop typing. Instead of firing the instant the results are empty, we hold the event for a brief pause. If the next keystroke changes the search, the pending event is dropped β€” so all those "still typing" fragments no longer count. Only the query left standing when they stop is recorded.
  2. Count each dead-end once. Within a single time the modal is open, the same search (including its filters) is counted at most once, so re-toggling a filter or retyping the same thing doesn't pile up duplicates.

The result: NoResults now reflects genuine dead-ends β€” a person searched, stopped, and truly saw nothing β€” which is the question we wanted answered. The event's label (which filters were active) and all the other events are unchanged, so no dashboards break; the NoResults count simply becomes trustworthy (and will drop substantially).

Technical

Client-side only β€” openlibrary/plugins/openlibrary/js/search-modal/SearchModal.js. No server, template, or API changes.

The empty-results branch of _fetchResults() no longer calls _track('NoResults', …) synchronously. It now calls a new _scheduleNoResultsTrack(fetchKey), which:

  • Defers the event behind a setTimeout (~1.2s). Because every keystroke starts a new fetch and reassigns _activeFetchKey, the deferred callback re-checks this._activeFetchKey === fetchKey and bails if the query has moved on. Net effect: only the final settled query in a typing burst is counted, not each partial string.
  • Dedupes via a per-session Set (_noResultsTracked) keyed on fetchKey β€” which is the existing _buildSearchJsonUrl(trimmed), so it already encodes query + availability + language. Repeat settles of the same (query+filters) fire once. The Set is cleared in _openModal, so each fresh open re-counts.

The pending timer is cleared everywhere a search is abandoned so no phantom event fires: _resetResults() (covers backspacing below the min length, the clear button, and the fetch-error path), _onDialogClosed(), and disconnectedCallback().

No false positives were possible before (the _results.length === 0 check is honest), so this is purely a de-noising/de-duplication change to the event cadence β€” the label taxonomy (unfiltered / availability / language / availability+language) is unchanged.

Testing

The event is only observable via Matomo, so verify in the browser dev console / network tab (filter for the analytics trackEvent call, category SearchModal, action NoResults):

  1. Open the search modal and type a query that returns nothing (e.g. asdkjfhaskjdfh), typing steadily. Before: several NoResults events fire (one per pause). After: a single NoResults fires ~1.2s after you stop typing.
  2. Type a real query that passes through an empty state on the way to results (e.g. slowly type an unusual title). Before: the intermediate empty fragment fires NoResults even though the final query has hits. After: no NoResults fires β€” the successful query supersedes the pending event.
  3. On an empty-result query, toggle "Readable only" off and back on. Before: a NoResults fires each toggle. After: at most one per distinct (query+filters) combination.
  4. Type an empty-result query, then close the modal (Esc) before the ~1.2s window elapses. After: no NoResults fires for the abandoned search.
  5. Sanity-check the other events are unaffected: Open, ResultClick, and SeeAllResults still fire exactly as before.

Screenshot

N/A β€” no visible UI change; this only alters when an analytics event is sent.

Stakeholders

The NoResults analytics event fired on every empty autocomplete response,
including the transient empties a query passes through while being typed and
every filter re-toggle, with no per-session dedupe. That inflated the count
far above the number of searches that actually ended with nothing.

Defer the event behind a short idle window (superseded by the next keystroke's
fetch) so only a query the patron stops on is counted, and dedupe repeat
settles of the same (query+filters) key per modal open.
@mekarpeles mekarpeles self-assigned this Jul 3, 2026
@lokesh
lokesh marked this pull request as ready for review July 3, 2026 18:06
@mekarpeles
mekarpeles merged commit f0fc3a8 into internetarchive:master Jul 3, 2026
3 of 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.

2 participants