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

core: add devtools path to DOMStats #11578

Merged
merged 12 commits into from
Nov 9, 2020
Merged

core: add devtools path to DOMStats #11578

merged 12 commits into from
Nov 9, 2020

Conversation

adrianaixba
Copy link
Collaborator

Adds devtools node path to DOMStats gatherer. Allows for devtools linkification on report.

fixes #11558

@adrianaixba adrianaixba requested a review from a team as a code owner October 19, 2020 17:25
@adrianaixba adrianaixba requested review from adamraine and removed request for a team October 19, 2020 17:25
@google-cla google-cla bot added the cla: yes label Oct 19, 2020
pathToElement: elementPathInDOM(deepestElement),
// ignore style since it will provide no additional context, and is often long
snippet: getOuterHTMLSnippet(deepestElement, ['style']),
devtoolsNodePath: getNodePath(deepestElement),
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was hoping to use getNodeDetails to get all the info. But, interestingly while running a smoke test (dbw), there was a case when the nodeSelector function failed because the node/element had an undefined tagName? So, I decided to fall back to just gathering the devtoolsNodePath & path.

Copy link
Member

@adamraine adamraine Oct 19, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It looks like that is because the element being resolved is a ShadowRoot not an Element. ShadowRoot does not have a tagName field so getNodeDetails runs into an error in the functions that use tagName. In the smoke test, the element with most children happens to be the host of a shadow dom.

I think getNodeDetails can work if you pass in the shadow root host when parentWithMostChildren is a ShadowRoot not an Element. You could also change getNodeSelector and getNodeLabel to handle a ShadowRoot being passed in. getOuterHTMLSnippet and getNodePath already do this which explains why those functions were working on their own:

