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 -// ] -