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

Introduce iframe transport to amp-analytics - 3p side #10596

Merged
merged 85 commits into from
Jul 28, 2017
Merged

Introduce iframe transport to amp-analytics - 3p side #10596

merged 85 commits into from
Jul 28, 2017

Conversation

jonkeller
Copy link
Contributor

This is the 3p-side companion to the amp analytics impl PR at #10442 and thus a simplified version of #10184

It is the result of rebasing the code that was at https://github.com/google/amphtml/pull/20 over to ampproject/amphtml

… queue much simpler than previous PR. Response functionality is included, but extraData is currently commented out - will add again before PR
… relying on omitting sandbox allow-same-origin
@@ -0,0 +1,264 @@
/**
Copy link
Contributor

Choose a reason for hiding this comment

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

how about iframe-transport-client.js?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Can do, if you think this will be more meaningful to customers. (Note that I am taking a vacation day Tuesday 7/25.)

Copy link
Contributor

Choose a reason for hiding this comment

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

this name is internal in our repo only, so it's a good idea to keep it consistent with other naming.
we can definitely use a different name for the compiled library (maybe including ampanalytics in the name).

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I like this idea. Done.

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

Choose a reason for hiding this comment

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

IframeTransportClient

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Will do

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

* @param messageType The type of message to subscribe to
* @VisibleForTesting
*/
subscribeTo(messageType) {
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 use IframeMessagingClient?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I'm a bit confused here. You said to change from IframeMessagingClient to SubscriptionAPI. Do you mean to have one side of the conversation using IframeMessagingClient, and the other side using SubscriptionAPI? I can research whether that will work, but want to make sure I understand you correctly.

Copy link
Contributor

Choose a reason for hiding this comment

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

sorry if i didn't make it clear.
SubscriptionApi and IframeMessagingClient is a pair. the former is installed in host page, the latter is for the iframe client.

that's how it works for our 3P ads 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.

Done

'Must implement onNewAmpAnalyticsInstance in ' +
this.win_.location.href);
events.forEach(event => {
const transportId = event['transportId'];
Copy link
Contributor

Choose a reason for hiding this comment

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

to leverage the typecheck : event.transportid

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Good idea - will do.

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

console.log("The page at: " + window.location.href +
" has received an event: " + event +
" from the creative with transport ID: " +
ampAnalytics.getTransportId());
Copy link
Contributor

Choose a reason for hiding this comment

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

so this onNewAmpAnalyticsInstance will be called for every transport?
I propose a simpler API:

function processAnalyticsEvent(event) {
  // vendor code goes here ...
}

const url = JSON.parse(window.name).scriptSrc;
if (url && url.startsWith('https://3p.ampproject.net/')) {
  loadScript(url).then(() => {  // see 3p.js
    window.context.onAnalyticsEvent(processAnalyticsEvent.bind(null));
  });
}

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yes, it contains the transportId as state. This way, if 2 creatives use the same vendor, we will not confuse events from one creative with events from the other. I fear that your proposal would force the vendor to do the work of keeping events from different creatives separated.
Can't call loadScript() in 3p from here: to do so, we would have to compile it into the JS lib, and this is the code to load that lib, so it would be a chicken-and-egg problem. But, my code to createElement(), set src, and then appendChild() is identical to what is in loadScript(), minus the callbacks which I don't have a use for currently.
Also, window context API is not being used.

Copy link
Contributor

Choose a reason for hiding this comment

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

would force the vendor to do the work of keeping events from different creatives separated

I don't feel we should do that much for the vendor. The reason we have this client lib with sacrificing of speed is that we don't want to expose the raw postMessage API to them (which is hard to migrate once vendors start to depend on it). So the only job of the client lib is to convert raw message to a js friendly object.

I want to keep the API simple and flexible. We simply flush the events to the client, each event has the transport info. And vendors can have their own design about how to passing the creative IDs along with the event. And it's their job how to group them / process them.

Can't call loadScript() in 3p from here

I was not talking about import the code here. you can copy-paste the impl 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.

Ok, I will work to simplify this. If you don't mind, I'll still leave my assert()s though.
Re: loadScript(), my current implementation is essentially the same, minus the callbacks. loadScript():

export function loadScript(win, url, opt_cb, opt_errorCb) {
  /** @const {!Element} */
  const s = win.document.createElement('script');
  s.src = url;
  if (opt_cb) {
    s.onload = opt_cb;
  }
  if (opt_errorCb) {
    s.onerror = opt_errorCb;
  }
  win.document.body.appendChild(s);
}

Mine:

  script = document.createElement('script');
  script.src = url;
  document.head.appendChild(script);

I'll switch that last line from document.head to document.body, though.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Simplification is done. Did not end up changing from "document.head" to "document.body" since the remote frame has no content and thus no body.

* Receives messages bound for this cross-domain iframe, from a particular
* creative.
*/
export class CreativeEventRouter {
Copy link
Contributor

Choose a reason for hiding this comment

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

why is this class necessary?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Currently CreativeEventRouter provides an association between a creative (transport) and the customer-provided listener function, and EventRouter handles communication with the parent frame (for any/all transports). Therefore the function passed to registerCreativeEventListener() is specific to one creative.

Copy link
Contributor

Choose a reason for hiding this comment

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

did we normalized all events into one type at host side already?

<title>Requests Frame</title>
<script>
/**
* To receive analytics events in a third-party frame, you must
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Comment is outdated

* @param ampAnalytics Call registerAmpAnalyticsEventListener() on this,
* passing your function which will receive the analytics events.
*/
window.processAmpAnalyticsEvent = (event, transportId) => {
Copy link
Contributor

Choose a reason for hiding this comment

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

thanks for simplifying. but this is a bit different from what I proposed.
i was thinking of exposing the API as window.context.onAnalyticsEvent(callback)

  1. more consistent with our ampcontext api
  2. this gives 3p code to decide when to call the api, as they might be loading something necessary asynchronously.

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, back to you.

}
}

if (!window.AMP_TEST) {
Copy link
Contributor

Choose a reason for hiding this comment

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

we can follow ampcontext-lib.js to split this piece out of this file.

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

<meta charset="UTF-8">
<title>Requests Frame</title>
<script>
window.addEventListener('amp-iframeTransportClientCreated', function() {
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Changed to resemble:

window.addEventListener('amp-windowContextCreated', function(){

* assign it to window.iframeTransportClient, to provide the creative with
* all the required functionality.
*/
try {
Copy link
Contributor Author

Choose a reason for hiding this comment

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

'Invalid/missing sentinel on iframe name attribute' + this.win_.name));
this.client_.makeRequest(
IFRAME_TRANSPORT_EVENTS_TYPE,
IFRAME_TRANSPORT_EVENTS_TYPE,
Copy link
Contributor

Choose a reason for hiding this comment

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

back to my previous comment, i think it make sense to have 2 different message type

SEND_IFRAME_TRANSPORT_EVENTS,
IFRAME_TRANSPORT_EVENTS

naming is after 3p-frame-messaging.js

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Fixed by creating constants with different names, but same value. See:
https://github.com/ampproject/amphtml/blob/master/src/iframe-helper.js#L217
https://github.com/ampproject/amphtml/blob/master/src/iframe-helper.js#L265
...for why values must be the same.
Just to be safe: Tested with same values, works. Tested with different values, does not work.

Copy link
Contributor

Choose a reason for hiding this comment

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

Tested with different values, does not work.

That does not sound right. Can you investigate?

Copy link
Contributor Author

@jonkeller jonkeller Jul 28, 2017

Choose a reason for hiding this comment

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

I believe this makes sense. In the class at https://github.com/ampproject/amphtml/blob/master/src/iframe-helper.js#L412, subscribers send a message of a certain type. That is stored in this.clientWindows_
Later, when send() is called (line 446), it looks at this.clientWindows_ to see who wants messages of that type, and sends it to them.
I don't see how it could work with the request/response message types being different. If you send a subscription request for message type X, it wouldn't/shouldn't know to send you message type Y.

Copy link
Contributor

Choose a reason for hiding this comment

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

All the current usage of IframeMessagingClient#makeRequest takes different type: https://github.com/ampproject/amphtml/blob/master/3p/ampcontext.js#L158

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Fixed, to resemble code in provided link

'Must call onAnalyticsEvent in ' + this.win_.location.href);
events.forEach(event => {
try {
this.listener_(event.message, event.transportId);
Copy link
Contributor

Choose a reason for hiding this comment

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

listener_ can be null

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Fixed.

@@ -19,9 +19,16 @@
* 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';
export const IFRAME_TRANSPORT_EVENTS = 'iframe-transport-events';
Copy link
Contributor

Choose a reason for hiding this comment

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

actually, it probably makes sense to merge this whole file into 3p-frame-messaging.js now

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Ok, I can do that. Thanks!

/** @protected {!IframeMessagingClient} */
this.client_ = new IframeMessagingClient(win);
this.client_.setHostWindow(this.win_.parent);
this.client_.setSentinel(dev().assertString(
Copy link
Contributor

Choose a reason for hiding this comment

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

Should this be user() instead of dev()? If it fails, I assume we cannot recover?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

<meta charset="UTF-8">
<title>Requests Frame</title>
<script>
window.addEventListener('amp-iframeTransportClientCreated', function() {
Copy link
Contributor

Choose a reason for hiding this comment

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

Any reason not to use big arrow notation? (e.g. () => {...})

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Agree with this, too. Will leave myself a note to also change

window.addEventListener('amp-windowContextCreated', function(){
after fixit week.

window.iframeTransportClient.onAnalyticsEvent(
(event, transportId) => {
// Now, do something meaningful with the AMP Analytics event
console.log("The page at: " + window.location.href +
Copy link
Contributor

Choose a reason for hiding this comment

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

Single quotes?

Copy link
Contributor

@lannka lannka left a comment

Choose a reason for hiding this comment

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

LGTM
thanks for the change.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants