Skip to content

Migrate activity to doc-scope and fix instrumentation resolution#6241

Merged
mkhatib merged 7 commits intoampproject:masterfrom
mkhatib:analytics-docs
Nov 30, 2016
Merged

Migrate activity to doc-scope and fix instrumentation resolution#6241
mkhatib merged 7 commits intoampproject:masterfrom
mkhatib:analytics-docs

Conversation

@mkhatib
Copy link
Copy Markdown
Contributor

@mkhatib mkhatib commented Nov 18, 2016

More for: #5565

Testing this with pwa.html example 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.

this.instrumentation_.addListener(
trigger, this.handleEvent_.bind(this, trigger), this.element);

this.instrumentationPromise_.then(instrumentation => {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

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.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Done

<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>
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

what does this do?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

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.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Not really needed.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Dropped

import {installCidService} from './cid-impl';
import {installCryptoService} from './crypto-impl';
import {installActivityService} from './activity-impl';
import {Activity} from './activity-impl';
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Sort

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Done... I hate sorting...

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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 => {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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(
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

getServicePromiseForDoc makes more sense here.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Done.

export function activityForDoc(nodeOrDoc) {
return /** @type {!Promise<!Activity>} */ (
getElementService(win, 'activity', 'amp-analytics'));
getElementServiceForDoc(nodeOrDoc, 'activity', 'amp-analytics'));
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Thanks for explaining. Ack.


this.set('BACKGROUND_STATE', () => {
return viewerForDoc(this.ampdoc.win.document).isVisible() ? '0' : '1';
return viewerForDoc(this.ampdoc).isVisible() ? '0' : '1';
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

ugh...

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Is this not accurate? Or the ugh on the old change? 😄

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Ack

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

sorry :/

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

:)


return this.consentPromise_
return instrumentationServiceForDoc(this.getAmpDoc())
.then(instrumentation => {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

As mentioned before, can you move this then after fetchRemoteConfig_? No reason to delay fetching remote data.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Done

@mkhatib
Copy link
Copy Markdown
Contributor Author

mkhatib commented Nov 22, 2016

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)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

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

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

@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.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This has to be the nearest context node you can find. Here, it could be either:

  1. this.element
  2. event.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.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Used this.element. I am not entirely sure but event object in this context doesn't seem to include a target.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

yes, it is not the js/dom event object but a struct defined by instrumentation.js.

}
}

let foundEl;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Would moving foundEl above the preceeding if block help? i think the contained chack can be abllied to that as well.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

You mean the closestBySelector(parentEl, '.-amp-element') result be checked if is contained in doc?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Done


this.set('BACKGROUND_STATE', () => {
return viewerForDoc(this.ampdoc.win.document).isVisible() ? '0' : '1';
return viewerForDoc(this.ampdoc).isVisible() ? '0' : '1';
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

sorry :/

foundEl = el.parentElement.querySelector(selector);
} else if (selector[0] == '#') {
return ampdoc.getElementById(selector.slice(1));
foundEl = el.ownerDocument.getElementById(selector.slice(1));
Copy link
Copy Markdown
Contributor Author

@mkhatib mkhatib Nov 23, 2016

Choose a reason for hiding this comment

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

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?

Copy link
Copy Markdown
Contributor

@dvoytenko dvoytenko Nov 23, 2016

Choose a reason for hiding this comment

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

Yes, this is not entirely correct. The situations you have to consider here are:

  1. The el is in the ampdoc directly. In that case the answer is simply ampdoc.getElementById().
  2. If the el is in the friendly iframe, then it should be ampdoc.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.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

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?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

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.

Copy link
Copy Markdown
Contributor

@avimehta avimehta Nov 24, 2016

Choose a reason for hiding this comment

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

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. el in this function is one such amp-analytics tag.

  • el contains some config that specifies that certain behavior for another element needs to be tracked. The element is identified using selector and selectionMethod. Lets call this element trackedElement.

  • Generally, trackedElement should always be in the same document as the el. So if el is in shadow dom or friendly iframe, trackedElement should 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.
  • How we search for trackedElement is determined by selectionMethod.

    • 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 of el
    • for no selectionMethod, we just search the DOM for the element.

hope this helps...

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

@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().

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

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())
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Done. this.element is the right one.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

👍

}
}

let foundEl;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

ampdoc is missing above in the @param

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed.

// Special case for root selector.
if (selector == ':host' || selector == ':root') {
const elWin = ampdoc.win;
const parentEl = elWin.frameElement && elWin.frameElement.parentElement;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

cc/ @avimehta here, I think he's the original author here, just to make sure we're not missing anything.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

yes, @dvoytenko is correct. .parentElement is an artifact of initial implementation. Please remove it and use the helper method instead.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Done.

foundEl = el.parentElement.querySelector(selector);
} else if (selector[0] == '#') {
return ampdoc.getElementById(selector.slice(1));
foundEl = el.ownerDocument.getElementById(selector.slice(1));
Copy link
Copy Markdown
Contributor

@dvoytenko dvoytenko Nov 23, 2016

Choose a reason for hiding this comment

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

Yes, this is not entirely correct. The situations you have to consider here are:

  1. The el is in the ampdoc directly. In that case the answer is simply ampdoc.getElementById().
  2. If the el is in the friendly iframe, then it should be ampdoc.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(
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This this should become ampdoc.contains(parentEl || foundEl)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Done

@mkhatib
Copy link
Copy Markdown
Contributor Author

mkhatib commented Nov 29, 2016

PTAL 👀

// Restrict result to be contained by ampdoc.
if (closestEl && ampdoc.contains(closestEl)) {
return closestEl;
const isContainedInDoc = ampdoc.contains(friendlyFrame || foundEl);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

don't need to create the const i think.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

yeah I just thought it's easier to read this way.

Copy link
Copy Markdown
Contributor

@avimehta avimehta left a comment

Choose a reason for hiding this comment

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

lgtm from my side.

return closestBySelector(parentEl, '.-amp-element');
}
if (friendlyFrame && (selector == ':host' || selector == ':root')) {
foundEl = closestBySelector(friendlyFrame, '.-amp-element');
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Done.

@mkhatib mkhatib merged commit 7ff0f3e into ampproject:master Nov 30, 2016
@mkhatib mkhatib deleted the analytics-docs branch November 30, 2016 01:24
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.

4 participants