Skip to content

Commit

Permalink
Introduce iframe transport to amp-analytics - 3p side (ampproject#10596)
Browse files Browse the repository at this point in the history
* Adds 3p iframe to amp analytics. Uses Subscription API. Event message queue much simpler than previous PR. Response functionality is included, but extraData is currently commented out - will add again before PR

* Fix SubscriptionAPI

* Brings in previous chanages to anchor-click-interceptor.js

* Adds unit tests

* Updates queue unit tests

* Updates custom types and unit tests

* Adds misc small fixes/nits from self-review

* Changes 2 URLs in example from absolute to relative

* Comments only

* Fixes TODO re: unlayoutCallback destroying 3p iframe

* Fixes an 80-char line length violation introduced in previous PR

* Implements review feedback

* Implements review feedback

* Addresses review feedback

* Reverts change to URL calculation, pending additional discussion

* Fixes unit test

* Fixes indentation of 2 lines

* Removes extraData, new creative message

* Fixes transport unit tests

* Clarifies transport ID

* Fixes whitespace

* Improves example triggers. Fixes issue of delivery of creative response

* Makes indentation consistent in this section

* Addresses review feedback

* Fixes a null check

* Keys off of type rather than URL. Fixes issue of null origin, without relying on omitting sandbox allow-same-origin

* Minor type annotation corrections

* Makes use of new param to SubscriptionAPI c'tor

* Removes allow-same-origin

* Addresses review feedback, including removing response

* Fixes unit test

* Adds a forgotten semicolon :(

* Addresses review feedback

* Splits crossDomainIframe functionality out from transport.js into iframe-transport.js (and likewise for unit tests)

* Removes a couple blank lines

* Fixes merge conflict

* Mostly changes comments, removes a small amount of redundant unit test code

* Changes enum to const since all but one enum value no longer used. Removes changes to iframe-helper, makes xframe have allow-same-origin. Moves iframe-transport teardown from amp-analytics.unlayoutCallback to detachedCallback

* Renames things using 3P in name, changes wire format from map to array

* Adds 3p iframe to amp analytics. Uses Subscription API. Event message queue much simpler than previous PR. Response functionality is included, but extraData is currently commented out - will add again before PR

* Amp 3p analytics using SubscriptionAPI, 3p side

* Fixes ampanalytics-lib tests

* Fixes linter issues

* Implements review feedback

* Removes extraData, new creative message

* Removes extraData, new creative message

* Stash

* Changes creative ID to transport ID

* Includes transport ID in example logging, response

* Renamed files/classes to get rid of '3p'

* Makes example ad click URL match parent branch

* Addresses review feedback

* Addresses further review feedback

* extensions/amp-analytics/0.1/amp-analytics.js

* Corrects rebase issue

* Add XSS check to example HTML

* Changes XSS message from log to warning

* Addresses review nits

* Addresses review feedback including removing response temporarily

* Clean up unit tests, rename event datatype

* Resolves rebase issue

* Intentionally adding trivial change, will remove in a minute

* Removing trival change added a minute ago. This is because Git UI is being strange.

* Changes an enum to a const, since all but one enum value have been removed

* Renames things using 3P in name, changes wire format from map to array

* Fixes unit test

* Renamed files/classes to get rid of '3p'

* Fixes some merge issues

* Found typo in method name, which led to renaming that and a few other things

* Adds missing space in error msg

* Addresses review feedback re: renaming files/class, and object field access. Have not yet addressed the comments about IframeMessagingClient nor simplifying to eliminate CreativeEventRouter.

* Switch to IframeMessagingClient, add clarifying comment

* Now uses IframeMessagingClient

* Greatly simplified iframe-transport-client

* Updates unit tests

* Changes whitespace only

* Fixes a broken unit test

* Changes comment only

* Makes lib architecture more similar to ampcontext

* Adds 3p/iframe-transport-client-lib.js

* Differentiates message name, fixes possible null exception

* Trivial change to re-run Percy, which was flakey last time

* Splits request/response message types better

* Moves message type declarations into 3p-frame-messaging.js

* Addresses a few nits
  • Loading branch information
jonkeller authored and keithwrightbos committed Jul 28, 2017
1 parent 6aa61ab commit 53925ed
Show file tree
Hide file tree
Showing 12 changed files with 319 additions and 51 deletions.
37 changes: 37 additions & 0 deletions 3p/iframe-transport-client-lib.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
/**
* 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 {IframeTransportClient} from './iframe-transport-client.js';
import {initLogConstructor, setReportError} from '../src/log';

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

/**
* If window.iframeTransportClient does not exist, we must instantiate and
* assign it to window.iframeTransportClient, to provide the creative with
* all the required functionality.
*/
try {
const iframeTransportClientCreated =
new Event('amp-iframeTransportClientCreated');
window.iframeTransportClient = new IframeTransportClient(window);
window.dispatchEvent(iframeTransportClientCreated);
} catch (err) {
// do nothing with error
}
93 changes: 93 additions & 0 deletions 3p/iframe-transport-client.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
/**
* 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 {tryParseJson} from '../src/json';
import {dev, user} from '../src/log';
import {MessageType} from '../src/3p-frame-messaging';
import {IframeMessagingClient} from './iframe-messaging-client';

/** @private @const {string} */
const TAG_ = 'iframe-transport-client';

/**
* Receives event messages bound for this cross-domain iframe, from all
* creatives
*/
export class IframeTransportClient {

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

/** @private {?function(string,string)} */
this.listener_ = null;

/** @protected {!IframeMessagingClient} */
this.client_ = new IframeMessagingClient(win);
this.client_.setHostWindow(this.win_.parent);
this.client_.setSentinel(user().assertString(
tryParseJson(this.win_.name)['sentinel'],
'Invalid/missing sentinel on iframe name attribute' + this.win_.name));
this.client_.makeRequest(
MessageType.SEND_IFRAME_TRANSPORT_EVENTS,
MessageType.IFRAME_TRANSPORT_EVENTS,
eventData => {
const events =
/**
* @type
* {!Array<../src/3p-frame-messaging.IframeTransportEvent>}
*/
(eventData['events']);
user().assert(events,
'Received malformed events list in ' + this.win_.location.href);
dev().assert(events.length,
'Received empty events list in ' + this.win_.location.href);
user().assert(this.listener_,
'Must call onAnalyticsEvent in ' + this.win_.location.href);
events.forEach(event => {
try {
this.listener_ &&
this.listener_(event.message, event.transportId);
} catch (e) {
user().error(TAG_,
'Exception in callback passed to onAnalyticsEvent: ' +
e.message);
}
});
});
}

/**
* Registers a callback function to be called when an AMP analytics event
* is received.
* Note that calling this a second time will result in the first listener
* being removed - the events will not be sent to both callbacks.
* @param {function(string,string)} callback
*/
onAnalyticsEvent(callback) {
this.listener_ = callback;
}

/**
* Gets the IframeMessagingClient
* @returns {!IframeMessagingClient}
* @VisibleForTesting
*/
getClient() {
return this.client_;
}
}
2 changes: 1 addition & 1 deletion build-system/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -936,7 +936,7 @@ app.get(['/dist/sw.js', '/dist/sw-kill.js', '/dist/ww.js'],
next();
});

