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

A4A amp nameframe #6099

Merged
merged 25 commits into from Nov 29, 2016
Merged

A4A amp nameframe #6099

merged 25 commits into from Nov 29, 2016

Conversation

tdrl
Copy link

@tdrl tdrl commented Nov 9, 2016

NameFrame is an AMP alternative to SafeFrame, for use in A4A code. It does not support the full SafeFrame API, but it does allow a creative to be set in a cross-domain iframe via the name attribute. It also supports postMessaging out a no-content message in the case of rendering failure.

@tdrl
Copy link
Author

tdrl commented Nov 9, 2016

I'm still doing manual tests and some cleanup, but I wanted to get it to you to review while I'm doing those steps.

/** @private {?string} */
this.experimentalNonAmpCreativeRenderMethod_ = null;
/** @private {!CROSS_ORIGIN_RENDERING_MODE} */
this.experimentalNonAmpCreativeRenderMethod_ =
Copy link
Contributor

Choose a reason for hiding this comment

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

Given this now includes the default (currently cache, but likely soon to be safe/name frame for ios), let's remove "experimental" from the name

Copy link
Author

Choose a reason for hiding this comment

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

Done.

if (rendered instanceof Error) {
throw rendered;
this.creativeBody_ = null; // Free resources.
this.experimentalNonAmpCreativeRenderMethod_ =
Copy link
Contributor

Choose a reason for hiding this comment

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

Any reason to this here as opposed to unlayoutCallback?

Copy link
Author

Choose a reason for hiding this comment

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

Malte requested it. The idea was to be sure to free the resources as soon as we were sure we're done with them. Since unlayoutCallback isn't guaranteed to be called, he wanted it to happen in place where it would be sure to happen.

this.emitLifecycleEvent('renderSafeFrameStart');
return utf8Decode(creativeBody).then(creative => {
/** @const {!Element} */
const name_data = Object.create(null);
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 just const name_data = {'creative': creative}?

Also a decent amount of duplication between this and safeframe code... you could have a single render via frame loader function that takes just the creative and then uses this.experimentalNonAmpCreativeRenderMethod_ to determine which (or you can pass the value if you're worried about state).

Copy link
Author

Choose a reason for hiding this comment

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

Done.

@@ -567,7 +567,7 @@ export class AmpA4A extends AMP.BaseElement {
this.adPromise_ = null;
this.adUrl_ = null;
this.creativeBody_ = null;
this.experimentalNonAmpCreativeRenderMethod_ = null;
this.nonAmpCreativeRenderMethod_ = null;
Copy link
Contributor

Choose a reason for hiding this comment

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

Should this be the default (client cache)? We should have a test where we create A4A Element, call unlayoutcallback, and verify state has not changed.

Copy link
Author

Choose a reason for hiding this comment

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

Default: Whoa, good catch. I missed that one. Thanks.

Test: Added.

}
this.creativeBody_ = null; // Free resources.
this.experimentalNonAmpCreativeRenderMethod_ =
this.nonAmpCreativeRenderMethod_ =
Copy link
Contributor

Choose a reason for hiding this comment

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

I meant why reset nonAmpCreativeRenderMethod_ here given you also reset in unlayoutCallback? I would remove this since we may want to know later in the flow...

Copy link
Author

Choose a reason for hiding this comment

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

Oh, I see. Yeah, I guess that makes sense. Done.

this.emitLifecycleEvent('renderSafeFrameStart');
return utf8Decode(creativeBody).then(creative => {
let srcPath;
let nameData;
if (method == CROSS_ORIGIN_RENDERING_MODE.SAFEFRAME) {
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 use a switch instead... I would argue its cleaner. You can also move the dev assert into the else (soon to be default) clause as dev().assert(false)... unless that feels ugly to you

Copy link
Author

Choose a reason for hiding this comment

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

Oh, right. The if ... else if is a leftover. Switch makes more sense now.

Re: location of the assert. I think it should be outside. It's dev mode so that we can see it if we manage to get here by mistake, but it will throw out of the code altogether. If we should happen to get here in prod mode, I'd like a gentler return.

Copy link
Contributor

@lannka lannka left a comment

Choose a reason for hiding this comment

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

Looks good in general

<head>
<meta charset="UTF-8">
<script>
window.onload = function() {
Copy link
Contributor

Choose a reason for hiding this comment

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

curious why do we wait for onload?

Copy link
Author

Choose a reason for hiding this comment

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

I wondered that myself! The issue seems to be that if you do a document.write() during load, it only appends to the doc. If you wait until load is completed, it replaces it, which is the desired behavior here.

Copy link
Contributor

Choose a reason for hiding this comment

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

good to know

@@ -504,11 +504,15 @@ function replaceUrls(mode, file) {
file = file.replace('https://cdn.ampproject.org/a4a-v0.max.js', '/dist/amp-inabox.js');
file = file.replace(/https:\/\/cdn.ampproject.org\/v0\//g, '/dist/v0/');
file = file.replace('https://cdn1.ampproject.org/viewer/google/v5.js', 'https://cdn.ampproject.org/viewer/google/v5.js');
file = file.replace(/https:\/\/3p.ampproject.net\/(?:\d+\/)?(.+)\.html/,
Copy link
Contributor

Choose a reason for hiding this comment

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

why is this needed?

Copy link
Author

Choose a reason for hiding this comment

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

In order to serve the nameframe during testing / local dev mode. It wasn't needed for frame.html because its name is initialized differently, I think.

Happy to try other approaches, if you have suggestions. This is the best thing I found so far.

Copy link
Contributor

Choose a reason for hiding this comment

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

I'm wondering which file needed in the test contains this https://3p.ampproject.net/...html ?

Copy link
Author

@tdrl tdrl Nov 13, 2016

Choose a reason for hiding this comment

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

Sorry -- should have been more clear. I meant, when manually loading pages for personal testing, not when running automated tests. If I have a file that looks like,

<body>
<h1>A putative ad.</h1>
<iframe src="https://3p.ampproject.net/12345678/nameframe.html"
        name='{"creative":"<html><head><meta charset=\"UTF-8\"><title>Who puts a title on an ad creative anyway?</title></head><body>This is an ad.</body></html>","context":{"foo":3.7,"bar":"kumquat"}}'></iframe>
</body>

I can load it directly in the browser, when serving locally.

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 is only for local dev purpose, you can just point the src to localhost directly?

Copy link
Author

Choose a reason for hiding this comment

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

Fair. Rewrite rule removed.

/** @private {?string} */
this.experimentalNonAmpCreativeRenderMethod_ = null;

/** @type {!CROSS_ORIGIN_RENDERING_MODE} @visibleForTesting */
Copy link
Contributor

Choose a reason for hiding this comment

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

does this pass gulp check-types?

Copy link
Author

Choose a reason for hiding this comment

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

Amazingly enough, yes. I was worried about that, but it didn't complain. Should it?

* @return {!Promise} awaiting load event for ad frame
* @private
*/
renderViaSafeFrame_(creativeBody) {
renderViaNameAttrOfXDomIframe_(creativeBody) {
Copy link
Contributor

Choose a reason for hiding this comment

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

since you already named it "nameframe", just call it renderViaNameFrame?

Copy link
Author

Choose a reason for hiding this comment

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

Sadly, it doesn't just do NameFrame yet. At the moment, it does both NameFrame and SafeFrame. This name is fugly, but captures both. Happy to change it in the future, when we remove SafeFrame support.

Copy link
Contributor

Choose a reason for hiding this comment

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

Fine. I guess you meant XOrigin not XDom?

Copy link
Author

Choose a reason for hiding this comment

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

I was thinking "Dom" == "Domain", not "DOM". But I agree that Origin is more precise. Changed.

/** @type {string} @visibleForTesting */
export const RENDERING_TYPE_HEADER = 'X-AmpAdRender';

/** @enum {string} */
export const CROSS_ORIGIN_RENDERING_MODE = {
Copy link
Contributor

Choose a reason for hiding this comment

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

nit: how about XORIGIN_MODE

Copy link
Author

Choose a reason for hiding this comment

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

Sure. Done.

let doc;
let iframe;
beforeEach(() => {
return createIframePromise(/* runtimeOff */ true).then(f => {
Copy link
Contributor

Choose a reason for hiding this comment

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

use describes.realWin, so you don't need createIframePromise here

Copy link
Author

Choose a reason for hiding this comment

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

I actually tried that and fell back after fighting with it for 2 hours. The issue is that it seems to preempt document.createElement('iframe') so that I couldn't get a cross-domain iframe out of it. As a result, I couldn't set up a NameFrame in it. :-P Quite possible I was just doing it wrong, though. Happy to revisit if you have guidance / code pointers.

Copy link
Contributor

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 problem. But would the following work?

describes.realWin('nameframe', {}, env => {
  let fixture;
  beforeEach(() => {
     fixture = env;
     win = fixture.win;
     doc = fixture.doc;
     iframe = doc.createElement('iframe');
     iframe.src = 'http://localhost:9876/dist.3p/current/nameframe.max.html';
  }

Copy link
Author

Choose a reason for hiding this comment

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

No, it doesn't. This is pretty much what I tried before. When I do this, the iframe that is created has srcdoc set to <h1>Fake iframe</h1>, which makes it some form of a friendly iframe and means that it doesn't load the src and doesn't work as a nameframe.

Including an example test of this format in the current push, so you can double-check and tell me if I'm missing something.

Copy link
Contributor

Choose a reason for hiding this comment

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

ah, it's caused by this.

you'll want to introduce a new field loadExternalResources in the spec (default to false) to disable doNotLoadExternalResourcesInTest.

Copy link
Author

Choose a reason for hiding this comment

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

Fixed. Thanks for the pointer!

win = fixture.win;
doc = fixture.doc;
iframe = doc.createElement('iframe');
iframe.src = getDefaultBootstrapBaseUrl(win, 'nameframe');
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 just hardcode the URL here. no need for this test to depend on 3p-iframe.js, which is covered with a separate unit test.

Copy link
Author

Choose a reason for hiding this comment

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

Done.

Copy link
Author

Choose a reason for hiding this comment

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

Though I do worry that hardcoding in the port is fragile to changes to the test rig. The benefit to doing it via getDefaultBootstrapBaseUrl is that it's then that function's job to worry about things like port number.

it('should load remote nameframe and succeed with valid JSON input', done => {
iframe.name = JSON.stringify({
creative: `<html>
<body>
Copy link
Contributor

Choose a reason for hiding this comment

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

to make it prettier, let's have some harmless indentation :-)

Copy link
Author

Choose a reason for hiding this comment

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

:-) Fair enough. Done.

import {getDefaultBootstrapBaseUrl} from '../../src/3p-frame';
import {createIframePromise} from '../../testing/iframe';

function expectNoContent(win, done) {
Copy link
Contributor

Choose a reason for hiding this comment

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

move helper method to the bottom of the file.

Copy link
Author

Choose a reason for hiding this comment

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

Done.

/** @private {?string} */
this.experimentalNonAmpCreativeRenderMethod_ = null;

/** @type {!CROSS_ORIGIN_RENDERING_MODE} @visibleForTesting */
Copy link
Contributor

Choose a reason for hiding this comment

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

Newline for @visibleForTesting

this.experimentalNonAmpCreativeRenderMethod_ =
fetchResponse.headers.get(RENDERING_TYPE_HEADER);
this.nonAmpCreativeRenderMethod_ =
fetchResponse.headers.get(RENDERING_TYPE_HEADER) ||
Copy link
Contributor

Choose a reason for hiding this comment

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

Do we need to assert that this is a valid enum value?

Copy link
Author

Choose a reason for hiding this comment

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

Can't just assert because I don't want to fail the entire method. Even if it gets an invalid value here, it will fall back to something that will work during rendering. But I agree that a dev notification is in order. (Dev because the value of this header is in the control of the ad network, not the publisher.) Added, along with a test for this case.

@tdrl
Copy link
Author

tdrl commented Nov 9, 2016

Thanks for the feedback @lannka . PTAL

@tdrl
Copy link
Author

tdrl commented Nov 15, 2016

@lannka , @jridgewell PTAL. I've left in an example test configured using describes.realWin as an example of how it breaks -- that's currently failing tests, but everything else should be addressed.

@lannka
Copy link
Contributor

lannka commented Nov 15, 2016

@tdrl in case you didn't see my previous comment regarding to the test, can you try disable doNotLoadExternalResourcesInTest?

@tdrl
Copy link
Author

tdrl commented Nov 17, 2016

@lannka Ah, yes, I had missed that previous comment. Thanks for bringing it up. And thanks for the code pointer! Works nicely now.

@tdrl
Copy link
Author

tdrl commented Nov 17, 2016

Thanks, @lannka . PTAL

@@ -0,0 +1,26 @@
<!DOCTYPE html>
Copy link
Member

Choose a reason for hiding this comment

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

Style nit: Needs to be lowercase. Same with UTF-8 below.

Copy link
Member

Choose a reason for hiding this comment

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

Copy link
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 @@
<!DOCTYPE html>
<html lang="en">
Copy link
Member

Choose a reason for hiding this comment

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

Semantic nit: Except for the developer-facing error message, which by ancient convention doesn't count, this file contains no English-language content and so a lang attribute is not appropriate.

Copy link
Author

Choose a reason for hiding this comment

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

Done.

@@ -230,68 +287,181 @@ describe('amp-a4a', () => {
}).onFirstCall().returns(Promise.resolve(mockResponse));
return createAdTestingIframePromise().then(fixture => {
const doc = fixture.doc;
const a4aElement = createA4aElement(doc);
a4aElement = createA4aElement(doc);
a4aElement.setAttribute('width', 200);
Copy link
Contributor

Choose a reason for hiding this comment

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

does it make sense to move those repeated setAttribute into createA4aElement? Also, use the helper method createElementWithAttributes()

Copy link
Author

Choose a reason for hiding this comment

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

Done. Also got rid of a number of body.appendChild because it's done in createA4AElement.

});

afterEach(() => {
expect(xhrMock).to.be.calledOnce;
Copy link
Contributor

Choose a reason for hiding this comment

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

it's not recommended to do a check in fixture.

you can just check it in one test case, no need to check the same thing in every test case

Copy link
Author

Choose a reason for hiding this comment

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

If I expect it to hold in all test cases, why not just add the code once, rather than everywhere? Regardless, I can move it. Does failing in one test case break all test cases that share the same afterEach?

Copy link
Author

Choose a reason for hiding this comment

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

Done.

@@ -598,10 +785,15 @@ describe('amp-a4a', () => {
a4a.buildCallback();
a4a.preconnectCallback(false);
const preconnects = doc.querySelectorAll('link[rel=preconnect]');
expect(preconnects.length).to.equal(2);
expect(preconnects.length).to.equal(3);
Copy link
Contributor

Choose a reason for hiding this comment

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

expect(array).to.have.length(3)

Copy link
Author

@tdrl tdrl Nov 28, 2016

Choose a reason for hiding this comment

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

Done. Replaced here and elsewhere.

When I looked it up on Chai, it appears that that use of length is deprecated and they suggest lengthOf instead: http://chaijs.com/api/bdd/#method_length

expect(preconnects[1].getAttribute('href')).to
.have.string('http://ads.localhost');
// AdSense origin.
expect(preconnects[2].getAttribute('href')).to
Copy link
Contributor

Choose a reason for hiding this comment

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

expect(x).to.have.property('key', 'value');

Copy link
Author

Choose a reason for hiding this comment

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

Done.

dev().assertEnumValue(XORIGIN_MODE,
this.nonAmpCreativeRenderMethod_,
'cross-origin render mode header');
} catch (err) {
Copy link
Contributor

Choose a reason for hiding this comment

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

maybe just introduce a util function isValidEnumValue() in types.js

Copy link
Author

Choose a reason for hiding this comment

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

Done.

@tdrl
Copy link
Author

tdrl commented Nov 28, 2016

@lannka and @taymonbeal PTAL

* @param {T} s
* @return {boolean}
*/
export function isEnumValue(enumObj, s) {
Copy link
Contributor

Choose a reason for hiding this comment

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

Please update assertEnumValue to use this helper.

Copy link
Author

Choose a reason for hiding this comment

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

Done.

Copy link
Contributor

@lannka lannka left a comment

Choose a reason for hiding this comment

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

LGTM. let me know when it's ready for merge.

@tdrl
Copy link
Author

tdrl commented Nov 29, 2016

I'm happy whenever Travis is.

@lannka
Copy link
Contributor

lannka commented Nov 29, 2016

@taymonbeal 's happiness is also important :-) unable to merge before collecting all the balls.

@tdrl
Copy link
Author

tdrl commented Nov 29, 2016

Yup. I think I've addressed all his comments (though he may come up with more). Gave him the heads-up to check it out again this morning.

@taymonbeal
Copy link
Member

LGTM. Sorry for delays in reviewing this, it got lost in the GitHub noise after I started getting notified on every PR in amphtml (I've since turned this off).

@tdrl
Copy link
Author

tdrl commented Nov 29, 2016

No worries. Thanks for your review, Taymon!

Okay, @lannka -- we're all clear. Press the button!

@lannka lannka merged commit b21e457 into ampproject:master Nov 29, 2016
@lannka
Copy link
Contributor

lannka commented Nov 29, 2016

Button pressed

@tdrl tdrl deleted the a4a-amp-safeframe branch November 29, 2016 21:25
Lith pushed a commit to Lith/amphtml that referenced this pull request Dec 22, 2016
* First draft of nameframe.  Still needs tests and some gulp config.

* Added server rewrite rules for 3p .html files.

* Started test rig for nameframe.

* Changes to test rig, but still not functional.

* Tests for nameframe itself in place.

* Tests for nameframe itself in place.

* Update gulp server rules to handle (optional) version number in nameframe URL.

* A4A promise chain changes in place.

* Moar tests.

* Lint fixes.

* Changes from reviews.

* Changes from reviews.

* Changes from reviews.

* Don't overwrite the rendering method if no header is present.

* Added preconnect.

* Updates in response to reviews.

* Updates in response to reviews.

* Rebase fixes.

* Fixes to tests.  Clear window.name in nameframe (b/c some browsers use it to identify the window internally, leading to many string copies and serious garbage allocation if the name field is large and kept around).

* Remove rewrite rule from server.js.

* Updates in response to reviews.

* Updates in response to reviews.

* Rebase against master and resolve conflicts.

* Response to reviews.

* Response to reviews.
Lith pushed a commit to Lith/amphtml that referenced this pull request Dec 22, 2016
* First draft of nameframe.  Still needs tests and some gulp config.

* Added server rewrite rules for 3p .html files.

* Started test rig for nameframe.

* Changes to test rig, but still not functional.

* Tests for nameframe itself in place.

* Tests for nameframe itself in place.

* Update gulp server rules to handle (optional) version number in nameframe URL.

* A4A promise chain changes in place.

* Moar tests.

* Lint fixes.

* Changes from reviews.

* Changes from reviews.

* Changes from reviews.

* Don't overwrite the rendering method if no header is present.

* Added preconnect.

* Updates in response to reviews.

* Updates in response to reviews.

* Rebase fixes.

* Fixes to tests.  Clear window.name in nameframe (b/c some browsers use it to identify the window internally, leading to many string copies and serious garbage allocation if the name field is large and kept around).

* Remove rewrite rule from server.js.

* Updates in response to reviews.

* Updates in response to reviews.

* Rebase against master and resolve conflicts.

* Response to reviews.

* Response to reviews.
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.

None yet

5 participants