GoogleChrome / web-vitals

Essential metrics for a healthy site.
https://web.dev/vitals
Apache License 2.0
7.56k stars 414 forks source link

Proposal: add inline assertions that run only in a new debug build #539

Open brendankenny opened 2 weeks ago

brendankenny commented 2 weeks ago

The problem

There are a few tricky areas within web-vitals where it's difficult to be certain that they're always correct because they're written to:

Testing is often difficult because failures tend to only occur in very unusual cases, but we have to know about the unusual case failures to test them, and web-vitals is written to not fall over in those unusual cases. Failures that do get isolated may still be flaky and difficult to reproduce, depending on races between browser scheduling and PerformanceEntry emitting (two recent examples: #536, #496).

Asserting the problem

Adding a debug build with assertions inline in the code could be a powerful tool to help with this. A quick example branch of what this could look like: https://github.com/GoogleChrome/web-vitals/compare/f6290e9...debug-assertions

The assertions would look like some permutation of

// pendingLoAFs must be in order of startTime.
if (DEBUG) {
  for (let i = 1; i < pendingLoAFs.length; i++) {
    if (pendingLoAFs[i].startTime < pendingLoAFs[i - 1].startTime) {
      throw new Error('pendingLoAFs not sorted by timestamps');
    }
  }
}

or pull the assertion out somewhere else

import {assertArrayInOrder} from './assertions.js';
// ...
if (DEBUG) {
  assertArrayInOrder(pendingLoAFs.map(l => l.startTime));
}

Rollup/terser can replace DEBUG with a constant false in the normal build and the entire code block will be dropped, resulting in no extra bytes shipped and no new possible errors for users (try out the branch to see it in action).

I believe the DEBUG check will also always work inside the assertion function as long as the if statement is the only thing at the top level, but I don't want to promise that yet if I'm wrong and terser sometimes misses that the resulting function is a no-op.

Meanwhile, debug builds run the assertions so they complain loudly when run in tests and we can periodically deploy to some relatively popular site, with errors reported to analytics. Users reporting issues can also be asked to deploy a debug build (in the style of https://github.com/GoogleChrome/web-vitals/issues/496#issuecomment-2166593956) to help gather data on how the error is occurring. Bonus: all the infrastructure is there if you want to add a bunch more assertions for your pet bug that day so you can make your own debug build and try it out on some test page.

I think we'd have to be disciplined to not have these everywhere—we don't want to be double checking even a small number of property accesses—but as a first pass at rules of thumb:

To fight against entropy, we could also require a plan when adding assertions, e.g. this one will live forever to help prevent future bugs (e.g. events stay sorted), this one can eventually be retired now that we've determined the case where it errored and we now handle it.

Possible problems with asserting the problem

Thoughts?

tunetheweb commented 2 weeks ago

anyone importing individual web-vitals scripts will get a ReferenceError thrown on the DEBUG check

That's gonna be a big problem IMHO as we have lots of people doing this internally at Google at least.

brendankenny commented 2 weeks ago

anyone importing individual web-vitals scripts will get a ReferenceError thrown on the DEBUG check

That's gonna be a big problem IMHO as we have lots of people doing this internally at Google at least.

As discussed out of band, the Google case is the easy one due to orderly build rule interfaces between libraries.

I think you're right that leaving DEBUG isn't a great solution for library users though, and we should eat the cost.

New proposal: The current individual imports get created by tsc and put in dist/modules/. It would be pretty easy to transform those files in place, swapping the debug variable for the expected boolean constant. Users would still need their own minifier to eliminate the dead code blocks in the false case, but that's not any different than what they already have to do to minify comments, whitespace, variable names, etc.

Terser would then run on those files to make the bundles, no terser global_defs required.