diff --git a/3p/iframe-transport-client-lib.js b/3p/iframe-transport-client-lib.js
new file mode 100644
index 000000000000..37c7909b3930
--- /dev/null
+++ b/3p/iframe-transport-client-lib.js
@@ -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
+}
diff --git a/3p/iframe-transport-client.js b/3p/iframe-transport-client.js
new file mode 100644
index 000000000000..36d3b2e0ef94
--- /dev/null
+++ b/3p/iframe-transport-client.js
@@ -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_;
+ }
+}
diff --git a/build-system/app.js b/build-system/app.js
index ca50bb95c1d0..21a84f6a45e9 100644
--- a/build-system/app.js
+++ b/build-system/app.js
@@ -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();
});
diff --git a/build-system/config.js b/build-system/config.js
index 56798217ae13..5ce5256099c4 100644
--- a/build-system/config.js
+++ b/build-system/config.js
@@ -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/**/*.*',
diff --git a/build-system/tasks/presubmit-checks.js b/build-system/tasks/presubmit-checks.js
index 21f61cdec30f..ee015fe8b89d 100644
--- a/build-system/tasks/presubmit-checks.js
+++ b/build-system/tasks/presubmit-checks.js
@@ -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',
diff --git a/examples/analytics-iframe-transport-remote-frame.html b/examples/analytics-iframe-transport-remote-frame.html
new file mode 100644
index 000000000000..8376c7bbf598
--- /dev/null
+++ b/examples/analytics-iframe-transport-remote-frame.html
@@ -0,0 +1,39 @@
+
+
+
+
+ Requests Frame
+
+
+
+
diff --git a/extensions/amp-analytics/0.1/amp-analytics.js b/extensions/amp-analytics/0.1/amp-analytics.js
index b751be5686f9..783012a52d4c 100644
--- a/extensions/amp-analytics/0.1/amp-analytics.js
+++ b/extensions/amp-analytics/0.1/amp-analytics.js
@@ -419,7 +419,7 @@ export class AmpAnalytics extends AMP.BaseElement {
// TODO(zhouyx, #7096) Track overwrite percentage. Prevent transport overwriting
if (inlineConfig['transport'] || this.remoteConfig_['transport']) {
const TAG = this.getName_();
- user().error(TAG, 'Inline or remote config should not' +
+ user().error(TAG, 'Inline or remote config should not ' +
'overwrite vendor transport settings');
}
}
diff --git a/extensions/amp-analytics/0.1/iframe-transport-message-queue.js b/extensions/amp-analytics/0.1/iframe-transport-message-queue.js
index e1d4723e7950..068a8f86b5b2 100644
--- a/extensions/amp-analytics/0.1/iframe-transport-message-queue.js
+++ b/extensions/amp-analytics/0.1/iframe-transport-message-queue.js
@@ -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} */
@@ -48,16 +46,13 @@ export class IframeTransportMessageQueue {
/**
* @private
- * {!Array}
+ * {!Array}
*/
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();
@@ -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.
*/
@@ -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_ = [];
diff --git a/extensions/amp-analytics/0.1/iframe-transport.js b/extensions/amp-analytics/0.1/iframe-transport.js
index 217b85983211..6d7a94746119 100644
--- a/extensions/amp-analytics/0.1/iframe-transport.js
+++ b/extensions/amp-analytics/0.1/iframe-transport.js
@@ -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,
@@ -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}));
}
diff --git a/extensions/amp-analytics/0.1/test/test-iframe-transport-client.js b/extensions/amp-analytics/0.1/test/test-iframe-transport-client.js
new file mode 100644
index 000000000000..090b9c5f6aa0
--- /dev/null
+++ b/extensions/amp-analytics/0.1/test/test-iframe-transport-client.js
@@ -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!'},
+ ]}));
+ });
+});
diff --git a/gulpfile.js b/gulpfile.js
index 7924c8624c1d..eb6caf9dd19b 100644
--- a/gulpfile.js
+++ b/gulpfile.js
@@ -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', {
@@ -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,
+ }),
]);
});
}
diff --git a/src/3p-frame-messaging.js b/src/3p-frame-messaging.js
index 21f091d2c1b3..8ab66e12f2b0 100644
--- a/src/3p-frame-messaging.js
+++ b/src/3p-frame-messaging.js
@@ -47,6 +47,10 @@ export const MessageType = {
// For amp-inabox
SEND_POSITIONS: 'send-positions',
POSITION: 'position',
+
+ // For amp-analytics' iframe-transport
+ SEND_IFRAME_TRANSPORT_EVENTS: 'send-iframe-transport-events',
+ IFRAME_TRANSPORT_EVENTS: 'iframe-transport-events',
};
/**
@@ -114,3 +118,19 @@ export function isAmpMessage(message) {
message.indexOf(AMP_MESSAGE_PREFIX) == 0 &&
message.indexOf('{') != -1);
}
+
+/** @typedef {{transportId: string, message: string}} */
+export let IframeTransportEvent;
+// An event, and the transport ID of the amp-analytics tags that
+// generated it. For instance if the creative with transport
+// ID 2 sends "hi", then an IframeTransportEvent would look like:
+// { transportId: "2", message: "hi" }
+// If the creative with transport ID 2 sent that, and also sent "hello",
+// and the creative with transport ID 3 sends "goodbye" then an *array* of 3
+// AmpAnalyticsIframeTransportEvent would be sent to the 3p frame like so:
+// [
+// { transportId: "2", message: "hi" }, // An AmpAnalyticsIframeTransportEvent
+// { transportId: "2", message: "hello" }, // Another
+// { transportId: "3", message: "goodbye" } // And another
+// ]
+
diff --git a/src/iframe-transport-common.js b/src/iframe-transport-common.js
deleted file mode 100644
index 83209bd32f23..000000000000
--- a/src/iframe-transport-common.js
+++ /dev/null
@@ -1,38 +0,0 @@
-/**
- * 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.
- */
-
-/**
- * @const {string}
- * This is the type of message that will be sent to the 3p frame.
- * The message will contain an array of the typedef declared below.
- */
-export const IFRAME_TRANSPORT_EVENTS_TYPE = 'IframeTransportEvents';
-
-/** @typedef {Object} */
-export let IframeTransportEvent;
-// An event, and the transport ID of the amp-analytics tags that
-// generated it. For instance if the creative with transport
-// ID 2 sends "hi", then an IframeTransportEvent would look like:
-// { transportId: "2", message: "hi" }
-// If the creative with transport ID 2 sent that, and also sent "hello",
-// and the creative with transport ID 3 sends "goodbye" then an *array* of 3
-// AmpAnalyticsIframeTransportEvent would be sent to the 3p frame like so:
-// [
-// { transportId: "2", message: "hi" }, // An AmpAnalyticsIframeTransportEvent
-// { transportId: "2", message: "hello" }, // Another
-// { transportId: "3", message: "goodbye" } // And another
-// ]
-