Migrate activity to doc-scope and fix instrumentation resolution#6241
Migrate activity to doc-scope and fix instrumentation resolution#6241mkhatib merged 7 commits intoampproject:masterfrom
Conversation
| this.instrumentation_.addListener( | ||
| trigger, this.handleEvent_.bind(this, trigger), this.element); | ||
|
|
||
| this.instrumentationPromise_.then(instrumentation => { |
There was a problem hiding this comment.
would it be simpler to set the value of instrumentation service at the end of layoutcallback. So something like this:
return instrumentationServiceForDoc(this.getAmpDoc()))
.then(instrumentation => {this.instrumentation_ = instrumentation;})
.then(() => this.consentPromise_)
.then(this.fetchRemoteConfig_.bind(this))
.then(this.onFetchRemoteConfigSuccess_.bind(this));
There was a problem hiding this comment.
Yes this would delay the layout for amp-analytics but it might make sense in this case since I guess analytics functionality is dependent on instrumentation being ready.
There was a problem hiding this comment.
Yes. @avimehta's suggestion is better. There won't be any real layout delays. But what we want is to resolve instrumentation after we start fetch.
examples/analytics.amp.html
Outdated
| <script async custom-element="amp-analytics" src="https://cdn.ampproject.org/v0/amp-analytics-0.1.js"></script> | ||
| <style amp-boilerplate>body{-webkit-animation:-amp-start 8s steps(1,end) 0s 1 normal both;-moz-animation:-amp-start 8s steps(1,end) 0s 1 normal both;-ms-animation:-amp-start 8s steps(1,end) 0s 1 normal both;animation:-amp-start 8s steps(1,end) 0s 1 normal both}@-webkit-keyframes -amp-start{from{visibility:hidden}to{visibility:visible}}@-moz-keyframes -amp-start{from{visibility:hidden}to{visibility:visible}}@-ms-keyframes -amp-start{from{visibility:hidden}to{visibility:visible}}@-o-keyframes -amp-start{from{visibility:hidden}to{visibility:visible}}@keyframes -amp-start{from{visibility:hidden}to{visibility:visible}}</style><noscript><style amp-boilerplate>body{-webkit-animation:none;-moz-animation:none;-ms-animation:none;animation:none}</style></noscript> | ||
| <script async src="https://cdn.ampproject.org/v0.js"></script> | ||
| <script async src="./viewer-integr.js" data-amp-report-test="viewer-integr.js"></script> |
There was a problem hiding this comment.
I thought this might be needed for the PWA stuff, but not sure. This is usually included for examples to be able to be included in the viewer.html demo.
| import {installCidService} from './cid-impl'; | ||
| import {installCryptoService} from './crypto-impl'; | ||
| import {installActivityService} from './activity-impl'; | ||
| import {Activity} from './activity-impl'; |
There was a problem hiding this comment.
Done... I hate sorting...
There was a problem hiding this comment.
It's actually quite pleasant once you get used to it.
| this.instrumentation_.addListener( | ||
| trigger, this.handleEvent_.bind(this, trigger), this.element); | ||
|
|
||
| this.instrumentationPromise_.then(instrumentation => { |
There was a problem hiding this comment.
Yes. @avimehta's suggestion is better. There won't be any real layout delays. But what we want is to resolve instrumentation after we start fetch.
| return /** @type {!InstrumentationService} */ (getExistingServiceForDoc( | ||
| nodeOrDoc, 'amp-analytics-instrumentation')); | ||
| return /** @type {!Promise<InstrumentationService>} */ ( | ||
| getElementServiceForDoc( |
There was a problem hiding this comment.
getServicePromiseForDoc makes more sense here.
| export function activityForDoc(nodeOrDoc) { | ||
| return /** @type {!Promise<!Activity>} */ ( | ||
| getElementService(win, 'activity', 'amp-analytics')); | ||
| getElementServiceForDoc(nodeOrDoc, 'activity', 'amp-analytics')); |
There was a problem hiding this comment.
FYI: this is the correct use of getElementServiceForDoc. However, getServicePromiseForDoc is better in the instrumentation.js. The reason is that this is a public declaration of the service.
There was a problem hiding this comment.
Thanks for explaining. Ack.
|
|
||
| this.set('BACKGROUND_STATE', () => { | ||
| return viewerForDoc(this.ampdoc.win.document).isVisible() ? '0' : '1'; | ||
| return viewerForDoc(this.ampdoc).isVisible() ? '0' : '1'; |
There was a problem hiding this comment.
Is this not accurate? Or the ugh on the old change? 😄
There was a problem hiding this comment.
The old code is "ugh". I'm thinking maybe we should consider prohibiting doc-service lookup via Document argument to avoid this kind of silly bugs.
|
|
||
| return this.consentPromise_ | ||
| return instrumentationServiceForDoc(this.getAmpDoc()) | ||
| .then(instrumentation => { |
There was a problem hiding this comment.
As mentioned before, can you move this then after fetchRemoteConfig_? No reason to delay fetching remote data.
|
PTAL 👀 |
|
|
||
| // For consistency with amp-pixel we also expand any url replacements. | ||
| return urlReplacementsForDoc(this.win.document).expandAsync(request) | ||
| return urlReplacementsForDoc(this.getAmpDoc()).expandAsync(request) |
There was a problem hiding this comment.
I am not completely sure if this is correct. I think this will return the service for the main window. What we want is the service a) for the embed if inside an embed and b) for the top level doc if the element is not in embed.
Same above and below.
There was a problem hiding this comment.
Yeah, we might need to revert this to use this.win.document then. However, this throws an error AFAIR but I can experiment some more.
cc/ @dvoytenko
There was a problem hiding this comment.
@dvoytenko using this.win.document I get errors like:
Error: No ampdoc found for [object HTMLDocument]
Is this supposed to work?
What seems to be working is to pass this.element as we discussed.
There was a problem hiding this comment.
This has to be the nearest context node you can find. Here, it could be either:
this.elementevent.target
I think the first option is better, but you need to check where events come from. This is something you should also document in comments very well.
There was a problem hiding this comment.
Used this.element. I am not entirely sure but event object in this context doesn't seem to include a target.
There was a problem hiding this comment.
yes, it is not the js/dom event object but a struct defined by instrumentation.js.
| } | ||
| } | ||
|
|
||
| let foundEl; |
There was a problem hiding this comment.
Would moving foundEl above the preceeding if block help? i think the contained chack can be abllied to that as well.
There was a problem hiding this comment.
You mean the closestBySelector(parentEl, '.-amp-element') result be checked if is contained in doc?
There was a problem hiding this comment.
Not sure if that's the right response to the original question. But checking that the result is in the ampdoc is a good idea.
|
|
||
| this.set('BACKGROUND_STATE', () => { | ||
| return viewerForDoc(this.ampdoc.win.document).isVisible() ? '0' : '1'; | ||
| return viewerForDoc(this.ampdoc).isVisible() ? '0' : '1'; |
| foundEl = el.parentElement.querySelector(selector); | ||
| } else if (selector[0] == '#') { | ||
| return ampdoc.getElementById(selector.slice(1)); | ||
| foundEl = el.ownerDocument.getElementById(selector.slice(1)); |
There was a problem hiding this comment.
This is actually more tricky than anticipated.
In embedded this will work.
In PWA this will fail since getElementById won't pierce shadow dom.
So this has to change to become something like:
let doc = ampdoc;
if (elWin !== ampdoc.win) {
doc = el.ownerDocument;
}
foundEl = doc.getElementById(selector.slice(1));@dvoytenko am I getting something wrong?
There was a problem hiding this comment.
Yes, this is not entirely correct. The situations you have to consider here are:
- The
elis in theampdocdirectly. In that case the answer is simplyampdoc.getElementById(). - If the
elis in the friendly iframe, then it should beampdoc.getElementById()el.ownerDocument.getElementById().
Whether it's option 1 or 2 you can tell by the presence of parentEl above. You probably want to refactor it a bit to use throughout.
Eventually we'll need to build out the math for all this logic, but let's first see what it is here.
Finally: this logic is not so much hard as it just has to deal with multiple environments. It's important to present them all in tests. Right now, from what I can tell, the tests are insufficient. Let's add more.
There was a problem hiding this comment.
Not sure I understand your reply, can you double check I think you might have missed something. Are you saying in both cases we need to use ampdoc.getElementById?
If the el is in the friendly iframe, then it should be ampdoc.getElementById().
This is inaccurate, ampdoc.getElementById will always return null in this case because it won't pierce the iframe window, did you mis-type this or am I missing something?
There was a problem hiding this comment.
I think overall I need a better understanding for what getElement should and shouldn't return. I'll draft a small document later and see if I can chat with @avimehta to figure that out.
There was a problem hiding this comment.
here is the summary. We can discuss it more over chat or gvc:
-
Analytics can be done on a page by using amp-analytics tag. There can be more than one of these tags.
elin this function is one such amp-analytics tag. -
elcontains some config that specifies that certain behavior for another element needs to be tracked. The element is identified usingselectorandselectionMethod. Lets call this elementtrackedElement. -
Generally,
trackedElementshould always be in the same document as theel. So ifelis in shadow dom or friendly iframe,trackedElementshould be in the shadow dom or friendly iframe as well.- One exception to this is when ':root' selector is specified. In this case, we travel through the ancestor chain and return the first
.-amp-element. This allows for tracking of shadow-dom or iframe as a whole unit.
- One exception to this is when ':root' selector is specified. In this case, we travel through the ancestor chain and return the first
-
How we search for
trackedElementis determined byselectionMethod.- for
selectionMethod == closest, we only look in ancestor chain and find the element that matches. - for
selectionMethod == scope, we only look for elements in parent element ofel - for no
selectionMethod, we just search the DOM for the element.
- for
hope this helps...
There was a problem hiding this comment.
@mkhatib Apologies for confusion. I meant to say:
If the el is in the friendly iframe, then it should be
ampdoc.getElementById()el.ownerDocument.getElementById().
There was a problem hiding this comment.
Thanks @avimehta for the explaination that helped a lot. I updated the code to reflect this.
I've also added some more expectations for the embedded case.
| if (threshold >= 0 && threshold <= 100) { | ||
| const key = this.expandTemplate_(spec['sampleOn'], trigger); | ||
| const keyPromise = urlReplacementsForDoc(this.win.document) | ||
| const keyPromise = urlReplacementsForDoc(this.getAmpDoc()) |
There was a problem hiding this comment.
Ditto: we need to figure out what's the right argument here. My general reaction is that it should be this.element most of the time, but please check.
There was a problem hiding this comment.
Done. this.element is the right one.
| } | ||
| } | ||
|
|
||
| let foundEl; |
There was a problem hiding this comment.
Not sure if that's the right response to the original question. But checking that the result is in the ampdoc is a good idea.
| return null; | ||
| } | ||
|
|
||
| const elWin = el.ownerDocument.defaultView; |
There was a problem hiding this comment.
ampdoc is missing above in the @param
| // Special case for root selector. | ||
| if (selector == ':host' || selector == ':root') { | ||
| const elWin = ampdoc.win; | ||
| const parentEl = elWin.frameElement && elWin.frameElement.parentElement; |
There was a problem hiding this comment.
Please switch to use getParentWindowFrameElement from service.js for frameElement calculation. Looking at this, I actually no idea why we need .parentElement here. I don't think it's needed. The .frameElement (returned by getParentWindowFrameElement) is in itself the parentEl.
There was a problem hiding this comment.
cc/ @avimehta here, I think he's the original author here, just to make sure we're not missing anything.
There was a problem hiding this comment.
yes, @dvoytenko is correct. .parentElement is an artifact of initial implementation. Please remove it and use the helper method instead.
| foundEl = el.parentElement.querySelector(selector); | ||
| } else if (selector[0] == '#') { | ||
| return ampdoc.getElementById(selector.slice(1)); | ||
| foundEl = el.ownerDocument.getElementById(selector.slice(1)); |
There was a problem hiding this comment.
Yes, this is not entirely correct. The situations you have to consider here are:
- The
elis in theampdocdirectly. In that case the answer is simplyampdoc.getElementById(). - If the
elis in the friendly iframe, then it should beampdoc.getElementById()el.ownerDocument.getElementById().
Whether it's option 1 or 2 you can tell by the presence of parentEl above. You probably want to refactor it a bit to use throughout.
Eventually we'll need to build out the math for all this logic, but let's first see what it is here.
Finally: this logic is not so much hard as it just has to deal with multiple environments. It's important to present them all in tests. Right now, from what I can tell, the tests are insufficient. Let's add more.
|
|
||
| if (foundEl) { | ||
| // Restrict result to be contained by ampdoc. | ||
| const isContainedInDoc = ampdoc.contains( |
There was a problem hiding this comment.
This this should become ampdoc.contains(parentEl || foundEl)
|
PTAL 👀 |
| // Restrict result to be contained by ampdoc. | ||
| if (closestEl && ampdoc.contains(closestEl)) { | ||
| return closestEl; | ||
| const isContainedInDoc = ampdoc.contains(friendlyFrame || foundEl); |
There was a problem hiding this comment.
don't need to create the const i think.
There was a problem hiding this comment.
yeah I just thought it's easier to read this way.
| return closestBySelector(parentEl, '.-amp-element'); | ||
| } | ||
| if (friendlyFrame && (selector == ':host' || selector == ':root')) { | ||
| foundEl = closestBySelector(friendlyFrame, '.-amp-element'); |
There was a problem hiding this comment.
I believe we should do this:
if (selector == :host || selector == :root) {
foundEl = friendlyFrame ? closestBySelector(...) : null;
}
Otherwise the code in else below does not actually produce the right result. We have a follow up issue for @avimehta to address this, but to start with it should not produce a wrong result.
More for: #5565
Testing this with
pwa.htmlexample looks good. Analytics events triggered and all. But something seems broken for some reason in that example, like I can't really see the document (or scroll it) but I was able to do scroll-element-into-view through the elements panel in devtools. So not sure what's going on there - body for the shadow dom is 0px height.@dvoytenko as we chatted, instrumentation is now resolved async in amp-analytics due to the race condition between the life cycle of the element and the time we actually install these services.
Let me know if this looks good so I can fix up the tests.