-
Notifications
You must be signed in to change notification settings - Fork 9.4k
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
refactor(StartUrl): switch from error to debugString object #2549
Conversation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
review!
lighthouse-core/gather/driver.js
Outdated
@@ -276,7 +276,7 @@ class Driver { | |||
} | |||
|
|||
// If both the data and the url are empty strings, the page had no manifest. | |||
return reject('No web app manifest found.'); | |||
return resolve(null); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🎉
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
do you want to flatten the promise setup here while you're in here? Getting rid of the surrounding promise and then the resolve and rejects should be able to just be return/throw
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
sure
}) | ||
.then(manifest => { | ||
if (!manifest.value.start_url || !manifest.value.start_url.raw) { | ||
return Promise.reject(new Error(`No web app manifest found on page ${options.url}`)); | ||
if (!manifest || !manifest.value.start_url || !manifest.value.start_url.raw) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I may have been the cause of this original conditional, but I don't think it's quite right.
!manifest
makes sense now (in casegetAppManifest
returnednull
, the real no-manifest case)- if there's a
manifest
, thenmanifest.value
can beundefined
if the manifest was malformed JSON - if there's a
manifest
and amanifest.value
there will always be amanifest.value.start_url
, as even if it's missing or the completely wrong type in the raw manifest,start_url
will then default to the document URL
(!manifest || !manifest.value)
should be sufficient, and up to you if you want to make the error strings different for the two cases
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
done
} | ||
|
||
if (manifest.value.start_url.debugString) { | ||
return Promise.reject(new Error(manifest.value.start_url.debugString)); | ||
throw new Error(manifest.value.start_url.debugString); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
do we want this to be an error? It'll be things like invalid start_url relative to ${manifestUrl}
or start_url must be same-origin as document
, which is nice to put in front of the developer, but the parsed manifest will still have a valid start_url
in those cases.
Not sure if we want to fall on the "this is broken" side or the side of how the browser actually treats it since we don't have a great logging spot for that type of message if no error here
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
it wasn't it was being caught below
}) | ||
.then(_ => this.executeFetchRequest(options.driver, this.startUrl)) | ||
.catch(err => { | ||
this.debugString = err.message; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
probably better to set this.debugString
above and return
rather than throwing errors and catching everything here. Doing that would also let errors from getAppManifest
, the manifest parser, and executeFetchRequest
get through without all being turned into debugStrings
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
that was kinda the point though since all of this could reasonably fail not because of a bug but just because of the page not having a service worker, fine with switching for now though and going back to this if it's too noisy
return Promise.reject(new Error('No start_url found inside the manifest')); | ||
if (this.debugString || !this.startUrl) { | ||
const debugString = this.debugString || 'No start_url found inside the manifest'; | ||
return Promise.resolve({debugString}); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
statusCode
of -1
in this case?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
done
@@ -76,7 +81,9 @@ class StartUrl extends Gatherer { | |||
}).pop(); // Take the last record that matches. | |||
|
|||
return options.driver.goOnline(options) | |||
.then(_ => navigationRecord ? navigationRecord.statusCode : -1); | |||
.then(_ => { | |||
return {statusCode: navigationRecord ? navigationRecord.statusCode : -1}; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
debugString
in the -1
case?
Codecov Report
@@ Coverage Diff @@
## master #2549 +/- ##
==========================================
+ Coverage 84.52% 84.54% +0.02%
==========================================
Files 169 169
Lines 5614 5615 +1
Branches 774 777 +3
==========================================
+ Hits 4745 4747 +2
+ Misses 855 854 -1
Partials 14 14
Continue to review full report at Codecov.
|
// We're not reading `response.errors` however it may contain critical and noncritical | ||
// errors from Blink's manifest parser: | ||
// https://chromedevtools.github.io/debugger-protocol-viewer/tot/Page/#type-AppManifestError | ||
if (!response.data) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
why drop the response.url
check? If the manifest 404s, for instance, response.data
will be undefined
but that should probably be considered an error state, not that there is no manifest
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
IMO, anything that's not a bug should not say 'GathererError: blah', if the manifest 404s that's not a bug so it shouldn't be an error, at best we can give them the slightly different debug strings, is that what you're pushing back against?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Saying "No web app manifest found on page" is definitely wrong in that case, so a better error string is needed. But I'm not sure I agree about Gatherer errors. I have no problem with a method fetching a manifest throwing on a manifest URL that doesn't exist.
But I assume the issue is us conflating Lighthouse bugs with exceptional states? We may need to get back into that conversation to get the functionality we need. We don't want to slide back into -1 land where every audit has to handle all possible gatherer error states, but agreed we probably need a better separation between the classes of errors.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I have no problem with a method fetching a manifest throwing on a manifest URL that doesn't exist.
I've got no problem with this throwing either just the overall pass throwing instead of returning debugString. There should be some distinguishing from a user's perspective between a bug they have no control over and something they need to take action on.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Also re:
Saying "No web app manifest found on page" is definitely wrong in that case, so a better error string is needed.
I'd don't actually agree that "Unable to retrieve manifest" is better than "No web app manifest found on page", as a user receiving the former I may believe that it's LH problem not being able to retrieve it and there's nothing for me to do whereas the latter gets me to check my manifest. There's probably room for improvement on both, but calling it "definitely wrong" seems overly critical. It's not wrong, there was no usable manifest found.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
maybe we need to start laying out the failure states for an audit ahead of time so we can decide on them once (in theory) and then it's obvious when they're implemented or not :)
If there's no <link rel="manifest">
in the page, it's obviously not an error, the audits simply just won't pass. However, to me, if there is a <link rel="manifest">
and the url 404s, that is an error worth reporting to the user, as much as a manifest made up of invalid JSON would be. They tried to include one and it's failing for some reason.
Agreed "Unable to retrieve manifest" is a bad error string for that and we should be as specific as possible to help with those situations. Also totally on board with exploring non-exception error returns, expected errors, etc after this so we can separate the two (or more?) clear classes of errors we're dealing with here.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
"Unable to retrieve manifest" is a bad error string for that and we should be as specific as possible to help with those situations
One downside of the current Page.getAppManifest
protocol call is that it gives no network status information (as far as I can tell). So e.g. if the server returns a 404 with a Cannot GET /manifest.json
response string, the protocol will just return a JSON.parse
error from trying to parse that string (with no other indication of what happened) and we just have to infer that there was a 404. Maybe we could see about getting more detail there.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
(the devtools Application panel suffers from the same problem, only showing "Errors and warnings: Line: 1, column: 1, Unexpected token.
" and you have to notice the 404 in the console/network panel to see what happened)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
One downside of the current Page.getAppManifest protocol call is that it gives no network status information (as far as I can tell).
Yeah feels like it should return something similar to an artifact {value: null, debugString: ''}
or whatever
} | ||
|
||
if (manifest.value.start_url.debugString) { | ||
return Promise.reject(new Error(manifest.value.start_url.debugString)); | ||
this.debugString = manifest.value.start_url.debugString; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
should this be a state with a debugString
and try to still fetch the start_url
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
hm, why? there's nothing to fetch if the start_url is bad
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
hm, why? there's nothing to fetch if the start_url is bad
if there's a parseable JSON file there's always a good start_url
, even if it has to fall back to just the page's URL.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
oh I see now, faulty start_urls are always ignored
debugString seems rather useless then there will always be a usable start_url...
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
debugString seems rather useless then there will always be a usable start_url
it is useful for debugging to see if your manifest isn't acting like you think it should...e.g. you have one start_url in mind and it's defaulting to another. That might be really hard to track down (or even notice that it's happening) otherwise.
I guess that makes me fall on the side of the current code and just show the error :) Lighthouse can be stricter than the spec in that case
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I guess this gets into the division of labor between DevTools and LH. LH shouldn't be your debugger IMO outside of general messages to get you on the right track/where to start looking. If everything is working from the PWA perspective (and document url was the right start url), do you really care about being flagged that you don't have a valid start url?
Either way, I added back the debug string to still do the check so if it passes it should pass with the debugString
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Either way, I added back the debug string to still do the check so if it passes it should pass with the debugString
I think that's a good solution for now and the devtools/LH discussion is a good one to revisit now that Audits 2.0 is a real thing. Pavel and Paul also love drawing diagrams about that sort of division so I'm sure they'd be happy to get in on it :)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pavel and Paul also love drawing diagrams about that sort of division so I'm sure they'd be happy to get in on it :)
haha :)
} | ||
|
||
afterPass(options, tracingData) { | ||
if (!this.startUrl) { | ||
return Promise.reject(new Error('No start_url found inside the manifest')); | ||
if (this.debugString || !this.startUrl) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
seems like either there's a debugString
or a startUrl
for all possible states?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
not if manifestParser/getAppManifest fails, but I guess that's a different can of worms altogether
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
not if manifestParser/getAppManifest fails, but I guess that's a different can of worms altogether
yeah, in that case (if pass
rejects) afterPass
should be ignored, but it will still run, which is weird. We should probably fix that...
@brendankenny will review and we'll get this sorted today! |
@@ -27,19 +27,16 @@ class Manifest extends Gatherer { | |||
afterPass(options) { | |||
return options.driver.getAppManifest() | |||
.then(response => { | |||
if (!response || !response.data) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
!response.data
not needed here?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
done
failures, | ||
manifestValues | ||
}; | ||
const result = {failures, manifestValues}; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
if result
already has failures
and manifestValues
on it but all three are being passed into these methods, should they instead just have a single result
parameter?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
done
const result = {failures, manifestValues}; | ||
WebappInstallBanner.assessManifest(manifestValues, failures, result); | ||
WebappInstallBanner.assessServiceWorker(artifacts, failures, result); | ||
WebappInstallBanner.assessOfflineStartUrl(artifacts, failures, result); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
MultiCheckAudit
is already somewhat confusing and this doesn't exactly simplify things :)
Short of a MultiCheckAudit
refactor, what about something like a warnings
array, which is used for debugString
, just like failures
but they won't fail the audit?
Just an idea, maybe we can come up with something simpler/more self-explanatory
@@ -103,7 +103,7 @@ URL.elideDataURI = function elideDataURI(url) { | |||
// Why? Special handling was added by Chrome team to allow a pushState transition between chrome:// pages. | |||
// As a result, the network URL (chrome://chrome/settings/) doesn't match the final document URL (chrome://settings/). | |||
function rewriteChromeInternalUrl(url) { | |||
if (!url.startsWith('chrome://')) return url; | |||
if (!url || !url.startsWith('chrome://')) return url; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
what new path needs this?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this was a drive-by fix of something I encountered on another branch, can't remember what site now though it was over a month ago :P
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
but to answer your question the equalWithExcludedFragments
can now be run on a non-existent startUrl which causes this to fail for every site that doesn't have the manifest
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
but to answer your question the
equalWithExcludedFragments
can now be run on a non-existent startUrl which causes this to fail for every site that doesn't have the manifest
ha, I had forgotten we had done that. I guess that's fine vs moving the .map(rewriteChromeInternalUrl);
into the try/catch
if (!manifest.value.start_url || !manifest.value.start_url.raw) { | ||
return Promise.reject(new Error(`No web app manifest found on page ${options.url}`)); | ||
if (!manifest || !manifest.value) { | ||
this.debugString = `No usable web app manifest found on page ${options.url}`; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
if there's a manifest
but no manifest.value
, there will be a manifest.debugString
probably worth keeping
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
done
} | ||
|
||
if (manifest.value.start_url.debugString) { | ||
return Promise.reject(new Error(manifest.value.start_url.debugString)); | ||
this.debugString = manifest.value.start_url.debugString; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
maybe a comment here as a reminder that even with a debugString
there will be a fallback start_url
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
done
const networkRecords = tracingData.networkRecords; | ||
const navigationRecord = networkRecords.filter(record => { | ||
return URL.equalWithExcludedFragments(record._url, this.startUrl) && | ||
record._fetchedViaServiceWorker; | ||
}).pop(); // Take the last record that matches. | ||
|
||
return options.driver.goOnline(options) | ||
.then(_ => navigationRecord ? navigationRecord.statusCode : -1); | ||
.then(_ => { | ||
let debugString = this.debugString; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
can we handle these cases separately for clarity? I think there's:
debugString
and nostartUrl
debugString
and no matchingnavigationRecord
- matching
navigationRecord
(and maybe adebugString
fromstart_url
parsing and other future messages)
we could probably give more information (e.g. start_url
was fetched but had some error, wasn't covered by the SW, etc), but that can wait for another day
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
done
PTAL |
result.failures.push(artifacts.StartUrl.debugString); | ||
} | ||
} else if (artifacts.StartUrl.debugString) { | ||
result.debugString = artifacts.StartUrl.debugString; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
weird, I left this comment before (and it was in the email), but it was eaten in the web interface:
MultiCheckAudit
is already somewhat confusing and this doesn't exactly simplify things :)
Short of a
MultiCheckAudit
refactor, what about something like awarnings
array, which is used fordebugString
, just like failures but they won't fail the audit?
Just an idea, maybe we can come up with something simpler/more self-explanatory
so here (for this idea) it would result.warnings.push(artifacts.StartUrl.debugString);
whether or not it hasOfflineStartUrl
, and then MultiCheckAudit
would handle appending result.warnings.join(', ')
to the debugString
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
done
@@ -157,7 +157,7 @@ describe('PWA: webapp install banner audit', () => { | |||
|
|||
return WebappInstallBannerAudit.audit(artifacts).then(result => { | |||
assert.strictEqual(result.rawValue, true); | |||
assert.equal(result.debugString, 'Warning!'); | |||
assert.equal(result.debugString, 'Warnings: Warning!'); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
:)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM
I'd like us to give more clues on how to fix things when we know (e.g. the manifest 404ed), but we can build that up in the future, and the real purpose of the PR (moving away from exceptions for non-errors) looks good.
split out from #2420 to reduce the noise there