Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Amp analytics 3p vendors #9661

Closed
wants to merge 52 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
52 commits
Select commit Hold shift + click to select a range
a4ea69b
Transfer files from old 3p_analytics to new fresh branch because it h…
jonkeller May 29, 2017
dabbd98
Now using iframe-messaging-client
jonkeller May 30, 2017
2a4cd8f
Implements message throttling
jonkeller May 31, 2017
cbead9f
Wires up xframe response
jonkeller May 31, 2017
e919e86
Fixes issues with multple ads
jonkeller May 31, 2017
7b5ba9d
Fixes extraData association with proper amp-analytics tag
jonkeller Jun 1, 2017
d44fd58
Fixes presubmits
jonkeller Jun 1, 2017
7856841
Fixes whitespace only
jonkeller Jun 1, 2017
99bea30
Fixes linter issues, mostly whitespace
jonkeller Jun 1, 2017
cd5daac
Fixes a comment. Also makes doneWithCrossDomainIframe() become a stat…
jonkeller Jun 1, 2017
ff6952e
Fixes unit tests
jonkeller Jun 1, 2017
e3f7e79
Review feedback low-hanging fruit
jonkeller Jun 1, 2017
eb9c356
Makes event names more explicit to 3p use case
jonkeller Jun 1, 2017
a403e37
Moves helper JS to 3p, compiles in imported resources, so helper JS n…
jonkeller Jun 2, 2017
6be6c26
Fixes message ordering so that extraData is always enqueued first. Ex…
jonkeller Jun 2, 2017
9a2900f
Extracts calls to timerFor(window) to a static class member, which no…
jonkeller Jun 3, 2017
d8e1517
Parameterizes click URL substitution. Stops using frame load promise …
jonkeller Jun 4, 2017
06b11ed
Fixes timer logic which can result in lost messages upon delayed fetch
jonkeller Jun 5, 2017
6bc9776
Merge of gulpfile.js
jonkeller Jun 5, 2017
e8c7f1f
Fix linter issues. Refactor object that encloses xframe responses
jonkeller Jun 5, 2017
7ce3793
1:1 checkpoint
jonkeller Jun 5, 2017
0c8b3a2
Makes response be an object, not string. Includes real absolute URL f…
jonkeller Jun 5, 2017
08706f8
Makes iframeMessagingClient now accept a newly-created iframe, and up…
jonkeller Jun 5, 2017
d44ca1b
Moves message type strings to an enum. Fixes closure typing issues
jonkeller Jun 6, 2017
8b6cec8
Fix closure typing issues
jonkeller Jun 6, 2017
beb78eb
Changes closure typing
jonkeller Jun 6, 2017
af36850
Removes gulpfile change that breaks type checking
jonkeller Jun 6, 2017
faad371
Adds file that enum and response container were extracted to
jonkeller Jun 6, 2017
30da904
Changes the flow to provide one AmpAnalytics3pCreativeMessageRouter p…
jonkeller Jun 7, 2017
21fb523
Fixes closure typing
jonkeller Jun 7, 2017
6a53ab6
Replaces 2 lines accidentally deleted during rebase
jonkeller Jun 7, 2017
cc245fa
Addresses review feedback, mostly minor
jonkeller Jun 7, 2017
b8bd741
Refactors message queueing from Transport into new class. Adds extern…
jonkeller Jun 8, 2017
b7d8665
Addresses review feedback re: minor code organization issues and logi…
jonkeller Jun 8, 2017
c25c199
Addresses PR feedback, mostly typing, and format of messages
jonkeller Jun 12, 2017
59170c8
Replaces custom throttling code with src/utils/rate-limit
jonkeller Jun 12, 2017
896e9fc
Changes format of postMessage messages
jonkeller Jun 13, 2017
10c6cd9
Addresses review feedback, mostly minor code style stuff
jonkeller Jun 14, 2017
14e9939
Removes frame-side message queueing which was necessary under old mes…
jonkeller Jun 14, 2017
be9c95c
Addresses review feedback
jonkeller Jun 14, 2017
036e2d0
Updates linter compliance
jonkeller Jun 15, 2017
7d3074b
Implements changes from self-review, mostly nits
jonkeller Jun 15, 2017
36918a4
Updates response handling to new data format
jonkeller Jun 19, 2017
3aeb8de
Addresses PR feedback, mostly nits
jonkeller Jun 21, 2017
591e18c
Puts anchor click listener binding into AmpDoc
jonkeller Jun 21, 2017
c446a5e
Fixes Travis issue that check-types is not catching locally
jonkeller Jun 21, 2017
3c71898
Removes throw statement from abstract method body, since the ensueing…
jonkeller Jun 21, 2017
dd4034b
Trivial comment change, to force Percy to run again
jonkeller Jun 22, 2017
f80268d
Adds unit tests
jonkeller Jun 27, 2017
e0b9f71
Makes typing of frame name more deliberate
jonkeller Jun 27, 2017
c84129a
Just modifies comment, to force CI to re-run
jonkeller Jun 27, 2017
94cf287
Fixes typing of frame name, for real this time
jonkeller Jun 27, 2017
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
225 changes: 225 additions & 0 deletions 3p/ampanalytics-lib.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,225 @@
/**
* Copyright 2017 The AMP HTML Authors. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS-IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

import './polyfills';
import {tryParseJson} from '../src/json';
import {dev, initLogConstructor, setReportError} from '../src/log';
import {IframeMessagingClient} from './iframe-messaging-client';
import {AMP_ANALYTICS_3P_MESSAGE_TYPE} from '../src/3p-analytics-common';

initLogConstructor();
// TODO(alanorozco): Refactor src/error.reportError so it does not contain big
// transitive dependencies and can be included here.
setReportError(() => {});

/** @private @const {string} */
const TAG_ = 'ampanalytics-lib';

