Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow users to opt-in to "compatibility mode" if uncaught JS errors occur #193

Merged
merged 14 commits into from
Sep 24, 2018

Conversation

toolness
Copy link
Collaborator

@toolness toolness commented Sep 24, 2018

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):

opt_in

(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):

opt_out

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 calls componentDidCatch(), it has already dispatched an error 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.

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.

Revert to safe mode if the app explodes
1 participant