function getOuterHTMLSnippet(element, ignoreAttrs = [], snippetCharacterLimit = 500) {
const ATTRIBUTE_CHAR_LIMIT = 75;
// Autofill information that is injected into the snippet via AutofillShowTypePredictions
// TODO(paulirish): Don't clean title attribute from all elements if it's unnecessary
const autoFillIgnoreAttrs = ['autofill-information', 'autofill-prediction', 'title'];
try {
// ShadowRoots are sometimes passed in; use their hosts' outerHTML.
if (element instanceof ShadowRoot) {
element = element.host;
}

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the explanation this worked! I'm wondering, though, why the shadowRoot case might have not been considered for the other functions (including getBoundingClientRect(element)), if it were on purpose or just a case that hadn't been encountered before - which I'm thinking might be the case.

@adrianaixba adrianaixba changed the title Domstats devtools core: add devtools path to DOMStats Oct 19, 2020
type: 'node',
path: stats.depth.devtoolsNodePath,
snippet: stats.depth.snippet,
}),
statistic: str_(UIStrings.statisticDOMDepth),
element: {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You are replacing element with node correct? You shouldn't need element in the items list anymore.

pathToElement: elementPathInDOM(deepestElement),
// ignore style since it will provide no additional context, and is often long
snippet: getOuterHTMLSnippet(deepestElement, ['style']),
devtoolsNodePath: getNodePath(deepestElement),
Copy link
Member

@adamraine adamraine Oct 19, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It looks like that is because the element being resolved is a ShadowRoot not an Element. ShadowRoot does not have a tagName field so getNodeDetails runs into an error in the functions that use tagName. In the smoke test, the element with most children happens to be the host of a shadow dom.

I think getNodeDetails can work if you pass in the shadow root host when parentWithMostChildren is a ShadowRoot not an Element. You could also change getNodeSelector and getNodeLabel to handle a ShadowRoot being passed in. getOuterHTMLSnippet and getNodePath already do this which explains why those functions were working on their own:

function getOuterHTMLSnippet(element, ignoreAttrs = [], snippetCharacterLimit = 500) {
const ATTRIBUTE_CHAR_LIMIT = 75;
// Autofill information that is injected into the snippet via AutofillShowTypePredictions
// TODO(paulirish): Don't clean title attribute from all elements if it's unnecessary
const autoFillIgnoreAttrs = ['autofill-information', 'autofill-prediction', 'title'];
try {
// ShadowRoots are sometimes passed in; use their hosts' outerHTML.
if (element instanceof ShadowRoot) {
element = element.host;
}

@@ -295,6 +295,7 @@ function getNodeSelector(node) {
* @param {Element} node
*/
function getSelectorPart(node) {
node = node instanceof ShadowRoot ? node.host : node;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If a ShadowRoot instance is passed in to getNodeSelector, it will output the selector part of the shadow host and then exit.

Sorry, I take back what I said about changing these functions to accomodate ShadowRoot. I it makes more senes to pass node.host into getNodeDetails when node is an instance of ShadowRoot. This way we can remove these lines and don't have to change jsdoc to be Element|ShadowRoot everywhere.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

WDYT about getNodeDetails doing this check? Historically when the responsibility to support ShadowRoots is pushed to the caller of a function it's almost always forgotten :)

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

SGTM. Sorry for changing so frequently @adrianaixba 😅

Copy link
Collaborator Author

@adrianaixba adrianaixba Oct 23, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@adamraine no worries! The more discussion, the better 😄

@adamraine
Copy link
Member

adamraine commented Oct 23, 2020

Looks like the reason getOuterHTMLSnippet accepts a ShadowRoot is because of dom stats sometimes passing one in: #9079

@brendankenny @patrickhulce Does it make sense to move the if(element instanceof ShadowRoot) logic from getOuterHTMLSnippet to the dom stats gatherer?

@patrickhulce
Copy link
Collaborator

Does it make sense to move the if(element instanceof ShadowRoot) logic from getOuterHTMLSnippet to the dom stats gatherer?

That's one option. I think the broader problem though is exactly what @adrianaixba stumbled on to. It's not super clear what our existing gatherers assume/support about ShadowRoots and prior to #9079 roots were still getting passed into that fn without any type errors (I assume due to our sad "@ts-ignore put into scope via stringification" which means these arguments are never really type checked 😢 ).

I would have a preference for normalizing Element|ShadowRoot into Element in any publicly exposed methods in page-functions to avoid the caller having to remember that responsibility in every gatherer.

selector: getNodeSelector(elem),
boundingRect: getBoundingClientRect(elem),
selector: getNodeSelector(shadowRootElem),
boundingRect: getBoundingClientRect(shadowRootElem),
snippet: getOuterHTMLSnippet(elem),
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

decided to leave these as is, since they take care of the shadowRoot case. @adamraine @patrickhulce what do y'all think?

Copy link
Member

@adamraine adamraine Oct 26, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

getNodePath will return different results for ShadowRoot and its host element, so leaving that as is was a good call.

@patrickhulce Would you prefer keeping the ShadowRoot logic in getOuterHTMLSnippet even though it won't be used anywhere after this patch?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree, I wouldn't go out of our way to remove ShadowRoot support from anything just yet, even if the path is currently being covered by our other logic. If we also removed the functions from the public interface of this module, then I'd feel less strongly, but seems like that step would benefit from a more targeted and comprehensive plan for what to do with shadow elements across all of lighthouse that feels out of scope for this PR.

Copy link
Member

@adamraine adamraine left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Whoops, lost track of this 😬

Overall LGTM


{"_comment": "Manually added event to make sample lhr not error", "name":"largestContentfulPaint::Candidate", "pid":75994,"tid":17667,"ts":185608247190,"ph":"R","cat":"loading,rail,devtools.timeline", "args": {"frame": "0x44d2861df8"}},
{"_comment": "Manually added event to make test CLS", "name":"LayoutShift", "pid":75994,"tid":775,"ts":185608247190,"ph":"R","cat":"loading,rail,devtools.timeline", "args": {"frame": "0x44d2861df8", "data": {"is_main_frame": true, "cumulative_score": 0.42}}},

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This seems unrelated. I don't mind the style change, but is it related to this patch somehow?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@adamraine This is unrelated. It's just a style change that has continued to pop up for me for some reason, I could revert.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the change is good, its probably too small for its own PR so I'm ok leaving it in this one :)

@@ -453,12 +452,13 @@ const getNodeDetailsString = `function getNodeDetails(elem) {
${getBoundingClientRect.toString()};
${getOuterHTMLSnippet.toString()};
${getNodeLabel.toString()};
const shadowRootElem = elem instanceof ShadowRoot ? elem.host : elem;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: the value of shadowRootElem will not necessarily be a shadow root. I think something like htmlElem makes more sense.


return name;
}

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

love it

Copy link
Member

@adamraine adamraine left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

@adrianaixba adrianaixba merged commit a73e19a into master Nov 9, 2020
@adrianaixba adrianaixba deleted the domstats-devtools branch November 9, 2020 23:45
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

domstats gathererer doesn't get devtools node path, uses custom "pathToElement"
5 participants