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
A4A amp nameframe #6099
Conversation
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_ = |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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_ = |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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); |
There was a problem hiding this comment.
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).
There was a problem hiding this comment.
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; |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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_ = |
There was a problem hiding this comment.
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...
There was a problem hiding this comment.
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) { |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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.
There was a problem hiding this 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() { |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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/, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
why is this needed?
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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
?
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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 */ |
There was a problem hiding this comment.
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
?
There was a problem hiding this comment.
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) { |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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 = { |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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 => { |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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';
}
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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
.
There was a problem hiding this comment.
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'); |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done.
There was a problem hiding this comment.
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> |
There was a problem hiding this comment.
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 :-)
There was a problem hiding this comment.
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) { |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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 */ |
There was a problem hiding this comment.
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) || |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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.
Thanks for the feedback @lannka . PTAL |
@lannka , @jridgewell PTAL. I've left in an example test configured using |
@tdrl in case you didn't see my previous comment regarding to the test, can you try disable |
@lannka Ah, yes, I had missed that previous comment. Thanks for bringing it up. And thanks for the code pointer! Works nicely now. |
Thanks, @lannka . PTAL |
@@ -0,0 +1,26 @@ | |||
<!DOCTYPE html> |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment.
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"> |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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); |
There was a problem hiding this comment.
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()
There was a problem hiding this comment.
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; |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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
?
There was a problem hiding this comment.
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); |
There was a problem hiding this comment.
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)
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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');
There was a problem hiding this comment.
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) { |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done.
… it to identify the window internally, leading to many string copies and serious garbage allocation if the name field is large and kept around).
@lannka and @taymonbeal PTAL |
* @param {T} s | ||
* @return {boolean} | ||
*/ | ||
export function isEnumValue(enumObj, s) { |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done.
There was a problem hiding this 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.
I'm happy whenever Travis is. |
@taymonbeal 's happiness is also important :-) unable to merge before collecting all the balls. |
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. |
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). |
No worries. Thanks for your review, Taymon! Okay, @lannka -- we're all clear. Press the button! |
Button pressed |
* 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.
* 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.
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 ano-content
message in the case of rendering failure.