/**
* Receives messages bound for this cross-domain iframe, from all creatives
*/
class AmpAnalytics3pMessageRouter {
Copy link
Contributor

Choose a reason for hiding this comment

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

to accelerate this PR, I suggest you split off the code into a 2nd PR. I can see clear boundary between the 2:
PR1: runtime side code
PR2: adding a library for 3p to use.

As long as there're clear API defined, you can easily separate the 2


/** @param {!Window} win */
constructor(win) {
/** @private {!Window} */
this.win_ = win;

// The sentinel is required by iframe-messaging-client, and is used to
// uniquely identify the frame as part of message routing
/** @const {string} */
this.sentinel_ = dev().assertString(
tryParseJson(this.win_.name, {}).sentinel,
'Invalid/missing sentinel on iframe name attribute' + this.win_.name);
if (!this.sentinel_) {
return;
}

/**
* Multiple creatives on a page may wish to use the same type of
* amp-analytics tag. This object provides a mapping between the
* IDs which identify which amp-analytics tag a message is to/from,
* with each ID's corresponding AmpAnalytics3pCreativeMessageRouter,
* which is an object that handles messages to/from a particular creative.
* @private {!Object<string, !AmpAnalytics3pCreativeMessageRouter>}
*/
this.creativeMessageRouters_ = {};

/**
* Handles communication between frames
* @private {!IframeMessagingClient}
*/
this.iframeMessagingClient_ = new IframeMessagingClient(win);
this.iframeMessagingClient_.setSentinel(this.sentinel_);
this.iframeMessagingClient_.registerCallback(
AMP_ANALYTICS_3P_MESSAGE_TYPE.CREATIVE,
messageContainer => {
let entries;
dev().assert(messageContainer.data &&
(entries = Object.entries(messageContainer.data)).length,
'Received empty new creative message');
entries.forEach(entry => {
const creativeId = entry[0];
const extraData = entry[1];
dev().assert(!this.creativeMessageRouters_[creativeId],
'Duplicate new creative message for ' + creativeId);
this.creativeMessageRouters_[creativeId] =
new AmpAnalytics3pCreativeMessageRouter(
this.win_, this.iframeMessagingClient_, creativeId, extraData);
});
});
this.iframeMessagingClient_.registerCallback(
AMP_ANALYTICS_3P_MESSAGE_TYPE.EVENT,
messageContainer => {
let entries;
dev().assert(messageContainer.data &&
(entries = Object.entries(messageContainer.data)).length,
'Received empty events message');
entries.forEach(entry => {
const creativeId = entry[0];
const messages = entry[1];
try {
dev().assert(messages && messages.length,
'Received empty events list for' + creativeId);
dev().assert(this.creativeMessageRouters_[creativeId],
'Discarding event message received prior to new creative' +
' message for' + creativeId);
this.creativeMessageRouters_[creativeId]
.sendMessagesToListener(messages);
} catch (e) {
dev().error(TAG_, 'Failed to send message to listener: ' +
Copy link
Contributor

Choose a reason for hiding this comment

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

Using the term "send" is a bit misleading here as one may expect it to mean you postMessaged. Perhaps "Failed to execute listener"...?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Re: your above comment which GitHub UI won't let me comment on, I forgot to "git add" the file to test the new Queue classes. Will add that together with addressing your comments 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.

And re: the comment I'm actually responding to here, done. Changed to "Failed to pass message to event listener"

e.message);
}
});
});
this.iframeMessagingClient_.sendMessage(
AMP_ANALYTICS_3P_MESSAGE_TYPE.READY);
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 fit on one line?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

81 characters, unless I rename stuff.

}
}

