Allow users to opt-in to "compatibility mode" if uncaught JS errors occur #193
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Fixes #154.
Rationale
Aaron Gustafson's Interaction is an Enhancement article notes that the vast majority of users who are running a site without JS don't actually have JS disabled, but rather have run into a variety of edge-cases where the browser's JS crashes or fails to load entirely. This is corroborated by GOV.UK's How many people are missing out on JavaScript enhancement? post.
We currently have one way to address such edge cases, which is the
<ProgressiveEnhancement>
React component introduced in #192. Among other things, this component uses a React error boundary to catch unexpected errors and gracefully fall back to the baseline experience.However, it's quite possible that there may be other browser compatibility issues or bugs in our JS that aren't gated by progressive enhancement.
In such cases, the tech-savvy user who is aware that our site works without JS might have the aptitude to try disabling JS on their browser, but it's very unlikely that most people would do that. Instead, we can try detecting when JS errors go uncaught and offer to opt the user into "compatibility mode"--essentially a flag in the request session that tells our server not to send JS bundles to the client.
User experience
When an uncaught JavaScript error occurs, we display the following at the bottom of the user's screen (using
position: fixed
so they are likely to see it):(Credit for the content of the notification goes to the 18F Writing Lab, as it was directly lifted from the 18F project I worked on to implement the same functionality.)
Once the user clicks the Activate compatibility mode button, the page is reloaded. All collapsible content, such as the hamburger menu and dropdowns, is automatically expanded, and the following opt-out appears at the bottom of their page (using standard static positioning, since it's not urgent that the user see it):
Clicking Deactivate compatibility mode does what one would expect, reloading the page and instructing the user's browser to load all JS.
UX for browsers with JS disabled
(The following UX wasn't present in my original implementation at 18F.)
In the rare case that the end-user's browser has JS fully disabled, we do something a bit special.
One of the unfortunate aspects of making a site work without JS is that collapsible content needs to be visible by default, which means that the overwhelming majority of users with JS will end up seeing an unsettling "flash of uncollapsed content" (FOUC) while the page is loading.
With the existence of compatibility mode, we can have a compromise: we'll always show collapsed content as collapsed during page load to prevent FOUC, but we'll show the compatibility mode opt-in UI to all users with JS disabled. Such users can then opt-in to compatibility mode if they want to see the uncollapsed content.
I think this is a good compromise, but we can always revert it if we want.
Implementation
The implementation is pretty similar to my original one at 18F: it attaches an
error
event listener to the page to notice uncaught errors, at which point it shows the opt-in UI. Whether the mode is enabled is set in a boolean flag in the request session, which does currently mean that logging in or out (or explicitly cancelling the onboarding process in step 1) will reset compatibility mode to being disabled.All the internal code also refers to compatibility mode as "safe mode" because it's less typing. 😛
Dealing with React error boundaries
However, there was a major wrinkle I ran into with the aforementioned React error boundary functionality that our
<ProgressiveEnhancement>
component uses: whenever React callscomponentDidCatch()
, it has already dispatched anerror
event to the window, which means that the compatibility mode code already knows about it!What this meant in practice was that if a progressively-enhanced component accidentally threw an error that was caught by the
<ProgressiveEnhancement>
wrapper, which gracefully switched the UX to the baseline experience, the compatibility mode opt-in UI would be shown, even though it didn't need to be.So instead of immediately showing the UI, compatibility mode actually waits a little while (250 ms at present) for other code on the page to tell it to ignore an error that it might have received. Once the time period has elapsed, it then checks to see if any of the errors it's caught weren't ignored, and if so, it shows the opt-in UI.
Risks
The main risk here is that it's not easy to detect whether an uncaught
error
event actually represents a real loss of functionality. It's also possible that browser extensions could cause such errors to occur, in which case the original error wasn't even our fault, and the user might be confused or annoyed.To help mitigate this, I've added a close button in the opt-in UI (which wasn't present in my original 18F implementation). Since the whole site is a single-page application, this means that the user can effectively just dismiss it once and never see it again. Er, unless uncaught errors happen repeatedly, which hopefully shouldn't happen. I guess we can monitor analytics and user feedback and add a "never show me this dialog again" sort of option if we really need to.