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

Amp-Analytics 3p vendor obfuscation #8887

Closed
wants to merge 11 commits into from
Closed

Amp-Analytics 3p vendor obfuscation #8887

wants to merge 11 commits into from

Conversation

jonkeller
Copy link
Contributor

@jonkeller jonkeller commented Apr 22, 2017

Allows third-party analytics vendors to use the amp-analytics tag. They can now specify requestsFrameUrl in vendors.js, and that will be loaded as a cross-domain iframe. It will be sent the events, and they can use arbitrary JS to process them. The transport glue will not be subject to version lock.

Closes #9433

if (onNewAmpAnalyticsInstance) {
onNewAmpAnalyticsInstance(instance);
// Warning: the following code is likely only temporary. Don't check in
// before getting resolution on that.
Copy link
Contributor Author

Choose a reason for hiding this comment

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

^^ See comment

<script async custom-element="amp-ad" src="https://cdn.ampproject.org/v0/amp-ad-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>
<style amp-custom>
Copy link
Contributor Author

@jonkeller jonkeller Apr 22, 2017

Choose a reason for hiding this comment

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

Fake ad network is broken! Created Issue #8935
The following CSS hack fixes display issues. The next file (see below) is a .json file, which in the existing example contains a JSON object, one of whose values is the HTML of the fake creative. However the fake ad JS code ends up just displaying the entire JSON, not just the HTML. So that's why, for now, the next file is .json but contains only HTML.
We should fix the fake ad network. I can tackle that in a separate PR, and then pretty this one up. But for now, this file and the next contain hacks to get it functional.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

/**
* Sends a message to a cross-domain frame, or queues the message if the
* frame is not yet ready to receive messages.
* TODO: Implement throttling if messages are sent too rapidly.
Copy link
Contributor Author

Choose a reason for hiding this comment

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

^^ TODO

}
config['requestsFrameUrl'] += config['dataHash'];
}
// TODO: Safety check requestsFrameUrl!
Copy link
Contributor Author

Choose a reason for hiding this comment

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

^^TODO

}
});
// TODO: Could also do something like:
// instance.sendMessageToCreative('hello');
Copy link
Contributor Author

Choose a reason for hiding this comment

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

^^ TODO

@@ -694,4 +719,100 @@ export class AmpAnalytics extends AMP.BaseElement {
}
}

/**
* Keeps track of the third-party vendors' cross-domain iframes. Only one
* static instance will be created, which will keep manage ALL amp-analytics
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Typo: should be "keep and manage"

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Fixed/moot

@keithwrightbos keithwrightbos requested review from bradfrizzell and removed request for tdrl April 24, 2017 14:09
@keithwrightbos
Copy link
Contributor

@bradfrizzell to take first pass on review, please notify me when you think it is ready for my review.

@@ -0,0 +1 @@
<!-- No, this is not actually JSON. Fake ad network is kind of broken and should be fixed separately. --><!doctype html><html amp4ads> <head> <meta charset=utf-8> <script async src='https://cdn.ampproject.org/v0.js'></script> <script async custom-element=amp-analytics src='https://cdn.ampproject.org/v0/amp-analytics-0.1.js'></script> <style amp4ads-boilerplate>body{visibility:hidden}</style> <meta name=viewport content=width=device-width,minimum-scale=1> </head> <body> <amp-img id=img src='https://upload.wikimedia.org/wikipedia/commons/6/6e/Golde33443.jpg' width=256 height=300></amp-img> <amp-analytics> <script type='application/json'> { "requestsFrameUrl": "http://localhost:8000/examples/analytics-3p-remote-frame.html", "dataHash": "key1=val1&key2=val2", "requests": { "pageview": "https://example.com/analytics?viewed=true&url=${canonicalUrl}&title=${title}&acct=${account}", "event": "https://example.com/analytics?eid=click&acct=${account}" }, "vars": { "account": "ABC123" }, "triggers": { "defaultPageview": { "on": "visible", "request": "pageview" }, "anchorClicks": { "on": "click", "selector": "img", "request": "event" } } } </script> </amp-analytics> </body></html>
Copy link
Contributor

Choose a reason for hiding this comment

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

If this isn't JSON, why is it in a .json file rather than .html?

Copy link
Contributor Author

@jonkeller jonkeller Apr 24, 2017

Choose a reason for hiding this comment

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

See comment above beginning with "Fake ad network is broken!". I'll go fix it in a separate PR, and then correct this here. But tl;dr if you put JSON like it says you're supposed to, you see the raw JSON in your ad, and if you try to specify <amp-ad type="fake" src="foo.html"> rather than foo.json, then you see nothing at all.

Copy link
Contributor

Choose a reason for hiding this comment

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

whoops sorry see that now

@@ -256,6 +259,23 @@ export class AmpAnalytics extends AMP.BaseElement {
return Promise.all(promises);
}

handleRequestsFrameUrl_(config) {
Copy link
Contributor

Choose a reason for hiding this comment

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

Add jsdoc

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done

@@ -256,6 +259,23 @@ export class AmpAnalytics extends AMP.BaseElement {
return Promise.all(promises);
}

handleRequestsFrameUrl_(config) {
if (config['requestsFrameUrl'] && config['dataHash']) {
Copy link
Contributor

Choose a reason for hiding this comment

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

make sure in the jsdoc that config is explicitly non-nullable, otherwise this predicate can throw

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done

*/
class AmpAnalyticsFrames {
constructor() {
this.map = new Map();
Copy link
Contributor

Choose a reason for hiding this comment

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

Name something more descriptive than 'map'. What is the purpose of this map? Could the various properties stored within all instead be instance variables on the class? It seems like the map is a layer of obfuscation that will make reading this class less clear in the future (unless there is a reason for the map I don't know).

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done

*/
create(parent, requestsFrameUrl) {
const context = {
"scriptSrc": "/examples/analytics-3p-remote-frame-helper.js",
Copy link
Contributor

Choose a reason for hiding this comment

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

nit: I believe these should be single quotes, no?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Agree. Also note that this is the flip side of my comment on examples/analytics-3p-remote-frame-helper.js line 17.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done

@@ -0,0 +1,26 @@

class AmpAnalyticsInstance {
Copy link
Contributor

Choose a reason for hiding this comment

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

Just name the class AmpAnalytics or something else without Instance in the name. As it is now, an instance of this class is an AmpAnalyticsInstance instance, which is confusing.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done

this.listener = listener;
}
};
let instance = new AmpAnalyticsInstance();
Copy link
Contributor

Choose a reason for hiding this comment

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

variable naming

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done / embarrassed


// The onNewAmpAnalyticsInstance() function must be implemented by the
// vendor's page
if (onNewAmpAnalyticsInstance) {
Copy link
Contributor

Choose a reason for hiding this comment

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

this should be window.onNewAmpAnalyticsInstance, as per the example frame.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done

<meta charset="UTF-8">
<title>Requests Frame</title>
<script>
window.onNewAmpAnalyticsInstance = (instance) => {
Copy link
Contributor

Choose a reason for hiding this comment

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

jsdoc would make this example more useful

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done, please let me know if not sufficient.

send(requestsFrameUrl, message) {
try {
const mapEntry = this.map.get(requestsFrameUrl);
if (mapEntry.isReady) {
Copy link
Contributor

Choose a reason for hiding this comment

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

mapEntry here as well

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done: renamed to frameData, refactored to transport.js.

*/
handleRequestsFrameUrl_(config) {
if (config['requestsFrameUrl'] && config['dataHash']) {
if (!config['dataHash'].startsWith('#')) {
Copy link
Contributor

Choose a reason for hiding this comment

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

Please use the startsWith helper.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done

if (!config['dataHash'].startsWith('#')) {
config['requestsFrameUrl'] += '#';
}
config['requestsFrameUrl'] += config['dataHash'];
Copy link
Contributor

Choose a reason for hiding this comment

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

Is it really necessary to mutate this value? This method isn't reentrant as is.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

You're right, this is kind of sketchy. My logic at the time is that the map is going to be keyed off of the concatenation, so might as well not duplicate that (admittedly small) effort every time we do a get/set/has on it. But...yeah, it's wrong. Will fix. Thanks.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done

*/
class AmpAnalyticsFrames {
constructor() {
this.requestsFrameMap = new Map();
Copy link
Contributor

Choose a reason for hiding this comment

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

Map is not available in all environments. Please use a prototype-less object (we have a helper).

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done

let frame = createElementWithAttributes(parent, 'iframe', {
sandbox: 'allow-scripts',
name: JSON.stringify({
'scriptSrc': '/examples/analytics-3p-remote-frame-helper.js'
Copy link
Contributor

Choose a reason for hiding this comment

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

Hm?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yeah, comment at L803 applies here as well. I should annotate in the code that this also needs to be replaced with something more permanent before checking in.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@keithwrightbos @jridgewell Where do you guys think the helper JS should live?

'scriptSrc': '/examples/analytics-3p-remote-frame-helper.js'
}),
});
frame.onload = () => {
Copy link
Contributor

Choose a reason for hiding this comment

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

Please use loadPromise

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done

this.setIsReady(requestsFrameUrl);
};
frame.src = requestsFrameUrl; // Set AFTER onload to avoid race
frame.style.cssText='width:0;height:0;visibility:hidden;';
Copy link
Contributor

Choose a reason for hiding this comment

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

Please use setStyles

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done

requestsFrame.msgQueue.push(message);
}
} catch (err) {
console.error("Failed to send event '" + err + "' to URL '" +
Copy link
Contributor

Choose a reason for hiding this comment

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

There's not an equivalent catch in the deferred case.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Removed

requestsFrame.isReady = true;
this.send_(requestsFrame.frame, requestsFrame.msgQueue);
requestsFrame.msgQueue = [];
this.requestsFrameMap.set(requestsFrameUrl, requestsFrame);
Copy link
Contributor

Choose a reason for hiding this comment

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

You can avoid the reset by truncating the array.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done (just deleted it instead of truncating, since will not need it again.)

* static instance will be created, which will keep and manage ALL amp-analytics
* tags.
*/
class AmpAnalyticsFrames {
Copy link
Contributor

Choose a reason for hiding this comment

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

This likely belongs in another class, hopefully one that reduces the duplication between this and transport.js.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Agree. Was not able to get to this today, but will do.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done - refactored into transport.js

requestsFrame.isReady = true;
this.send_(requestsFrame.frame, requestsFrame.msgQueue);
requestsFrame.msgQueue = [];
this.requestsFrameMap.set(requestsFrameUrl, requestsFrame);
Copy link
Contributor Author

Choose a reason for hiding this comment

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

@jridgewell You commented earlier here about avoiding the reset. I tested simply removing the .set() here and it works fine but please let me know if that is what you were intending.

}),
});
frame.onload = () => {
loadPromise(frame).then(() => {
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Linter seems wary of this. Is this how you intended for loadPromise to be used here, @jridgewell ?

[16:45:49] ========== [16:45:49] Found forbidden: "{loadPromise" in extensions/amp-analytics/0.1/amp-analytics.js:19:8 [16:45:49] Most users should use BaseElement…loadPromise. [16:45:49] ========== [16:45:49] Found forbidden: " loadPromise" in extensions/amp-analytics/0.1/amp-analytics.js:771:4 [16:45:49] Most users should use BaseElement…loadPromise. [16:45:49] ========== [16:45:53] Please remove these usages or consult with the AMP team.

const requestsFrameKey = this.buildRequestsFrameUrlWithDataHash(
this.config_['requestsFrameUrl'], this.config_['dataHash']);
AmpAnalytics.requestsFrames.send(requestsFrameKey,
request);
Copy link
Contributor Author

Choose a reason for hiding this comment

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

This is another case where extracting requestsFrameKey is not strictly necessary, but looks better to my eye, as to not do so would be very cluttered. Please let me know if anyone disagrees.

Copy link
Contributor

@bradfrizzell bradfrizzell left a comment

Choose a reason for hiding this comment

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

I'm still finding the naming of requestsFrameMap and requestsFrameUrl etc to be pretty confusing. A 'requestsFrame' is a third party amp analytics iframe, right? And the map is a map of ping urls to the iframes? And the url is an analytics ping url? Is all that correct? If so, I think this code would be made a lot more readable with different names.

const frame = createElementWithAttributes(parent, 'iframe', {
sandbox: 'allow-scripts',
name: JSON.stringify({
'scriptSrc': '/examples/analytics-3p-remote-frame-helper.js',
Copy link
Contributor

Choose a reason for hiding this comment

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

// DO NOT SUBMIT

Copy link
Contributor

Choose a reason for hiding this comment

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

Agreed! This cannot be a relative url

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Let's discuss what it should be. https://cdn.ampproject.org/something... but I'm not clear on the semantics of /c and /c/s etc. after that.

loadPromise(frame).then(() => {
this.setIsReady(requestsFrameUrl);
});
frame.src = requestsFrameUrl; // Set AFTER loadPromise to avoid race
Copy link
Contributor

Choose a reason for hiding this comment

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

If this needs to be done after loadPromise is done, the current implementation will not do that. loadPromise returns a promise, and frame.src will be set immediately after the previous line, whether or not that promise has actually resolved. Is that an issue?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I don't need to do it after the promise is done, just after the promise is set up. In other words I don't want the sequence to be: 1. src is set, 2. frame is loaded, 3. ready event is fired, 4. we set up something to listen for the ready event, because then the ready event may not get received by anything. I may be being overzealous here, given JavaScript's single-threadedness, though.

Copy link
Contributor

Choose a reason for hiding this comment

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

Ah I see. Just wanted clarification based on your comment there

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Fair point. I can update the comment to make it clearer.

@jonkeller
Copy link
Contributor Author

@bradfrizzell Re: the "I'm still finding..." comment, basically yes, you understand correctly. I will change the names, both because you're right that they're a bit confusing, and because the JSON config format just changed such that "requestsFrameUrl" is no longer called that.

Copy link
Contributor

@keithwrightbos keithwrightbos left a comment

Choose a reason for hiding this comment

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

We should handle the case where we remove the iframe if all amp analytics instances of a given type (e.g. moat) have been removed via unlayoutCallback.

@@ -0,0 +1,26 @@

Copy link
Contributor

Choose a reason for hiding this comment

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

Add copyright comment?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done


class AmpAnalyticsRemoteFrameManager {
constructor() {
this.listener = null;
Copy link
Contributor

Choose a reason for hiding this comment

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

private? type doc?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done

this.listener = listener;
}
};
let remoteFrameMgr = new AmpAnalyticsRemoteFrameManager();
Copy link
Contributor

Choose a reason for hiding this comment

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

const?

*/
window.onNewAmpAnalyticsInstance = (instance) => {
instance.registerAmpAnalyticsEventListener((arrayOfEventStrings) => {
for (evt of arrayOfEventStrings) {
Copy link
Contributor

Choose a reason for hiding this comment

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

I personally prefer arrayOfEventStrings.forEach(evt => {
....
});

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done

@@ -199,6 +202,8 @@ export class AmpAnalytics extends AMP.BaseElement {
this.analyticsGroup_ =
this.instrumentation_.createAnalyticsGroup(this.element);

this.handleRequestsFrameUrl_();
Copy link
Contributor

Choose a reason for hiding this comment

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

This can be triggered from buildCallback if the amp-analytics element has attribute trigger=immediate but we definitely cannot modify the page at that point. Instead we should do this explicitly as part of layoutCallback. Also we need to find a way to "deprioritize" the frame creation as current code would give it priority of 1 while we want only the frame creation to be 2. This will need some discussion with Dima.

const frame = createElementWithAttributes(parent, 'iframe', {
sandbox: 'allow-scripts',
name: JSON.stringify({
'scriptSrc': '/examples/analytics-3p-remote-frame-helper.js',
Copy link
Contributor

Choose a reason for hiding this comment

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

Agreed! This cannot be a relative url

this.setIsReady(requestsFrameUrl);
});
frame.src = requestsFrameUrl; // Set AFTER loadPromise to avoid race
setStyles(frame, {width: 0,height: 0,visibility: 'hidden'});
Copy link
Contributor

Choose a reason for hiding this comment

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

Why not display none vs visibility hidden? This should also likely be position absolute at the top of the page or something similar to ensure it doesn't potentially interact with any elements on the page

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done

});
frame.src = requestsFrameUrl; // Set AFTER loadPromise to avoid race
setStyles(frame, {width: 0,height: 0,visibility: 'hidden'});
this.requestsFrameMap[requestsFrameUrl] = {
Copy link
Contributor

Choose a reason for hiding this comment

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

Given load modifies this object, shouldn't it be initialized prior to load listener?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done

const requestsFrame = this.requestsFrameMap[requestsFrameUrl];
requestsFrame.isReady = true;
this.send_(requestsFrame.frame, requestsFrame.msgQueue);
requestsFrame.msgQueue = [];
Copy link
Contributor

Choose a reason for hiding this comment

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

Why not deallocate completely? delete requestFrame.requestsFrame

Copy link
Contributor Author

Choose a reason for hiding this comment

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

That's fine, since we can (currently) never go from isReady=true to isReady=false. Do you think that will ever be possible? If so, and if we delete msgQueue, then in that case we'd have to reset it to []. I currently can't think of any case where we'd need to do that but figured I'd bring it up.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done

// Warning: the following code is likely only temporary. Don't check
// in before getting resolution on that.
frame && frame.contentWindow &&
frame.contentWindow.postMessage({ampAnalyticsEvents: messages}, '*');
Copy link
Contributor

Choose a reason for hiding this comment

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

I assume all browsers we support will handle passing content as an object instead of string? Is this what we do in window context? Also, I thought we wanted to share the window.context infrastructure?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

As I recall, we decided w/ Brad that that would bring too much baggage / require much effort to expose? Happy to re-open the discussion - as the comment on 816 says, the current impl isn't intended to necessarily be permanent.

@@ -0,0 +1 @@
<!-- No, this is not actually JSON. Fake ad network is kind of broken and should be fixed separately. --><!doctype html><html amp4ads> <head> <meta charset=utf-8> <script async src='https://cdn.ampproject.org/v0.js'></script> <script async custom-element=amp-analytics src='https://cdn.ampproject.org/v0/amp-analytics-0.1.js'></script> <style amp4ads-boilerplate>body{visibility:hidden}</style> <meta name=viewport content=width=device-width,minimum-scale=1> </head> <body> <amp-img id=img src='https://upload.wikimedia.org/wikipedia/commons/6/6e/Golde33443.jpg' width=256 height=300></amp-img> <amp-analytics> <script type='application/json'> { "requests": { "pageview": "viewed=true&url=${canonicalUrl}&title=${title}&acct=${account}", "event": "eid=click&acct=${account}" }, "vars": { "account": "ABC123" }, "triggers": { "defaultPageview": { "on": "visible", "request": "pageview" }, "anchorClicks": { "on": "click", "selector": "img", "request": "event" } }, "transport": { "iframe": "http://localhost:8000/examples/analytics-3p-remote-frame.html", "dataHash": "key1=val1&key2=val2", "beacon": false, "xhrpost": false, "image": false } } </script> </amp-analytics> </body></html>
Copy link
Contributor Author

Choose a reason for hiding this comment

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

@lannka
Copy link
Contributor

lannka commented May 12, 2017

As mentioned in email, before moving further on this PR, I'd like to see the overall plan of all the changes to amp-analytics, and how do you split up PRs and approach to that.

On the other side, again, I'd really like to have the user of this API (like Moat) to agree on the design.

@@ -573,6 +578,25 @@ export class AmpAnalytics extends AMP.BaseElement {
return request;
});
}
/* TODO REMOVE THIS BEFORE CHECKING IN!
Copy link
Contributor Author

Choose a reason for hiding this comment

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

I didn't add this, so suspect it's a conflict from rebasing or some such. Will investigate.

if (window.onNewAmpAnalyticsInstance) {
window.onNewAmpAnalyticsInstance(remoteFrameMgr_);
// Warning: the following code is likely only temporary. Don't check in
// before getting resolution on that.
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Pointing out this comment again since previous one got marked outdated.

@@ -59,15 +59,24 @@ export class AmpAdNetworkFakeImpl extends AmpA4A {
// and the signature is "FAKESIG". This mode is only allowed in
// `localDev` and primarily used for A4A Envelope for testing.
// See DEVELOPING.md for more info.
const creative = this.transformCreativeLocalDev_(deserialized);
Copy link
Contributor Author

@jonkeller jonkeller May 18, 2017

Choose a reason for hiding this comment

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

Ignore changes to this file...will become moot once #9331 merges.

// DO NOT MERGE THIS
// Warning: the scriptSrc URL below is only temporary. Don't merge
// before getting resolution on that.
const frame = createElementWithAttributes(ampDoc, 'iframe', {
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Pointing out this comment again since previous one got marked outdated. In previous thread, I suggested discussing what the actual value of scriptSrc should be.

static sendToCrossDomainIframe_(frame, messages) {
// DO NOT MERGE THIS
// Warning: the following code is likely only temporary. Don't check
// in before getting resolution on that.
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Pointing out this comment again since previous one got marked outdated. Flip side of the one in examples/analytics-3p-remote-frame-helper.js

…e specified in vendors.js, allows iframe response to creative
@jonkeller
Copy link
Contributor Author

@lannka Here is the tracking issue you requested: #9433

class AmpAnalyticsRemoteFrameManager {
constructor() {
/**
* @type {Function}
Copy link
Contributor

Choose a reason for hiding this comment

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

Looks like this function expects a parameter, if so please tighten up the typing (e.g. ?function<!Array> or similar)

window.onNewAmpAnalyticsInstance(remoteFrameMgr_);
// Warning: the following code is likely only temporary. Don't check in
// before getting resolution on that.
window.addEventListener("message", (msg) => {
Copy link
Contributor

Choose a reason for hiding this comment

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

single parameter functions do not require parens here and throughout

*/
const remoteFrameMgr_ = new AmpAnalyticsRemoteFrameManager();

window.requestIdleCallback = window.requestIdleCallback || function(cb) {
Copy link
Contributor

Choose a reason for hiding this comment

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

window.requestIdleCallback = window.requestIdleCallback || cb => setTimeout(cb, 1);

@@ -220,6 +228,19 @@ export class AmpAnalytics extends AMP.BaseElement {
this.analyticsGroup_ =
this.instrumentation_.createAnalyticsGroup(this.element);

if (this.config_['transport'] && this.config_['transport']['iframe']) {
Transport.processCrossDomainIframe(this.getAmpDoc().win.document,
Copy link
Contributor

Choose a reason for hiding this comment

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

Can this just be this.win.document or this.element.ownerDocument? Or is this required to get access to the parent window when the creative is in a friendly frame?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I believe is required to get that access.

*/
static sendRequestUsingCrossDomainIframe(request, transportOptions) {
const frameUrl = Transport.appendHashToUrl_(transportOptions['iframe'],
transportOptions['dataHash']);
Copy link
Contributor

Choose a reason for hiding this comment

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

No need for dataHash anymore, right?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Correct, that is the next step, but not part of the current commit.

ampDoc.body.appendChild(frame);
}
if (processResponse) {
window.addEventListener("message", (msg) => {
Copy link
Contributor

Choose a reason for hiding this comment

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

message should be single quoted. Make sure to run gulp lint and gulp check-types periodically

this.config_['transport'],
(msg) => {
try {
if (this.element.ownerDocument.location.href !== 'about:srcdoc') {
Copy link
Contributor

Choose a reason for hiding this comment

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

This is to allow the creative to communicate with the amp-analytics instance? Surely this code is temporary as otherwise it would be XSS? Add TODO/DO NOT SUBMIT?

inlineConfig.transport.iframe) ||
(this.remoteConfig_ && this.remoteConfig_.transport &&
this.remoteConfig_.transport.iframe))) {
user().error(this.getName_(), 'transport/iframe may only be specified' +
Copy link
Contributor

Choose a reason for hiding this comment

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

You need to actually return here or otherwise prevent this situation. As is, I believe this would allow the code to continue to be executed.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Ah, I thought user().error() throws. Will add return.

@@ -398,6 +419,18 @@ export class AmpAnalytics extends AMP.BaseElement {
}
const typeConfig = this.predefinedConfig_[type] || {};

// transport.iframe is only allowed to be specified in typeConfig, not
// the others. Allowed when running locally for testing purposes.
if (!getMode().localDev &&
Copy link
Contributor

Choose a reason for hiding this comment

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

This feels a bit more readable but up to you:

let hasFrame = false;
[defaultConfig, inlineConfig, this.remoteConfig_].forEach(config => {
if (config && config.transport && config.transport.iframe) {
user().error('invalid config contains iframe transport', config);
// WARN but allow in dev mode.
hasFrame = getMode().localDev;
}
});
if (hasFrame) {
... error and prevent execution
}

const frameData = Transport.crossDomainFrames[frameUrl];
frameData.isReady = true;
this.sendToCrossDomainIframe_(frameData.frame, frameData.msgQueue);
delete frameData.msgQueue;
Copy link
Contributor

Choose a reason for hiding this comment

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

Are you sure you want to delete it? What about grouping/throttling?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yeah, will need to re-add the queue when throttling is implemented in a future phase. Though there is no need for it in the interim.

@jonkeller
Copy link
Contributor Author

FYI this PR is old and crufty. I plan to just delete it and make a new one towards the end of the week.

@jonkeller jonkeller closed this May 24, 2017
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

6 participants