app.get('/dist/ampanalytics-lib.js', (req, res, next) => {
app.get('/dist/iframe-transport-client-lib.js', (req, res, next) => {
req.url = req.url.replace(/dist/, 'dist.3p/current');
next();
});
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/**/iframe-transport-client-lib.js',
'!validator/dist/**/*.*',
'!validator/node_modules/**/*.*',
'!validator/nodejs/node_modules/**/*.*',
Expand Down
1 change: 1 addition & 0 deletions build-system/tasks/presubmit-checks.js
Original file line number Diff line number Diff line change
Expand Up @@ -282,6 +282,7 @@ var forbiddenTerms = {
whitelist: [
'3p/integration.js',
'3p/ampcontext-lib.js',
'3p/iframe-transport-client-lib.js',
'ads/alp/install-alp.js',
'ads/inabox/inabox-host.js',
'dist.3p/current/integration.js',
Expand Down
39 changes: 39 additions & 0 deletions examples/analytics-iframe-transport-remote-frame.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Requests Frame</title>
<script>
window.addEventListener('amp-iframeTransportClientCreated', () => {
/**
* To receive AMP analytics events in a third-party frame, you must
* pass a callback function to this method. The callback will be called
* when an event is received, and will be passed two parameters:
* @param {string} event A string of the format specified in the
* requests block of the amp-analytics JSON config
* @param {string} transportId An ID uniquely identifying which creative
* generated the event
*/
window.iframeTransportClient.onAnalyticsEvent(
(event, transportId) => {
// Now, do something meaningful with the AMP Analytics event
console.log('The page at: ' + window.location.href +
' has received an event: ' + event +
' from the creative with transport ID: ' + transportId);
});
});

// Load the script specified in the iframe’s name attribute:
const url = JSON.parse(window.name).scriptSrc;
if (url && url.startsWith('https://3p.ampproject.net/')) {
script = document.createElement('script');
script.src = url;
document.head.appendChild(script);
// The script will be loaded, and will call onNewAmpAnalyticsInstance()
} else {
console.warn('Received invalid URL - risk of XSS! ' + url);
}
</script>
</head>
<!-- The frame will not be visible, so there is no need for a body tag. -->
</html>
15 changes: 5 additions & 10 deletions extensions/amp-analytics/0.1/iframe-transport-message-queue.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,7 @@
*/

import {dev} from '../../../src/log';
import {
IFRAME_TRANSPORT_EVENTS_TYPE,
} from '../../../src/iframe-transport-common';
import {MessageType} from '../../../src/3p-frame-messaging';
import {SubscriptionApi} from '../../../src/iframe-helper';

/** @private @const {string} */
Expand Down Expand Up @@ -48,16 +46,13 @@ export class IframeTransportMessageQueue {

/**
* @private
* {!Array<!../../../src/iframe-transport-common.IframeTransportEvent>}
* {!Array<!../../../src/3p-frame-messaging.IframeTransportEvent>}
*/
this.pendingEvents_ = [];

/** @private {string} */
this.messageType_ = IFRAME_TRANSPORT_EVENTS_TYPE;

/** @private {!../../../src/iframe-helper.SubscriptionApi} */
this.postMessageApi_ = new SubscriptionApi(this.frame_,
this.messageType_,
MessageType.SEND_IFRAME_TRANSPORT_EVENTS,
true,
() => {
this.setIsReady();
Expand Down Expand Up @@ -94,7 +89,7 @@ export class IframeTransportMessageQueue {

/**
* Enqueues an event to be sent to a cross-domain iframe.
* @param {!../../../src/iframe-transport-common.IframeTransportEvent} event
* @param {!../../../src/3p-frame-messaging.IframeTransportEvent} event
* Identifies the event and which Transport instance (essentially which
* creative) is sending it.
*/
Expand All @@ -117,7 +112,7 @@ export class IframeTransportMessageQueue {
*/
flushQueue_() {
if (this.isReady() && this.queueSize()) {
this.postMessageApi_.send(IFRAME_TRANSPORT_EVENTS_TYPE,
this.postMessageApi_.send(MessageType.IFRAME_TRANSPORT_EVENTS,
/** @type {!JsonObject} */
({events: this.pendingEvents_}));
this.pendingEvents_ = [];
Expand Down
5 changes: 3 additions & 2 deletions extensions/amp-analytics/0.1/iframe-transport.js
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,8 @@ export class IframeTransport {
const useLocal = getMode().localDev || getMode().test;
const useRtvVersion = !useLocal;
const scriptSrc = calculateEntryPointScriptUrl(
this.win_.parent.location, 'ampanalytics-lib', useLocal, useRtvVersion);
this.win_.parent.location, 'iframe-transport-client-lib',
useLocal, useRtvVersion);
const frameName = JSON.stringify(/** @type {JsonObject} */ ({
scriptSrc,
sentinel,
Expand Down Expand Up @@ -182,7 +183,7 @@ export class IframeTransport {
dev().assert(frameData.queue, 'Event queue is missing for ' + this.id_);
frameData.queue.enqueue(
/**
* @type {!../../../src/iframe-transport-common.IframeTransportEvent}
* @type {!../../../src/3p-frame-messaging.IframeTransportEvent}
*/
({transportId: this.id_, message: event}));
}
Expand Down
101 changes: 101 additions & 0 deletions extensions/amp-analytics/0.1/test/test-iframe-transport-client.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
/**
* 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 {MessageType} from '../../../../src/3p-frame-messaging';
import {
IframeTransportClient,
} from '../../../../3p/iframe-transport-client';
import {dev, user} from '../../../../src/log';
import {adopt} from '../../../../src/runtime';
import * as sinon from 'sinon';

adopt(window);

let nextId = 5000;
function createUniqueId() {
return String(++(nextId));
}

describe('iframe-transport-client', () => {
let sandbox;
let badAssertsCounterStub;
let iframeTransportClient;
let sentinel;

beforeEach(() => {
sandbox = sinon.sandbox.create();
badAssertsCounterStub = sandbox.stub();
sentinel = createUniqueId();
window.name = '{"sentinel": "' + sentinel + '"}';
iframeTransportClient = new IframeTransportClient(window);
sandbox.stub(dev(), 'assert', (condition, msg) => {
if (!condition) {
badAssertsCounterStub(msg);
}
});
sandbox.stub(user(), 'assert', (condition, msg) => {
if (!condition) {
badAssertsCounterStub(msg);
}
});
});

afterEach(() => {
sandbox.restore();
});

/**
* Sends a message from the current window to itself
* @param {string} type Type of the message.
* @param {!JsonObject} object Message payload.
*/
function send(type, data) {
const object = {};
object['type'] = type;
object['sentinel'] = sentinel;
if (data['events']) {
object['events'] = data['events'];
} else {
object['data'] = data;
}
const payload = 'amp-' + JSON.stringify(object);
window./*OK*/postMessage(payload, '*');
}

it('fails to create iframeTransportClient if no window.name ', () => {
const oldWindowName = window.name;
expect(() => {
window.name = '';
new IframeTransportClient(window);
}).to.throw(/Cannot read property 'sentinel' of undefined/);
window.name = oldWindowName;
});

it('sets sentinel from window.name.sentinel ', () => {
expect(iframeTransportClient.getClient().sentinel_).to.equal(sentinel);
});

it('receives an event message ', () => {
window.processAmpAnalyticsEvent = (event, transportId) => {
expect(transportId).to.equal('101');
expect(event).to.equal('hello, world!');
};
send(MessageType.IFRAME_TRANSPORT_EVENTS, /** @type {!JsonObject} */ ({
events: [
{transportId: '101', message: 'hello, world!'},
]}));
});
});
18 changes: 18 additions & 0 deletions gulpfile.js
Original file line number Diff line number Diff line change
Expand Up @@ -265,6 +265,17 @@ function compile(watch, shouldMinify, opt_preventRemoveAndMakeDir,
include3pDirectories: true,
includePolyfills: false,
}),
compileJs('./3p/', 'iframe-transport-client-lib.js',
'./dist.3p/' + (shouldMinify ? internalRuntimeVersion : 'current'), {
minifiedName: 'iframe-transport-client-v0.js',
checkTypes: opt_checkTypes,
watch: watch,
minify: shouldMinify,
preventRemoveAndMakeDir: opt_preventRemoveAndMakeDir,
externs: ['ads/ads.extern.js',],
include3pDirectories: true,
includePolyfills: false,
}),
// For compilation with babel we start with the amp-babel entry point,
// but then rename to the amp.js which we've been using all along.
compileJs('./src/', 'amp-babel.js', './dist', {
Expand Down Expand Up @@ -668,6 +679,13 @@ function checkTypes() {
includePolyfills: true,
checkTypes: true,
}),
closureCompile(['./3p/iframe-transport-client-lib.js'], './dist',
'iframe-transport-client-check-types.js', {
externs: ['ads/ads.extern.js'],
include3pDirectories: true,
includePolyfills: true,
checkTypes: true,
}),
]);
});
}
Expand Down

0 comments on commit 53925ed

Please sign in to comment.