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 Ad Refresh #9535

Merged
merged 92 commits into from Jul 18, 2017
Merged

AMP Ad Refresh #9535

merged 92 commits into from Jul 18, 2017

Conversation

glevitzky
Copy link
Contributor

@glevitzky glevitzky commented May 24, 2017

Implements amp-ad refresh feature.

What's not included:

  • Logic around amp-sticky-ad. I'd prefer to leave this for a future PR.

Closes #4038

this.refreshManager_ = getRefreshManagerFor(this.win);

this.refreshManager_.registerElement(this.element, refresher => {
console.log('Eureka!');
Copy link
Member

Choose a reason for hiding this comment

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

I assume all console.log statements aren't supposed to be here.

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. I'll be sure to remove them once I make more progress.

@@ -345,6 +347,45 @@ export class AmpA4A extends AMP.BaseElement {

/** @private {string} */
this.safeframeVersion_ = DEFAULT_SAFEFRAME_VERSION;

/** @private @const {RefreshManager} */
Copy link
Member

Choose a reason for hiding this comment

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

!RefreshManager

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.

* share the same minOnScreenPixelRatioThreshold will be monitored by the
* same IO.
*
* @private {!Object<string, (IntersectionObserver|!IntersectionObserverPolyfill)>}
Copy link
Member

Choose a reason for hiding this comment

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

!IntersectionObserver

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.

* monitored, indexed by a unique ID stored as data-amp-a4a-refresh-id on
* the element.
*
* @private {!Object<string, RegisteredElementWrapper>}
Copy link
Member

Choose a reason for hiding this comment

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

!RegisteredElementWrapper

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,470 @@
/**
* Copyright 2016 The AMP HTML Authors. All Rights Reserved.
Copy link
Member

Choose a reason for hiding this comment

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

2017

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.


/**
* @param {!Element} element The element to be wrapped.
* @param {!function()} callback The function to be invoked when the element
Copy link
Member

Choose a reason for hiding this comment

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

function()

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.

/**
* The function that will be invoked when the refreshInterval has expired.
*
* @private @const {!function()}
Copy link
Member

Choose a reason for hiding this comment

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

function()

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.

/**
* Invokes the callback function.
*
* @param {RefreshManager} refreshManager The calling refreshManager, passed
Copy link
Member

Choose a reason for hiding this comment

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

!RefreshManager

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,117 @@
/**
* Copyright 2015 The AMP HTML Authors. All Rights Reserved.
Copy link
Member

Choose a reason for hiding this comment

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

2017

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.

});
refreshManager.registerElement(
testElement, () => {
expect(false).to.be.true;
Copy link
Member

Choose a reason for hiding this comment

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

You can use fail here. Also, how can resolver ever run? What's the path of execution through this test?

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 wouldn't put too much stock in the tests I have at the moment, as they're most likely broken at this point. This particular one, I don't think, was ever finished, but I'll keep your point in mind.

@taymonbeal
Copy link
Member

Also, please add Markdown docs for page authors.

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.

Note that doubleclick requires at least one new parameter on refresh ad requests: rc which is the number of refresh requests.

* @param {(string|number)} threshold
* @return {(!IntersectionObserver|!IntersectionObserverPolyfill)}
*/
getIntersectionObserverWithThreshold_(threshold) {
Copy link
Contributor

Choose a reason for hiding this comment

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

Implementations for the thresholds needed (percentage within viewport for given duration) already exist within AMP analytics so let's re-use it (https://github.com/ampproject/amphtml/blob/master/ads/google/a4a/performance.js#L278).

this.refreshManager_.registerElement(this.element, refresher => {
console.log('Eureka!');
this.isRefreshing_ = true;
this.unlayoutCallback();
Copy link
Contributor

Choose a reason for hiding this comment

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

Agreed and doesn't unlayoutCallback remove the frame causing the existing creative to be destroyed? The idea is to leave the existing creative in place until we have retrieved the new creative so instead of calling unlayoutCallback, you should just create a new instance of the adPromise to "restart" ad retrieval and then on response you can clear or instead have the creative render code detect its in the refresh state and clear the frame just prior to creating it.

this.unlayoutCallback();
this.onLayoutMeasure();
this.adPromise_.then(() => {
if (!this.isRefreshing_) {
Copy link
Contributor

Choose a reason for hiding this comment

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

When would this occur? Presumably only if unlayoutCallback was executed? If so, we should expect the promise chain to have been cancelled, 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.

When would what occur, specifically? If you mean this.isRefreshing_ being set to false, then this could happen if we attempt to collapse the slot in onLayoutMeasure.

@@ -345,6 +347,44 @@ export class AmpA4A extends AMP.BaseElement {

/** @private {string} */
this.safeframeVersion_ = DEFAULT_SAFEFRAME_VERSION;

/** @private @const {!./refresh-manager.RefreshManager} */
this.refreshManager_ = refreshManagerFor(this.win);
Copy link
Contributor

Choose a reason for hiding this comment

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

Negative here is that networks that do not require refresh will include its code in their binary unnecessarily. We should consider how to avoid this long term but given that only Doubleclick wants to support this currently, perhaps we should consider how to make it directly a feature in its impl.

/** @private @const {!./refresh-manager.RefreshManager} */
this.refreshManager_ = refreshManagerFor(this.win);

this.refreshManager_.registerElement(this.element, refresher => {
Copy link
Contributor

Choose a reason for hiding this comment

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

Item missing here is building the config that indicates if/how element is configured for refresh. Will this be page level? block level? both?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

It will be both. As mentioned in the PR description, this isn't implemented yet.

@@ -152,6 +156,39 @@ export class AmpAdNetworkDoubleclickImpl extends AmpA4A {
]);
}

initializeRefreshIfEligible() {
Copy link
Contributor

Choose a reason for hiding this comment

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

Doc and any reason this can't be private?

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.

}
new RefreshManager(this.win, this.element, refresher => {
this.isRefreshing = true;
this.tearDownSlot();
Copy link
Contributor

Choose a reason for hiding this comment

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

Seems like this logic should be within AmpA4A. I realize it means we'll bloat for networks that do not support refresh but it seems to me to be a cleaner way to ensure proper integration (no need to expose getAdPromise for instance). Assume that refreshmanager will call a function on the A4A instance (e.g. refresh())). Advantage is that at least refresh manager is not within AmpA4A and so it won't be compiled into networks that don't need it. Also, make getRefreshConfiguration part of RefreshManager.

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 (!this.isRefreshing_) {
// If this refresh cycle was canceled, such as in a no-content
// response case, keep showing the old creative.
refresher.initiateRefreshCycle();
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Will fix this in next pass.

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.

Do CSI pings appropriately distinguish between refresh and non-refresh requests? Probably good to have.

/** @type {!../extensions/amp-a4a/0.1/refresh-manager.RefreshConfig} */
export const DEFAULT_REFRESH_CONFIG = {
visiblePercentageMin: 50,
totalTimeMin: 0,
Copy link
Contributor

Choose a reason for hiding this comment

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

What is totalTimeMin for? I don't see you reference it anywhere.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Total time the ad must be on screen to be considered "seen". This is the same parameter that amp-analytics' visibility takes.


/**
* @typedef {{
* visiblePercentageMin: number,
Copy link
Contributor

Choose a reason for hiding this comment

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

Can you add documentation explaining what these mean? Also would be good if time values indicated milliseconds

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. All times are in ms.

@@ -741,6 +762,34 @@ export class AmpA4A extends AMP.BaseElement {
}

/**
* Refreshes ad slot by fetching new creative and rendering it.
Copy link
Contributor

Choose a reason for hiding this comment

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

Mention that it leaves the remaining creative in place until the new ad promise chain resolves

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.

*/
refresh() {
this.isRefreshing_ = true;
this.tearDownSlot();
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 handle multi-size and no-fill collapse cases:
tearDown will attempt to revert the slot size to its original which could fail if the slot is within the viewport. If its collapsed it just means the ad request will never get sent (as there is code that detects if the slot has no height/width). But if multi-size I assume it would fail because the sizes given could be greater than the slot? We can handle these cases in one of two ways:

  1. Delay the refresh attempt until after the slot could be resized (would mean re-attempting resize once we know it could be successful when the slot is not in the viewport)
  2. For multi-size case, we could just remove sizes that are no longer eligible.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Option 2 is what we initially planned on, so that's what I'm going to implement.

this.tearDownSlot();
this.initiateAdRequest();
this.adPromise_.then(() => {
if (!this.isRefreshing_) {
Copy link
Contributor

Choose a reason for hiding this comment

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

Check promiseId indicating cancellation?

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.isRefreshing_ is true.
* @protected
*/
destroyFrame(force = false) {
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 to set default value as false as undefined is essentially equivalent here.

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.iframe.parentElement.removeChild(this.iframe);
this.iframe = null;
}
this.destroyFrame();
Copy link
Contributor

Choose a reason for hiding this comment

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

shouldn't this be this.destroyFrame(true) to ensure the frame is destroyed on unlayoutcallback?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Depends on what we want to do in the event of unlayoutCallback firing while the slot is being refreshed. As long as !isRefreshing_, then the frame will be destroyed, but if unlayoutCallback executes while isRefreshing_, then the frame will remain.

Copy link
Contributor

Choose a reason for hiding this comment

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

Yeah this is somewhat complex due to the fact that we only want to destroy the frame if its not AMP (as we do not want to allow JS to execute on pages no longer visible to the user). So for AMP creatives when unlayoutCallback occurs, we should leave the frame in place and cancel any pending refresh promises (which I believe would happen anyway given we increment the promiseId though you need to add the check as I mentioned in a previous comment). However, we want to ensure that if the user swipes back to the page, the refresh interval either continues or restarts depending on production definition.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

We took deliberate steps to avoid keeping a reference to the RefreshManager in AmpA4A, so it may be difficult to cancel/restart refresh on demand.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Regarding the user swiping back, the refresh cycle will stop on its own until the user sees the new creative anyway. So the worst case is that we refresh a number of slots on a page, which the user will not see unless he swipes back to the original page, at most once per refresh-enabled slot.


// Will initiate the refresh lifecycle iff the slot has been enabled to do
// so through an appropriate data attribute, or a page-level meta tag.
new RefreshManager(this).initiateRefreshCycle();
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 wait until after layout otherwise its possible refresh interval would include time to fetch and render first ad slot?

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.

}
analyticsForDoc(this.element_, true).then(analytics => {
analytics.getAnalyticsRoot(this.element_).getVisibilityManager()
.listenElement(this.element_, this.config_, null, null, () => {
Copy link
Contributor

Choose a reason for hiding this comment

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

Will this fire multiple times allowing for continued refresh? Or do you need to re-initialize after every successful render?

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 passes a callback to a4a.refresh() that when invoked restarts the refresh cycle.

return false;
}
let refreshEnabled =
this.element_.getAttribute('data-enable-refresh') == 'true';
Copy link
Contributor

Choose a reason for hiding this comment

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

Let's assume that the value will be a number indicating interval and you take the Math.max(value, 30sec) so this function can return the interval. Something like the following which gives preference to attribute and falls back to meta tag if present.

if (!this.config) {
return null;
}
let metaTag;
return Math.max(30, Number(this.element_.getAttribute('data-enable-refresh')) || ((metaTag = this.win_.document.getElementsByName(amp-ad-enable-refresh:${this.adType_}) ? 0 : Number(metaTag.getAttribute('content')));

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've changed the spec so that enabling a slot is done by setting data-enable-refresh=true. But I see your point in that this is ugly, so I tried doing something similar to your suggestion.

@@ -0,0 +1,65 @@
<!---
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Not sure whether this is the appropriate location for this information; will move upon reviewer's suggestion.

Copy link
Contributor

Choose a reason for hiding this comment

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

Let's move this to amp-ad-network-doubleclick-impl md. We should probably also indicate that this is still in beta, meaning we don't yet verify its correctness.

@@ -84,3 +84,21 @@ export const signingServerURLs = {
'cloudflare': 'https://amp.cloudflare.com/amp-ad-verifying-keyset.json',
'cloudflare-dev': 'https://amp.cloudflare.com/amp-ad-verifying-keyset-dev.json',
};

/** @type {!../extensions/amp-a4a/0.1/refresh-manager.RefreshConfig} */
const DEFAULT_REFRESH_CONFIG = {
Copy link
Contributor

Choose a reason for hiding this comment

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

Why do we need this here? Why don't we just pass the config into RefreshManager when its created within amp-ad-network-doubleclick-impl?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

RefreshManager pulls in the config on its own. The DEFAULT_REFRESH_CONFIG object is there for convenience, in case other networks want to use the same sort of configuration DoubleClick is using. I can take it or leave it, though.

Copy link
Contributor

Choose a reason for hiding this comment

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

Let's remove it, I don't see much value in having it here and this file is included in amp-ad

@@ -54,9 +54,10 @@ const AmpAdImplementation = {

/** @const {!Object} */
export const ValidAdContainerTypes = {
'AMP-STICKY-AD': 'sa',
'AMP-CAROUSEL': 'ac',
Copy link
Contributor

Choose a reason for hiding this comment

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

Did you intend for these and the multi-size changes to be included in this PR?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

At this point, they're kind of entangled, and the changes are isolated such that it should (hopefully) not be too burdensome on the reviewer. If you disagree strongly, I can spend some time splitting those off into a separate PR, though.

// show the loader for a quarter of a second before switching to
// the new creative.
timerFor(this.win).delay(() => {
this.attemptToRenderCreative().then(() => {
Copy link
Contributor

Choose a reason for hiding this comment

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

I believe this will need to be wrapped in a vsync mutate given you're modifying outside of layoutCallback. This brings me to another point which is that we shouldn't actually render if the slot is outside of the renderOutsideViewport value otherwise we'll allow JS to execute unnecessarily and hurt the pub's active view rate. This makes me think that we should re-evaluate if we could trigger the lifecycle to re-initate layoutCallback and destroy the previous frame/load the new one at that time.

@glevitzky
Copy link
Contributor Author

These test failures are really fun. I get a different set of failing tests depending on whether I run the files (1) locally and independently, (2) locally and all together, and (3) in Travis.

* @param {function()} refreshEndCallback When called, this function will
* restart the refresh cycle.
*/
refresh(refreshEndCallback) {
Copy link
Contributor

Choose a reason for hiding this comment

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

this file was 1.6k lines, and this PR adds 100+ more. I was about to propose a refactoring of this file.

Can we start from this PR? separate new logic out as a class or so?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Keith and I already went back and forth on this quite a bit. I think this function, at minimum, needs to be in this file, or else we'll be forced to expose various private fields that we'd prefer not to.

@@ -84,3 +84,21 @@ export const signingServerURLs = {
'cloudflare': 'https://amp.cloudflare.com/amp-ad-verifying-keyset.json',
'cloudflare-dev': 'https://amp.cloudflare.com/amp-ad-verifying-keyset-dev.json',
};

/** @type {!../extensions/amp-a4a/0.1/refresh-manager.RefreshConfig} */
const DEFAULT_REFRESH_CONFIG = {
Copy link
Contributor

Choose a reason for hiding this comment

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

Let's remove it, I don't see much value in having it here and this file is included in amp-ad

*
* @type {!Object<string, !../extensions/amp-a4a/0.1/refresh-manager.RefreshConfig>}
*/
export const refreshConfigs = {
Copy link
Contributor

Choose a reason for hiding this comment

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

Ditto, this should be removed

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Maybe this is irrelevant given that there's only one network that supports refresh at the moment, but where would other networks put their refresh configurations (which determine viewability criteria)?

if (strict) {
return null;
}
continue;
}

// Check that if multi-size-validation is on, that the secondary sizes
Copy link
Contributor

Choose a reason for hiding this comment

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

FWIW without multiSizeValidation enabled, we don't verify that the size if > 0. Perhaps GPT has special values that could be <= 0?

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 (widthCond(width) && heightCond(height)) {
const isWidthIllegal = widthCond(width);
const isHeightIllegal = heightCond(height);
if (isWidthIllegal && isHeightIllegal) {
Copy link
Contributor

Choose a reason for hiding this comment

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

This seems overally verbose. I would instead allow badParams to be an array and errorBuilder should handle it as such:

if (widthCond(width)) { badParams.push({...}); }
if (heightCond(height)) { badParams.push({...}); }
user().assert(!reportParam || !badParams.length, errorBuilder(badParams));
return !badParams.length;

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Great suggestion, thanks! Done.

@@ -36,9 +36,11 @@ import {isArray, isObject, isEnumValue} from '../../../src/types';
import {some} from '../../../src/utils/promise';
import {utf8Decode} from '../../../src/utils/bytes';
import {viewerForDoc} from '../../../src/services';
import {timerFor} from '../../../src/services';
Copy link
Contributor

Choose a reason for hiding this comment

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

We can combine all of these into a single import. Something like:

import {cryptoFo, resourcesForDoc, timerFor, viewerFor} from....

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. Unless you actually want all/most of the imports inline.


/**
* Indicates whether the ad is currently in the process of being refreshed.
*
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 wouldn't put an extra line here and in other fields

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.

* restart the refresh cycle.
*/
refresh(refreshEndCallback) {
this.isRefreshing = true;
Copy link
Contributor

Choose a reason for hiding this comment

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

Add an assert before this: dev().assert(!this.isRefreshing)

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.togglePlaceholder(true);
this.destroyFrame(true);
// We don't want the next creative to appear too suddenly, so we
// show the loader for a quarter of a second before switching to
Copy link
Contributor

Choose a reason for hiding this comment

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

I still think a quarter of a second is short... most people can't really perceive that other than a flash. I suggest this be 1sec

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.

refreshEndCallback();
return;
}
this.togglePlaceholder(true);
Copy link
Contributor

Choose a reason for hiding this comment

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

These should be within layoutCallback to ensure you are not mutating the DOM outside of a vsync. This includes everything within the timer delay (just make sure layoutCallback returns the results of the timerFor promise. This way you shouldn't need refresh ready promise as its all contained within layoutCallback.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Unfortunately, I need to call refreshEndCallback(), which is passed to refresh() from the RefreshManager, after rendering is complete to restart the refresh cycle. I can move all of the DOM mutating stuff to layoutCallback, but I'll still need refreshReadyPromise_ so that I can signal within refresh() when to call refreshEndCallback(). Also, timerFor(this.win).delay() returns a cancellation id, not a promise; not sure how much that matters.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Also, it appears that forcing the scheduler to run earlier removes the ad loader, so moving everything into layoutCallback has the effect of essentially never showing the loader, just one ad quickly transitioning to another.

This happens with my code as:

  /** @override */
  layoutCallback() {
    if (this.isRefreshing) {
      this.togglePlaceholder(true);
      this.destroyFrame(true);
    }
    return this.attemptToRenderCreative().then(result => {
      if (this.isRefreshing) {
        // We don't want the next creative to appear too suddenly, so we
        // show the loader for a quarter of a second before switching to
        // the new creative.
        timerFor(this.win).delay(() => {
          this.togglePlaceholder(false);
          this.refreshReadyPromiseResolver_(result);
        }, 1000);
      }
      return result;
    });
  }

At some point, very quickly, after displaying the loader, the runtime removes it before this.togglePlaceholder(false) is called. Quick enough that you can't see it.

(containerTypeSet[ValidAdContainerTypes['AMP-FX-FLYING-CARPET']]
|| containerTypeSet[ValidAdContainerTypes['AMP-STICKY-AD']])
(enclosingContainers.indexOf(
ValidAdContainerTypes['AMP-FX-FLYING-CARPET']) != -1
Copy link
Contributor

Choose a reason for hiding this comment

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

can you use includes:

const pfx = enclosingContainers.includes(ValidAdContainerTypes['AMP-FX-FLYING-CARPET']) || enclosingContainers.includes(ValidAdContainerTypes['AMP-STICKY-AD']);

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.

* @return {!Array<Array<number>>} An array of dimensions.
* @param {boolean} strict If set to true, this indicates that a single
* malformed size should cause the entire multi-size data string to be
* abnadoned. If set to false, then malformed sizes will be ignored, and the
Copy link
Contributor

Choose a reason for hiding this comment

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

abnadoned -> abandoned

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.

* abnadoned. If set to false, then malformed sizes will be ignored, and the
* remainder of the string will be parsed for any additional sizes.
* Additionally, errors will only be reported if this flag is set to true.
* @return {?Array<Array<number>>} An array of dimensions.
Copy link
Contributor

Choose a reason for hiding this comment

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

{?Array<!Array<number>>}

Copy link
Contributor Author

Choose a reason for hiding this comment

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

It's possible for this function to return an empty array.

return;
w => isNaN(w) || w <= 0,
h => isNaN(h) || h <= 0,
badParams => badParams.map(badParam =>
Copy link
Contributor

Choose a reason for hiding this comment

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

Seems like you do a similar operation multiple times where you do a badParams mapping and then return or continue based on strict. Consider creating a helper function to save cost & paste

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Every call to validateDimensions has slightly varying arguments, so I'm not sure how to take advantage of a helper function here. I'm open to suggestions, though.

this.isRefreshing = false;

/** @private {boolean} */
this.isRelayoutNeeded_ = false;
Copy link
Contributor

Choose a reason for hiding this comment

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

Could consider making refreshing a state variable and combine these.

metaTagContent ? metaTagContent.split(',') : [];
for (let i = 0; i < networkIntervalPairs.length; i++) {
const pair = networkIntervalPairs[i].split('=');
if (pair.length != 2) {
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:

user().assert(pair.length == 2, 'refresh metadata...');
if (pair[0] == this.adType_) {
return ...
}

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 (isNaN(refreshInterval) || refreshInterval == '') {
user().warn(TAG, 'refresh interval must be a number');
return null;
} else if (refreshInterval < MIN_REFRESH_INTERVAL) {
Copy link
Contributor

Choose a reason for hiding this comment

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

I don't believe you are properly handling a string based interval here. Perhaps it should be:
const refreshIntervalNum = Number(refreshInterval);
dev().assert(!isNaN(refreshInterval) && refreshInterval >= MIN_REFRESH_INTERVAL, 'invalid refresh interval, must be at least ${MIN_REFRESH_INTERVAL}s: ${refreshInterval}`);

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.

totalTimeMin: 0,
continuousTimeMin: 1,
});
if (this.refreshManager_.isRefreshable()) {
Copy link
Contributor

Choose a reason for hiding this comment

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

should refreshManager constructor just initiate the refresh cycle? Then you could minimize this to:

this.refreshManager_ = this.refreshManager_ || new RefreshManager(...);

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.

* @param {function()} refreshEndCallback
*
* @override
*/
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 just be /** @override */

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.

* @return {!Promise} awaiting render complete promise
*/
init(iframe, opt_isA4A) {
init(iframe, opt_isA4A, opt_isRefreshing) {
Copy link
Contributor

Choose a reason for hiding this comment

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

Let's discuss offline but ideally we wouldn't polluate xorigin handler for this case

@rsimha
Copy link
Contributor

rsimha commented Jun 21, 2017

The Percy failure was due to #10022, which has now been fixed.

* the refresh function complete. This is particularly handy for testing.
*/
refresh(refreshEndCallback) {
return new Promise(resolve => {
Copy link
Contributor

Choose a reason for hiding this comment

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

Why do you need a new Promise here? You should just be able to have each of the promise chains return

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'd have to wrap the timerFor block in a new Promise at the least. I can do that if you find it preferable.

refreshEndCallback();
return;
}
this.getVsync().mutate(() => {
Copy link
Contributor

Choose a reason for hiding this comment

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

I think this should be:
return this.mutateElement(() => {
this.togglePlaceholder(true);
return timerFor(this.win)....
});

Note that this delays the timer execution until we are able to mutate. I believe this is the correct behavior but I thought we had discussed relying on priority to impose the delay? Essentially the flow would be:

  • in refresh initiate ad request
  • after ad request, if valid creative (which I'm not sure you're asserting) toggle placeholder and schedule for layout
  • ensure that priority is not update to 0 even if an AMP creative (this will ensure 1 second delay is imposed)
  • layoutCallback can then execute per usual

Copy link
Contributor Author

@glevitzky glevitzky Jun 23, 2017

Choose a reason for hiding this comment

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

We discussed both options, changing priority and using timer. It seems to me that messing with the priority to get a 1 second delay seems like roundabout way of doing it when we can just use a timer.

after ad request, if valid creative (which I'm not sure you're asserting)

If the creative would force a collapse, then isRefreshing would be set to false, short circuiting the function early. Also, as previously mentioned, timerFor(...).delay() returns a string cancellation id, not a promise.

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 timerFor(...).promise(1000) which will return a promise allowing for easier integration

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, sorry. Didn't know that function existed.

refreshEndCallback();
return;
}
return this.getVsync().mutate(() => {
Copy link
Contributor

Choose a reason for hiding this comment

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

I still think this should be this.mutateElement(() => {...

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.

sandbox.restore();
});

it('should effectively reset the slot and invoke given callback', () => {
Copy link
Contributor

Choose a reason for hiding this comment

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

Can we add a couple more tests:

  • Verify that if ad promise does not fill, we do nothing (no placeholder, no destroy frame, etc)
  • Verify that unlayoutCallback called during refresh works correctly
  • Verify that unlayoutCallback followed by resume works correctly
  • Verify refresh does not increment ifi as part of the request and include refresh count

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Working on bullet points 1, 2, and 4 (this will need to be part of doubleclick impl's tests). Not sure what you mean for the third one, especially by resume. Can you elaborate?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

These tests are rather painful to write given how much they rely on the lifecycle methods. Before I continue, could you verify that the rest of the PR looks good, so that I can be sure the tests I'm writing make sense?

@@ -0,0 +1,65 @@
<!---
Copy link
Contributor

Choose a reason for hiding this comment

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

Let's move this to amp-ad-network-doubleclick-impl md. We should probably also indicate that this is still in beta, meaning we don't yet verify its correctness.

*/
export let RefreshConfig;

export const MIN_REFRESH_INTERVAL = 3;
Copy link
Contributor

Choose a reason for hiding this comment

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

MIN_REFRESH_INTERVAL is set to 3 seconds, but the comment for checkAndSanitizeRefreshInterval_() states that the minimum is 30 seconds as well as the documentation in the "amp-ad-network-doubleclick-impl-internal.md" file.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

The shorter time delay makes testing and development much easier. I'll be sure to have this changed to 30 before submitting.

@lannka lannka requested a review from zhouyx June 27, 2017 21:33
@lannka
Copy link
Contributor

lannka commented Jun 27, 2017

@zhouyx could you please take a look at this PR?

* @param {!Element} adElement
* @return {!Array<string>}
*/
export function getEnclosingContainerTypes(adElement) {
Copy link
Contributor

Choose a reason for hiding this comment

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

why we have a counter 20 here? Is checking el.tagName == 'BODY' better, like what we do in #isAdPositionAllowed function

Copy link
Member

Choose a reason for hiding this comment

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

This is considered a best practice in the context of ads tagging. Apparently, even though it should by all rights be impossible, there have been reports of JavaScript tags encountering cyclic DOM trees in the wild. If code that traversed the DOM without a loop counter encountered such a cycle, the browser would hang.

That said, I'm not sure AMP really needs to worry about this.

return Promise.resolve(false);
}
return new Promise(resolve => {
analyticsForDoc(this.element_, true).then(analytics => {
Copy link
Contributor

Choose a reason for hiding this comment

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

Hmmm I wonder if there's better approach here. I don't like the idea of loading analytics extension only to use the visibilityManager. visibiltyManager is a small part of amp-analytics, we can either refactor the visiblityManager to a shared class, or have refresh manager implement its version.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Initially I had my own implementation, but was explicitly asked to use visibilityManager instead. If we go with the refactoring route, who would own the refactoring?

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'm not too familiar with how visibility-manager, or most of analytics for that matter, works. Is there a reason I can't directly create an instance of VisibilityManager and use that?

Copy link
Contributor

Choose a reason for hiding this comment

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

I see. if we refactor it I think it will need to be a service. Do you still remember why asked to use visibilityManager?

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 think the idea was to not reinvent the wheel. I don't know if there was any reason beyond 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.

Would it be possible to leave this as is for now, and optimize it in a later PR? I would gladly own the refactoring task.

Copy link
Contributor

Choose a reason for hiding this comment

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

There will be the hard question where do we put the code if we want to share.
We certainly don't want it to be in amphtml/core to bloat the size.
We also don't want a4a to have direct code dependency with analytics.

And also, given that your logic might be simpler than the visibilitymanager in amp-analytics, implementing your own might not be bad idea. You can still reuse the lower level IntersectionObserver code (polyfill etc).

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Okay, I'm sure I can salvage most of the code that I had already written.

Copy link
Contributor

Choose a reason for hiding this comment

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

@jridgewell since you're planning to provide visibility info in a central place with your layoutmanager work, for an easier upgrade to that, do you think here we should use InOb directly, or should we rely on the viewportCallback?

? this.refreshCount_ + 1
: (this.fromResumeCallback ? 1 : this.refreshCount_ || null);
this.win['ampAdGoogleIfiCounter'] = this.win['ampAdGoogleIfiCounter'] || 1;
this.ifi_ = (this.isRefreshing && this.ifi_) ||
Copy link
Contributor

Choose a reason for hiding this comment

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

this.ifi_ is initialized as 0 so isRefreshing case will never evaluate to true, right? It will always increment global?

Copy link
Contributor Author

@glevitzky glevitzky Jul 6, 2017

Choose a reason for hiding this comment

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

On the first pass this.ifi_ will be set to some non-zero value. On the second pass, if this.isRefreshing is true, then we'll use the value of this.ifi_ (no incrementing), otherwise we'll use the incremented value of this.win['ampAdGoogleIfiCounter'].

/** @override */
layoutCallback() {
const superReturnValue = super.layoutCallback();
this.refreshManager_ = this.refreshManager_ || new RefreshManager(this, {
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 to add check that we do not allow refresh if SRA is enabled. Ideally we would also report this to the pub

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.

* IntersectionObserver implementation. It will implement the core logic of
* the refresh lifecycle, including the transitions of the DFA.
*
* @return {function(!Array<IntersectionObserverEntry>)}
Copy link
Contributor

Choose a reason for hiding this comment

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

{!function(!Array<!IntersectionObserverEntry>>)}

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Using {!function(!Array<IntersectionObserverEntry>>)} since the array can be empty.

*/
ioCallbackGenerator_() {
const refreshManager = this;
return entries => {
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 this can't be:
return entries => entries.forEach(entry => {
if (entry.target != refreshManager.element_) return;
switch(refreshManager.state_) {
...
}
});

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.

* @param {!Element} adElement
* @return {!Array<string>}
*/
export function getEnclosingContainerTypes(adElement) {
Copy link
Member

Choose a reason for hiding this comment

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

This is considered a best practice in the context of ads tagging. Apparently, even though it should by all rights be impossible, there have been reports of JavaScript tags encountering cyclic DOM trees in the wild. If code that traversed the DOM without a loop counter encountered such a cycle, the browser would hang.

That said, I'm not sure AMP really needs to worry about this.

* abandoned. If set to false, then malformed sizes will be ignored, and the
* remainder of the string will be parsed for any additional sizes.
* Additionally, errors will only be reported if this flag is set to true.
* @return {?Array<Array<number>>} An array of dimensions.
Copy link
Member

Choose a reason for hiding this comment

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

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

badDim: 'width',
badVal: width,
};
errorBuilder, reportError) {
Copy link
Member

Choose a reason for hiding this comment

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

Optional parameters need to have explicit default values.

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.

* Attemps to render the returned creative following the resolution of the
* adPromise.
*
* @return {!Promise} Whether the creative was successfully rendered.
Copy link
Member

Choose a reason for hiding this comment

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

Needs generic arguments.

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.isRefreshing is true.
* @protected
*/
destroyFrame(force) {
Copy link
Member

Choose a reason for hiding this comment

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

Optional parameter needs explicit default.

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.

/**
* Defines the DFA states for the refresh cycle.
*
* (1) All newly registered elements begin in the INITIAL state.
Copy link
Member

Choose a reason for hiding this comment

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

Better to use 1. as this is a Markdown list.

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 {!Object<string, (!IntersectionObserver|!IntersectionObserverPolyfill)>}
*/
const observers = {};
Copy link
Member

Choose a reason for hiding this comment

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

Can you explain why there needs to be mutable state in module-level variables? I tend to think of this as an antipattern.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

There's a many to many relationship between RefreshManagers and IntersectionObservers. A RefreshManager needs a way of referencing all previously created IntersectionObservers so as to not create duplicates. Moreover, each IntersectionObserver iterates over a list of entries, each of which is associated with a particular RefreshManager--meaning each IO needs to have a way of referencing all existing RefreshManagers.

If you insist, I can wrap the module-level variables in a singleton, but it seems like overkill to me.

Copy link
Member

Choose a reason for hiding this comment

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

I'm slightly worried about the implications for testing and the more usual approach would be to use a window-level property, but if this works then it works, I suppose.

// The network has opted-in.
!!(this.config_
// The publisher has enabled refresh on this slot.
&& (this.refreshInterval_ || this.refreshInterval_ == '')
Copy link
Member

Choose a reason for hiding this comment

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

Infix operators go before line breaks.

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.

* IntersectionObserver implementation. It will implement the core logic of
* the refresh lifecycle, including the transitions of the DFA.
*
* @param {!Array<IntersectionObserverEntry>} entries
Copy link
Member

Choose a reason for hiding this comment

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

!IntersectionObserverEntry

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.

}

/**
* Converts config to appropriate units.
Copy link
Member

Choose a reason for hiding this comment

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

Please state explicitly that this mutates its argument.

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.

* The element will remain in this state until reset() is called on the
* element, at which point it will return to the INITIAL state.
*
* @enum {string}
Copy link
Member

Choose a reason for hiding this comment

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

If the enum is unexported then the tests probably shouldn't be introspecting it. But you can leave it as is.

this.isRefreshing = true;
this.tearDownSlot();
this.initiateAdRequest();
const promiseId = this.promiseId_;
Copy link
Contributor

Choose a reason for hiding this comment

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

Add a dev().assert(this.adPromise_)

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.

/** @override */
layoutCallback() {
const superReturnValue = super.layoutCallback();
user().assert(
Copy link
Contributor

Choose a reason for hiding this comment

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

We don't want to throw here so instead just wrap user().warn in if statement

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.

refresh(refreshEndCallback) {
const promise = super.refresh(refreshEndCallback);
this.refreshCount_++;
return promise;
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 return super.refresh(refreshEndCallback)?

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.

@@ -487,6 +489,52 @@ describes.sandboxed('amp-ad-network-doubleclick-impl', {}, () => {
// Ensure that "auto" doesn't appear anywhere here:
expect(url).to.match(/sz=[0-9]+x[0-9]+%7C1x2%7C3x4&/));
});
it('should have the correct ifi numbers - no refresh', function() {
Copy link
Contributor

Choose a reason for hiding this comment

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

Add SRA + refresh test

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.

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.

Minor items. Waiting to merge until @lannka or @zhouyx have a chance to complete review.

(this.refreshInterval_ || this.refreshInterval_ == '') &&
// The slot is contained only within container types eligible for
// refresh.
!getEnclosingContainerTypes(this.element_).filter(container =>
Copy link
Contributor

Choose a reason for hiding this comment

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

arguably this constraint should be within doubleclick impl and not refresh manager as other networks will likely not have the same constraint. Mind moving it?

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.

@@ -72,4 +72,48 @@ Below the term `primary size` refers to the width and height pair specified by t
- `data-multi-size` A string of comma separated sizes, which if present, forces the tag to request an ad with all of the given sizes, including the primary size. Each individual size must be a number (the width) followed by a lowercase 'x' followed by a number (the height). Each dimension specified this way must not be larger than its counterpart in the primary size. Further, each dimension must be no less than 2/3rds of the corresponding primary dimension, unless `data-mutli-size-validation` is set to false.
- `data-multi-size-validation` If set to false, this will allow secondary sizes (those specified in the `data-multi-size` attribute) to be less than 2/3rds of the corresponding primary size. By default this is assumed to be true.

### AMP Ad Refresh
Copy link
Contributor

Choose a reason for hiding this comment

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

Mind adding a section on SRA and mentioning the refresh+SRA is not supported?

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.

@erwinmombay
Copy link
Member

is this good to merge?

@zhouyx
Copy link
Contributor

zhouyx commented Jul 18, 2017

LGTM

@b2adops
Copy link

b2adops commented Jul 3, 2018

has there been any progress on this project to enable refresh on inline amp ad units? I work with several publishers where this functionality has been requested.

@glevitzky
Copy link
Contributor Author

Yes, refresh is now available on all DoubleClick type amp-ad units, except where SRA is enabled.

@b2adops
Copy link

b2adops commented Jul 3, 2018

Great! Is enabling as simple as adding data-enable-refresh="30" to the amp tag, or is more needed in order to enable? Is it best practice to add a refresh call to each individual ad slot, or in the meta tag?

@glevitzky
Copy link
Contributor Author

data-enable-refresh="30" is all you need. There's no preference between using a meta-tag or doing it at the slot-level--either is fine, depending on your use-case.

@b2adops
Copy link

b2adops commented Jul 3, 2018

Thanks! We're working with a company called adthrive and the amp tags they provide are not DoubleClick type, would the same principles work here, or would we need these ads to run as doubleclick to ensure functionality?

@glevitzky
Copy link
Contributor Author

Ah, my apologies for misunderstanding. Currently, DoubleClick is the only ad network that supports refresh. In order for adthrive to support it, they would need to write their own Fast Fetch extension and implement refresh within it. If this is something they're interested in doing, let me know, and I'll try and provide as much support as I can.

@b2adops
Copy link

b2adops commented Jul 3, 2018

Thanks so much, I really appreciate it. I've got a call with adthrive this afternoon and will have a better idea of next steps then.

thanks again!

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