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
AMP Ad Refresh #9535
Conversation
extensions/amp-a4a/0.1/amp-a4a.js
Outdated
this.refreshManager_ = getRefreshManagerFor(this.win); | ||
|
||
this.refreshManager_.registerElement(this.element, refresher => { | ||
console.log('Eureka!'); |
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 assume all console.log
statements aren't supposed to be 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.
Correct. I'll be sure to remove them once I make more progress.
extensions/amp-a4a/0.1/amp-a4a.js
Outdated
@@ -345,6 +347,45 @@ export class AmpA4A extends AMP.BaseElement { | |||
|
|||
/** @private {string} */ | |||
this.safeframeVersion_ = DEFAULT_SAFEFRAME_VERSION; | |||
|
|||
/** @private @const {RefreshManager} */ |
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.
!RefreshManager
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.
* share the same minOnScreenPixelRatioThreshold will be monitored by the | ||
* same IO. | ||
* | ||
* @private {!Object<string, (IntersectionObserver|!IntersectionObserverPolyfill)>} |
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.
!IntersectionObserver
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.
* monitored, indexed by a unique ID stored as data-amp-a4a-refresh-id on | ||
* the element. | ||
* | ||
* @private {!Object<string, RegisteredElementWrapper>} |
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.
!RegisteredElementWrapper
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,470 @@ | |||
/** | |||
* Copyright 2016 The AMP HTML Authors. All Rights Reserved. |
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.
2017
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.
|
||
/** | ||
* @param {!Element} element The element to be wrapped. | ||
* @param {!function()} callback The function to be invoked when the element |
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.
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.
Done.
/** | ||
* The function that will be invoked when the refreshInterval has expired. | ||
* | ||
* @private @const {!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.
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.
Done.
/** | ||
* Invokes the callback function. | ||
* | ||
* @param {RefreshManager} refreshManager The calling refreshManager, passed |
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.
!RefreshManager
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,117 @@ | |||
/** | |||
* Copyright 2015 The AMP HTML Authors. All Rights Reserved. |
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.
2017
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.
}); | ||
refreshManager.registerElement( | ||
testElement, () => { | ||
expect(false).to.be.true; |
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 fail
here. Also, how can resolver
ever run? What's the path of execution through this 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.
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.
Also, please add Markdown docs for page authors. |
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.
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) { |
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.
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).
extensions/amp-a4a/0.1/amp-a4a.js
Outdated
this.refreshManager_.registerElement(this.element, refresher => { | ||
console.log('Eureka!'); | ||
this.isRefreshing_ = true; | ||
this.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.
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.
extensions/amp-a4a/0.1/amp-a4a.js
Outdated
this.unlayoutCallback(); | ||
this.onLayoutMeasure(); | ||
this.adPromise_.then(() => { | ||
if (!this.isRefreshing_) { |
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.
When would this occur? Presumably only if unlayoutCallback was executed? If so, we should expect the promise chain to have been cancelled, right?
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.
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.
extensions/amp-a4a/0.1/amp-a4a.js
Outdated
@@ -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); |
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.
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.
extensions/amp-a4a/0.1/amp-a4a.js
Outdated
/** @private @const {!./refresh-manager.RefreshManager} */ | ||
this.refreshManager_ = refreshManagerFor(this.win); | ||
|
||
this.refreshManager_.registerElement(this.element, refresher => { |
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.
Item missing here is building the config that indicates if/how element is configured for refresh. Will this be page level? block level? both?
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 will be both. As mentioned in the PR description, this isn't implemented yet.
@@ -152,6 +156,39 @@ export class AmpAdNetworkDoubleclickImpl extends AmpA4A { | |||
]); | |||
} | |||
|
|||
initializeRefreshIfEligible() { |
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.
Doc and any reason this can't be private?
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.
Removed.
} | ||
new RefreshManager(this.win, this.element, refresher => { | ||
this.isRefreshing = true; | ||
this.tearDownSlot(); |
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.
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.
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.
extensions/amp-a4a/0.1/amp-a4a.js
Outdated
if (!this.isRefreshing_) { | ||
// If this refresh cycle was canceled, such as in a no-content | ||
// response case, keep showing the old creative. | ||
refresher.initiateRefreshCycle(); |
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.
Will fix this in next pass.
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 CSI pings appropriately distinguish between refresh and non-refresh requests? Probably good to have.
ads/_a4a-config.js
Outdated
/** @type {!../extensions/amp-a4a/0.1/refresh-manager.RefreshConfig} */ | ||
export const DEFAULT_REFRESH_CONFIG = { | ||
visiblePercentageMin: 50, | ||
totalTimeMin: 0, |
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.
What is totalTimeMin for? I don't see you reference it anywhere.
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.
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, |
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 you add documentation explaining what these mean? Also would be good if time values indicated milliseconds
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. All times are in ms.
extensions/amp-a4a/0.1/amp-a4a.js
Outdated
@@ -741,6 +762,34 @@ export class AmpA4A extends AMP.BaseElement { | |||
} | |||
|
|||
/** | |||
* Refreshes ad slot by fetching new creative and rendering it. |
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.
Mention that it leaves the remaining creative in place until the new ad promise chain resolves
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.
extensions/amp-a4a/0.1/amp-a4a.js
Outdated
*/ | ||
refresh() { | ||
this.isRefreshing_ = true; | ||
this.tearDownSlot(); |
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 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:
- 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)
- For multi-size case, we could just remove sizes that are no longer eligible.
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.
Option 2 is what we initially planned on, so that's what I'm going to implement.
extensions/amp-a4a/0.1/amp-a4a.js
Outdated
this.tearDownSlot(); | ||
this.initiateAdRequest(); | ||
this.adPromise_.then(() => { | ||
if (!this.isRefreshing_) { |
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.
Check promiseId indicating cancellation?
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.
extensions/amp-a4a/0.1/amp-a4a.js
Outdated
* this.isRefreshing_ is true. | ||
* @protected | ||
*/ | ||
destroyFrame(force = false) { |
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 need to set default value as false as undefined is essentially equivalent 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.
Done.
this.iframe.parentElement.removeChild(this.iframe); | ||
this.iframe = null; | ||
} | ||
this.destroyFrame(); |
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.
shouldn't this be this.destroyFrame(true) to ensure the frame is destroyed on 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.
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.
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.
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.
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.
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.
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.
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(); |
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 wait until after layout otherwise its possible refresh interval would include time to fetch and render first ad slot?
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.
} | ||
analyticsForDoc(this.element_, true).then(analytics => { | ||
analytics.getAnalyticsRoot(this.element_).getVisibilityManager() | ||
.listenElement(this.element_, this.config_, null, 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.
Will this fire multiple times allowing for continued refresh? Or do you need to re-initialize after every successful render?
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.
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'; |
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.
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')));
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'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.
extensions/amp-a4a/amp-ad-refresh.md
Outdated
@@ -0,0 +1,65 @@ | |||
<!--- |
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 whether this is the appropriate location for this information; will move upon reviewer's suggestion.
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.
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.
ads/_a4a-config.js
Outdated
@@ -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 = { |
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 do we need this here? Why don't we just pass the config into RefreshManager when its created within amp-ad-network-doubleclick-impl?
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.
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.
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.
Let's remove it, I don't see much value in having it here and this file is included in amp-ad
ads/google/a4a/utils.js
Outdated
@@ -54,9 +54,10 @@ const AmpAdImplementation = { | |||
|
|||
/** @const {!Object} */ | |||
export const ValidAdContainerTypes = { | |||
'AMP-STICKY-AD': 'sa', | |||
'AMP-CAROUSEL': 'ac', |
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.
Did you intend for these and the multi-size changes to be included in this PR?
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.
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.
extensions/amp-a4a/0.1/amp-a4a.js
Outdated
// show the loader for a quarter of a second before switching to | ||
// the new creative. | ||
timerFor(this.win).delay(() => { | ||
this.attemptToRenderCreative().then(() => { |
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 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.
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. |
extensions/amp-a4a/0.1/amp-a4a.js
Outdated
* @param {function()} refreshEndCallback When called, this function will | ||
* restart the refresh cycle. | ||
*/ | ||
refresh(refreshEndCallback) { |
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.
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?
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.
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.
ads/_a4a-config.js
Outdated
@@ -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 = { |
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.
Let's remove it, I don't see much value in having it here and this file is included in amp-ad
ads/_a4a-config.js
Outdated
* | ||
* @type {!Object<string, !../extensions/amp-a4a/0.1/refresh-manager.RefreshConfig>} | ||
*/ | ||
export const refreshConfigs = { |
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.
Ditto, this should be removed
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 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 |
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.
FWIW without multiSizeValidation enabled, we don't verify that the size if > 0. Perhaps GPT has special values that could be <= 0?
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.
ads/google/utils.js
Outdated
if (widthCond(width) && heightCond(height)) { | ||
const isWidthIllegal = widthCond(width); | ||
const isHeightIllegal = heightCond(height); | ||
if (isWidthIllegal && isHeightIllegal) { |
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.
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;
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.
Great suggestion, thanks! Done.
extensions/amp-a4a/0.1/amp-a4a.js
Outdated
@@ -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'; |
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.
We can combine all of these into a single import. Something like:
import {cryptoFo, resourcesForDoc, timerFor, viewerFor} from....
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. Unless you actually want all/most of the imports inline.
extensions/amp-a4a/0.1/amp-a4a.js
Outdated
|
||
/** | ||
* Indicates whether the ad is currently in the process of being refreshed. | ||
* |
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: I wouldn't put an extra line here and in other fields
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.
extensions/amp-a4a/0.1/amp-a4a.js
Outdated
* restart the refresh cycle. | ||
*/ | ||
refresh(refreshEndCallback) { | ||
this.isRefreshing = true; |
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.
Add an assert before this: dev().assert(!this.isRefreshing)
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.
extensions/amp-a4a/0.1/amp-a4a.js
Outdated
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 |
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 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
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.
extensions/amp-a4a/0.1/amp-a4a.js
Outdated
refreshEndCallback(); | ||
return; | ||
} | ||
this.togglePlaceholder(true); |
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.
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.
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.
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.
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.
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.
ads/google/a4a/utils.js
Outdated
(containerTypeSet[ValidAdContainerTypes['AMP-FX-FLYING-CARPET']] | ||
|| containerTypeSet[ValidAdContainerTypes['AMP-STICKY-AD']]) | ||
(enclosingContainers.indexOf( | ||
ValidAdContainerTypes['AMP-FX-FLYING-CARPET']) != -1 |
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 you use includes:
const pfx = enclosingContainers.includes(ValidAdContainerTypes['AMP-FX-FLYING-CARPET']) || enclosingContainers.includes(ValidAdContainerTypes['AMP-STICKY-AD']);
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.
ads/google/utils.js
Outdated
* @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 |
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.
abnadoned -> abandoned
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.
ads/google/utils.js
Outdated
* 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. |
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.
{?Array<!Array<number>>}
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 possible for this function to return an empty array.
return; | ||
w => isNaN(w) || w <= 0, | ||
h => isNaN(h) || h <= 0, | ||
badParams => badParams.map(badParam => |
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.
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
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.
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.
extensions/amp-a4a/0.1/amp-a4a.js
Outdated
this.isRefreshing = false; | ||
|
||
/** @private {boolean} */ | ||
this.isRelayoutNeeded_ = false; |
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.
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) { |
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 this just be:
user().assert(pair.length == 2, 'refresh metadata...');
if (pair[0] == this.adType_) {
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.
Done.
if (isNaN(refreshInterval) || refreshInterval == '') { | ||
user().warn(TAG, 'refresh interval must be a number'); | ||
return null; | ||
} else if (refreshInterval < MIN_REFRESH_INTERVAL) { |
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 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}`);
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.
totalTimeMin: 0, | ||
continuousTimeMin: 1, | ||
}); | ||
if (this.refreshManager_.isRefreshable()) { |
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 refreshManager constructor just initiate the refresh cycle? Then you could minimize this to:
this.refreshManager_ = this.refreshManager_ || new RefreshManager(...);
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.
* @param {function()} refreshEndCallback | ||
* | ||
* @override | ||
*/ |
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.
This can just be /** @override */
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.
* @return {!Promise} awaiting render complete promise | ||
*/ | ||
init(iframe, opt_isA4A) { | ||
init(iframe, opt_isA4A, opt_isRefreshing) { |
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.
Let's discuss offline but ideally we wouldn't polluate xorigin handler for this case
The Percy failure was due to #10022, which has now been fixed. |
extensions/amp-a4a/0.1/amp-a4a.js
Outdated
* the refresh function complete. This is particularly handy for testing. | ||
*/ | ||
refresh(refreshEndCallback) { | ||
return new Promise(resolve => { |
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 do you need a new Promise here? You should just be able to have each of the promise chains 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.
I'd have to wrap the timerFor block in a new Promise at the least. I can do that if you find it preferable.
extensions/amp-a4a/0.1/amp-a4a.js
Outdated
refreshEndCallback(); | ||
return; | ||
} | ||
this.getVsync().mutate(() => { |
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 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
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.
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.
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 timerFor(...).promise(1000) which will return a promise allowing for easier integration
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, sorry. Didn't know that function existed.
extensions/amp-a4a/0.1/amp-a4a.js
Outdated
refreshEndCallback(); | ||
return; | ||
} | ||
return this.getVsync().mutate(() => { |
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 still think this should be this.mutateElement(() => {...
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.
sandbox.restore(); | ||
}); | ||
|
||
it('should effectively reset the slot and invoke given callback', () => { |
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 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
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.
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?
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.
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?
extensions/amp-a4a/amp-ad-refresh.md
Outdated
@@ -0,0 +1,65 @@ | |||
<!--- |
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.
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; |
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.
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.
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.
The shorter time delay makes testing and development much easier. I'll be sure to have this changed to 30 before submitting.
@zhouyx could you please take a look at this PR? |
* @param {!Element} adElement | ||
* @return {!Array<string>} | ||
*/ | ||
export function getEnclosingContainerTypes(adElement) { |
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 we have a counter 20 here? Is checking el.tagName == 'BODY'
better, like what we do in #isAdPositionAllowed
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.
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 => { |
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.
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.
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.
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?
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 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?
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 see. if we refactor it I think it will need to be a service. Do you still remember why asked to use visibilityManager?
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 think the idea was to not reinvent the wheel. I don't know if there was any reason beyond that.
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.
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.
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 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).
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.
Okay, I'm sure I can salvage most of the code that I had already written.
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.
@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_) || |
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.
this.ifi_ is initialized as 0 so isRefreshing case will never evaluate to true, right? It will always increment global?
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.
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, { |
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.
Make sure to add check that we do not allow refresh if SRA is enabled. Ideally we would also report this to the pub
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.
* IntersectionObserver implementation. It will implement the core logic of | ||
* the refresh lifecycle, including the transitions of the DFA. | ||
* | ||
* @return {function(!Array<IntersectionObserverEntry>)} |
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.
{!function(!Array<!IntersectionObserverEntry>>)}
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.
Using {!function(!Array<IntersectionObserverEntry>>)}
since the array can be empty.
*/ | ||
ioCallbackGenerator_() { | ||
const refreshManager = this; | ||
return entries => { |
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 this can't be:
return entries => entries.forEach(entry => {
if (entry.target != refreshManager.element_) return;
switch(refreshManager.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.
* @param {!Element} adElement | ||
* @return {!Array<string>} | ||
*/ | ||
export function getEnclosingContainerTypes(adElement) { |
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.
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.
ads/google/utils.js
Outdated
* 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. |
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.
!Array
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.
ads/google/utils.js
Outdated
badDim: 'width', | ||
badVal: width, | ||
}; | ||
errorBuilder, reportError) { |
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.
Optional parameters need to have explicit default values.
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.
extensions/amp-a4a/0.1/amp-a4a.js
Outdated
* Attemps to render the returned creative following the resolution of the | ||
* adPromise. | ||
* | ||
* @return {!Promise} Whether the creative was successfully rendered. |
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.
Needs generic arguments.
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.
extensions/amp-a4a/0.1/amp-a4a.js
Outdated
* this.isRefreshing is true. | ||
* @protected | ||
*/ | ||
destroyFrame(force) { |
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.
Optional parameter needs explicit default.
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.
/** | ||
* Defines the DFA states for the refresh cycle. | ||
* | ||
* (1) All newly registered elements begin in the INITIAL 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.
Better to use 1.
as this is a Markdown list.
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.
* | ||
* @const {!Object<string, (!IntersectionObserver|!IntersectionObserverPolyfill)>} | ||
*/ | ||
const observers = {}; |
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 you explain why there needs to be mutable state in module-level variables? I tend to think of this as an antipattern.
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'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.
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 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_ == '') |
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.
Infix operators go before line breaks.
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.
* IntersectionObserver implementation. It will implement the core logic of | ||
* the refresh lifecycle, including the transitions of the DFA. | ||
* | ||
* @param {!Array<IntersectionObserverEntry>} entries |
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.
!IntersectionObserverEntry
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.
} | ||
|
||
/** | ||
* Converts config to appropriate units. |
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 state explicitly that this mutates its argument.
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.
* 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} |
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 the enum is unexported then the tests probably shouldn't be introspecting it. But you can leave it as is.
extensions/amp-a4a/0.1/amp-a4a.js
Outdated
this.isRefreshing = true; | ||
this.tearDownSlot(); | ||
this.initiateAdRequest(); | ||
const promiseId = this.promiseId_; |
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.
Add a dev().assert(this.adPromise_)
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.
/** @override */ | ||
layoutCallback() { | ||
const superReturnValue = super.layoutCallback(); | ||
user().assert( |
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.
We don't want to throw here so instead just wrap user().warn in if statement
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.
refresh(refreshEndCallback) { | ||
const promise = super.refresh(refreshEndCallback); | ||
this.refreshCount_++; | ||
return promise; |
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 return super.refresh(refreshEndCallback)?
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.
@@ -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() { |
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.
Add SRA + refresh 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.
(this.refreshInterval_ || this.refreshInterval_ == '') && | ||
// The slot is contained only within container types eligible for | ||
// refresh. | ||
!getEnclosingContainerTypes(this.element_).filter(container => |
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.
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?
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.
@@ -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 |
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.
Mind adding a section on SRA and mentioning the refresh+SRA is not supported?
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.
is this good to merge? |
LGTM |
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. |
Yes, refresh is now available on all DoubleClick type amp-ad units, except where SRA is enabled. |
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? |
|
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? |
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. |
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! |
Implements amp-ad refresh feature.
What's not included:
Closes #4038