new AmpAnalytics3pMessageRouter(window);

/**
* Receives messages bound for this cross-domain iframe, from a particular
* creative. Also sends response messages from the iframe meant for this
* particular creative.
*/
class AmpAnalytics3pCreativeMessageRouter {
/**
* @param {!Window} win The enclosing window object
* @param {!IframeMessagingClient} iframeMessagingClient Facilitates
* communication with the frame
* @param {!string} creativeId The ID of the creative to route messages
* to/from
* @param {string=} opt_extraData Extra data to be passed to the frame
*/
constructor(win, iframeMessagingClient, creativeId, opt_extraData) {
/** @private {!Window} */
this.win_ = win;

/** @private {!IframeMessagingClient} */
this.iframeMessagingClient_ = iframeMessagingClient;

/** @private {!string} */
this.creativeId_ = creativeId;

/** @private {?string} */
this.extraData_ = opt_extraData;

/** @private {?function(!Array<!AmpAnalytics3pEvent>)} */
this.eventListener_ = null;

if (this.win_.onNewAmpAnalyticsInstance) {
try {
this.win_.onNewAmpAnalyticsInstance(this);
} catch (e) {
dev().error(TAG_, 'Exception thrown by onNewAmpAnalyticsInstance in' +
Copy link
Contributor

Choose a reason for hiding this comment

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

Consider moving onNewAmpAnalyticsInstance check prior to creating AmpAnalytics3pCreativeMessageRouter as AmpAnalytics3pMessageRouter essentially expects it to work properly. In other words, it would call router's sendMessagesToListener function even though client is not actually listening

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.win_.location.href + ': ' + e.message);
}
} else {
dev().error(TAG_, 'Must implement onNewAmpAnalyticsInstance in' +
this.win_.location.href);
}
}

/**
* Registers a callback function to be called when AMP Analytics events occur.
* There may only be one listener. If another function has previously been
* registered as a listener, it will no longer receive events.
* @param {!function(!Array<AmpAnalytics3pEvent>)} listener A function
* that takes an array of event strings, and does something with them.
*/
registerAmpAnalytics3pEventsListener(listener) {
if (this.eventListener_) {
dev().warn(TAG_, 'Replacing existing eventListener for ' +
this.creativeId_);
}
this.eventListener_ = listener;
}

/**
* Receives message(s) from a creative for the cross-domain iframe
* and passes them to that iframe's listener, if a listener has been
* registered
* @param {!Array<AmpAnalytics3pEvent>} messages The message that was received
*/
sendMessagesToListener(messages) {
if (!messages.length || !this.eventListener_) {
Copy link
Contributor

Choose a reason for hiding this comment

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

Consider adding a dev().warn indicating messages are being lost and registerAmpAnalytics3pEventsListener should be called.

We could also try and recover from previous invalid/missing onNewAmpAnalyticsInstance 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.

Given the fix to your previous comment, it is no longer possible to reach here without implementing onNewAmpAnalyticsInstance, as the enclosing AmpAnalytics3pMessageRouter will now not have been created.
However, this could still happen if they have done the first line here but not the second:

      window.onNewAmpAnalyticsInstance = ampAnalytics => {
        ampAnalytics.registerAmpAnalytics3pEventsListener(events => {

Unfortunately we can't go and create that for them, so I just made the dev().warn message explicitly say "Attempted to send messages when no listener configured. Be sure to first call registerAmpAnalytics3pEventsListener()"

return;
}
try {
this.eventListener_(messages);
} catch (e) {
dev().error(TAG_, 'Caught exception executing listener for ' +
this.creativeId_ + ': ' + e.message);
}
}

/**
* Gets any optional extra data that should be made available to the
* cross-domain frame, in the context of a particular creative.
* @returns {?string}
*/
getExtraData() {
return this.extraData_;
}

/**
* Sends a message from the third-party vendor's metrics-collection page back
* to the creative.
* @param {!Object} response The response to send.
*/
sendMessageToCreative(response) {
this.iframeMessagingClient_.sendMessage(
AMP_ANALYTICS_3P_MESSAGE_TYPE.RESPONSE,
this.buildAmpAnalytics3pResponse_(response));
}

/**
* Builds an instance of AmpAnalytics3pResponse
* @param {!Object} response The response from the iframe, which can be
* any object
* @returns {AmpAnalytics3pResponse}
*/
buildAmpAnalytics3pResponse_(response) {
const messageObject = /** @type {AmpAnalytics3pResponse} */ ({
destination: this.creativeId_,
data: response,
});
return messageObject;
}
}

26 changes: 24 additions & 2 deletions 3p/iframe-messaging-client.js
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ export class IframeMessagingClient {
this.win_ = win;
/** @private {?string} */
this.rtvVersion_ = getMode().rtvVersion || null;
/** @private {!Window} */
/** @private {!(Window|HTMLIFrameElement)} */
this.hostWindow_ = win.parent;
/** @private {?string} */
this.sentinel_ = null;
Expand Down Expand Up @@ -101,6 +101,15 @@ export class IframeMessagingClient {
*/
setupEventListener_() {
listen(this.win_, 'message', event => {
if (this.hostWindowIsActuallyAnIframe_()) {
// this.hostWindow_ can now be set to an iframe, after it has been
Copy link
Contributor

Choose a reason for hiding this comment

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

why hostWindow can be an iframe?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

It's a chicken-and-egg problem. I've created this iframe, and I want to know when it is ready. So I want to say "I am willing to receive messages from this iframe's contentWindow". But the iframe's content window is not defined until it has finished rendering. So basically, if I can only use the window object, then I can't know it's ready unless I can receive messages from it, but I can't receive messages from it until I know it's ready.

Copy link
Contributor

Choose a reason for hiding this comment

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

should you wait for a handshake initiated from the iframe window?

Copy link
Contributor

Choose a reason for hiding this comment

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

oh, wait, you wanted to make the iframe client a "host". which should not be a case after you switched to SubscriptionApi.

// created but before it has finished loading. If we've gotten a
// message from that iframe, then it must exist, so its
// contentWindow is now non-null.
this.hostWindow_ =
/** @type {!Window} */ (this.hostWindow_.contentWindow);
}

// Does it look a message from AMP?
if (event.source != this.hostWindow_) {
return;
Expand All @@ -116,7 +125,20 @@ export class IframeMessagingClient {
}

/**
* @param {!Window} win
* Determines whether frameOrWindow is a frame, or a window.
* @returns {boolean}
*/
hostWindowIsActuallyAnIframe_() {
// If it's a window, it will have .postMessage
// We check for that before .contentWindow, since a cross-domain window
// may throw if we try to access anything unsafe
return (!this.hostWindow_.postMessage && !!this.hostWindow_.contentWindow);
}

/**
* @param {!(Window|HTMLIFrameElement)} win The window to communicate with.
* This may be set to a newly-created iframe instead, since its
* contentWindow will be null until it renders.
*/
setHostWindow(win) {
this.hostWindow_ = win;
Expand Down
5 changes: 5 additions & 0 deletions build-system/amp.extern.js
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,11 @@ AmpViewerMessage.prototype.rsvp;
/** @public {string|undefined} */
AmpViewerMessage.prototype.error;

// AMP-Analytics Cross-domain iframes
let AmpAnalytics3pReadyMessage;
let AmpAnalytics3pNewCreative;
let AmpAnalytics3pEvent;
let AmpAnalytics3pResponse;

// amp-viz-vega related externs.
/**
Expand Down
1 change: 1 addition & 0 deletions build-system/config.js
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,7 @@ module.exports = {
'!{node_modules,build,dist,dist.tools,' +
'dist.3p/[0-9]*,dist.3p/current-min}/**/*.*',
'!dist.3p/current/**/ampcontext-lib.js',
'!dist.3p/current/**/ampanalytics-lib.js',
'!validator/dist/**/*.*',
'!validator/node_modules/**/*.*',
'!validator/nodejs/node_modules/**/*.*',
Expand Down
4 changes: 4 additions & 0 deletions build-system/dep-check-config.js
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,10 @@ exports.rules = [
{
filesMatching: '{src,extensions}/**/*.js',
mustNotDependOn: '3p/**/*.js',
whitelist: [
'extensions/amp-analytics/0.1/transport.js->' +
'3p/iframe-messaging-client.js',
Copy link
Contributor Author

@jonkeller jonkeller Jun 1, 2017

Choose a reason for hiding this comment

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

This exception is added because the amp-analytics tag needs to send messages to the third-party iframe. It covers the import of the file that contains sendMessage().

Copy link
Contributor

Choose a reason for hiding this comment

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

don't do this

],
},

// Rules for extensions.
Expand Down
3 changes: 3 additions & 0 deletions build-system/tasks/presubmit-checks.js
Original file line number Diff line number Diff line change
Expand Up @@ -284,6 +284,7 @@ var forbiddenTerms = {
whitelist: [
'3p/integration.js',
'3p/ampcontext-lib.js',
'3p/ampanalytics-lib.js',
'ads/alp/install-alp.js',
'ads/inabox/inabox-host.js',
'dist.3p/current/integration.js',
Expand Down Expand Up @@ -317,7 +318,9 @@ var forbiddenTerms = {
'3p/iframe-messaging-client.js',
'3p/ampcontext.js',
'3p/ampcontext-integration.js',
'3p/ampanalytics-lib.js',
'dist.3p/current/integration.js', // includes previous
'extensions/amp-analytics/0.1/transport.js',
],
},
'\\.sendMessageAwaitResponse\\(': {
Expand Down
38 changes: 38 additions & 0 deletions examples/analytics-3p-remote-frame.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Requests Frame</title>
<script>
/**
* To receive analytics events in a third-party frame, you must
* implement this method, with this signature. Within that method, you
* must create a function which processes the analytics events, and
* pass that function to the registerAmpAnalytics3pEventsListener() method
* of the object which is passed as a parameter to
* onNewAmpAnalyticsInstance().
* @param ampAnalytics Call registerAmpAnalyticsEventListener() on this,
* passing your function which will receive the analytics events.
*/
window.onNewAmpAnalyticsInstance = ampAnalytics => {
ampAnalytics.registerAmpAnalytics3pEventsListener(events => {
events.forEach(event => {
// Now, do something meaningful with the AMP Analytics event
console.log("Received an event: " + event +
", and my extra data is: " +
ampAnalytics.getExtraData());
ampAnalytics.sendMessageToCreative(
{'status': 'received', 'somethingElse': '42'});
});
});
};

// Load the script specified in the iframe’s name attribute:
script = document.createElement('script');
script.src = JSON.parse(window.name).scriptSrc;
Copy link
Contributor

Choose a reason for hiding this comment

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

I fear this opens an XSS exploit for 3p analytic vendors. They shouldn't just trust the script src is to a valid AMP url. Instead we should either provide only the path to the file and hard code the domain or the vendor should have code verifying the domain. See how remote HTML loads the context script: https://github.com/ampproject/amphtml/blob/master/3p/remote.html#L9

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 sounds like a good idea.
In Transport.createCrossDomainIframe, it sets the URL like this:

    const scriptSrc = getMode().localDev
      ? '/dist.3p/current/ampanalytics-lib.js'
      : `${urls.thirdParty}/$internalRuntimeVersion$/ampanalytics-v0.js`;

I just ran it and forced the non-local path, and that gave me:

https://3p.ampproject.net/$internalRuntimeVersion$/ampanalytics-v0.js

So, sure, I could definitely just send everything after "https://3p.ampproject.net", and hard-code that part on the client side.
Though this makes me also wonder if I'm missing something necessary for the substitution of the internal runtime 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.

Hmm...though we'll also have to know on that side whether we're local, since I won't want to prepend the "https://3p.ampproject.net" in that case.

document.head.appendChild(script);
// The script will be loaded, and will call onNewAmpAnalyticsInstance()
</script>
</head>
<!-- The frame will not be visible, so there is no need for a body tag. -->
</html>
26 changes: 26 additions & 0 deletions examples/analytics-3p.amp.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
<!doctype html>
<html ⚡>
<head>
<meta charset="utf-8">
<title>3P AMP Analytics Example</title>
<link rel="canonical" href="amps.html" >
<meta name="viewport" content="width=device-width,minimum-scale=1,initial-scale=1">
<link href='https://fonts.googleapis.com/css?family=Georgia|Open+Sans|Roboto' rel='stylesheet' type='text/css'>
<script async custom-element="amp-analytics" src="https://cdn.ampproject.org/v0/amp-analytics-0.1.js"></script>
<script async custom-element="amp-ad" src="https://cdn.ampproject.org/v0/amp-ad-0.1.js"></script>
<style amp-boilerplate>body{-webkit-animation:-amp-start 8s steps(1,end) 0s 1 normal both;-moz-animation:-amp-start 8s steps(1,end) 0s 1 normal both;-ms-animation:-amp-start 8s steps(1,end) 0s 1 normal both;animation:-amp-start 8s steps(1,end) 0s 1 normal both}@-webkit-keyframes -amp-start{from{visibility:hidden}to{visibility:visible}}@-moz-keyframes -amp-start{from{visibility:hidden}to{visibility:visible}}@-ms-keyframes -amp-start{from{visibility:hidden}to{visibility:visible}}@-o-keyframes -amp-start{from{visibility:hidden}to{visibility:visible}}@keyframes -amp-start{from{visibility:hidden}to{visibility:visible}}</style><noscript><style amp-boilerplate>body{-webkit-animation:none;-moz-animation:none;-ms-animation:none;animation:none}</style></noscript>
<script async src="https://cdn.ampproject.org/v0.js"></script>
</head>
<body>
Here is some text above the ad.<br/>
<amp-ad width="300" height="400"
type="fake"
src="fake_amp_ad_with_3p_analytics.json"
data-use-a4a="true" fakesig="true">
<div placeholder>Loading...</div>
<div fallback>Could not display the fake ad :(</div>
</amp-ad>
<br/>
Here is some text below the ad.<br/>
</body>
</html>
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"creative": "<!doctype html><html amp4ads><head><meta charset=utf-8><script async src=https://cdn.ampproject.org/v0.js></script><script async custom-element=amp-analytics src=https://cdn.ampproject.org/v0/amp-analytics-0.1.js></script><style amp4ads-boilerplate>body{visibility:hidden}</style><meta name=viewport content=width=device-width,minimum-scale=1></head><body><a href=\"http://localhost/landing.html?testStatus=3PANALYTICS(test,status)&testOther=3PANALYTICS(test,somethingElse)&test2Status=3PANALYTICS(test2,status)&test2Other=3PANALYTICS(test2,somethingElse)\"><amp-img height=300 id=img src=https://upload.wikimedia.org/wikipedia/commons/6/6e/Golde33443.jpg width=256></amp-img> <p class=frob>By Golden Trvs Gol twister (Own work) [CC BY-SA 4.0 (http://creativecommons.org/licenses/by-sa/4.0)], via Wikimedia Commons</p></a><amp-analytics type=\"test\"><script type=application/json>{\"requests\":{\"pageview\":\"viewed=true&url=${canonicalUrl}&title=${title}&acct=${account}\",\"event\":\"eid=click&acct=${account}\"},\"vars\":{\"account\":\"ABC123\"},\"triggers\":{\"defaultPageview\":{\"on\":\"visible\",\"request\":\"pageview\"},\"anchorClicks\":{\"on\":\"click\",\"selector\":\"img\",\"request\":\"event\"}},\"transport\":{\"iframe\":\"http://localhost:8000/examples/analytics-3p-remote-frame.html\",\"extraData\":\"ThisIsExtraData\"}}</script></amp-analytics><script type=\"application/json\" amp-ad-metadata>{\"ampRuntimeUtf16CharOffsets\":[55,222],\"bodyAttributes\":\"\",\"bodyUtf16CharOffsets\":[356,2436],\"customElementExtensions\":[\"amp-analytics\"],\"jsonUtf16CharOffsets\":{\"amp-analytics\":[661,2410]}}</script></body></html>",
"signature": "ACWMjZE6z6W9JyytdwVJ6FpbUxDxRcyRbYVT9mv/eYZbaUHP4x9nVHPA9B298mIZeqr73aWipv4aldRrb6SWS7SBi9rgKRvB2F6ppS5mXsEQCkZaBxuBgPKrz4hl2F3mElpV60ScN7xX2kt5tGJp+E9sZCU6K9w9j7XKJRhQ6Hc5TZnPvrWeUiT0Pth0O/KREKIBbR3EocM/5TYEHlQ38e2dtEIiyY6B1BgPLBz80uKASIDNS/IgJoaFWqR7VkZkztq7sjrhgKEGPscJiqVSviN9r9beAJnsBzFio4Onu+FMwY0+kkdKvZ7fhiuMvuSmGqcyrwqwLlGSc0dqYiqx7EGhTDKM"
}

Loading