Skip to content

Commit

Permalink
Track impression on amp landing page (#5606)
Browse files Browse the repository at this point in the history
* wip

* get response back from proxy server

* first impl

* another approach

* fix

* fix type check

* wip

* add getter

* add getter to sourceUrl

* address comments, add timeout test

* test fix

* small fix

* use getter method
  • Loading branch information
zhouyx committed Oct 28, 2016
1 parent d727bdc commit aa205dc
Show file tree
Hide file tree
Showing 8 changed files with 301 additions and 21 deletions.
15 changes: 15 additions & 0 deletions build-system/server.js
Expand Up @@ -426,6 +426,21 @@ app.use('/examples/amp-fresh.amp.(min.|max.)?html', function(req, res, next) {
next();
});


app.use('/impression-proxy/', function(req, res) {
assertCors(req, res, ['GET']);
// Fake response with the following optional fields:
// location: The Url the that server would have sent redirect to w/o ALP
// tracking_url: URL that should be requested to track click
// gclid: The conversion tracking value
const body = {
'location': 'localhost:8000/examples/?gclid=1234&foo=bar&example=123',
'tracking_url': 'tracking_url',
'gclid': '1234',
};
res.send(body);
});

// Proxy with unminified JS.
// Example:
// http://localhost:8000/max/s/www.washingtonpost.com/amphtml/news/post-politics/wp/2016/02/21/bernie-sanders-says-lower-turnout-contributed-to-his-nevada-loss-to-hillary-clinton/
Expand Down
12 changes: 9 additions & 3 deletions src/document-info.js
Expand Up @@ -44,8 +44,6 @@ export let DocumentInfoDef;
export function documentInfoForDoc(nodeOrDoc) {
return /** @type {!DocumentInfoDef} */ (getServiceForDoc(nodeOrDoc,
'documentInfo', ampdoc => {
const url = ampdoc.getUrl();
const sourceUrl = getSourceUrl(url);
const rootNode = ampdoc.getRootNode();
let canonicalUrl = rootNode && rootNode.AMP
&& rootNode.AMP.canonicalUrl;
Expand All @@ -56,11 +54,19 @@ export function documentInfoForDoc(nodeOrDoc) {
canonicalUrl = parseUrl(canonicalTag.href).href;
}
const pageViewId = getPageViewId(ampdoc.win);
return {url, sourceUrl, canonicalUrl, pageViewId};
const res = {
get sourceUrl() {
return getSourceUrl(ampdoc.getUrl());
},
canonicalUrl,
pageViewId,
};
return res;
}));
}



/**
* Returns a relatively low entropy random string.
* This should be called once per window and then cached for subsequent
Expand Down
94 changes: 90 additions & 4 deletions src/impression.js
Expand Up @@ -14,31 +14,69 @@
* limitations under the License.
*/

import {user} from './log';
import {dev, user} from './log';
import {isExperimentOn} from './experiments';
import {viewerForDoc} from './viewer';
import {xhrFor} from './xhr';
import {
isProxyOrigin,
parseUrl,
parseQueryString,
addParamsToUrl,
} from './url';
import {timerFor} from './timer';
import {getMode} from './mode';

const TIMEOUT_VALUE = 8000;

let trackImpressionPromise = null;

/**
* A function to get the trackImpressionPromise;
* @return {!Promise}
*/
export function getTrackImpressionPromise() {
return dev().assert(trackImpressionPromise);
}

/**
* Function that reset the trackImpressionPromise only for testing
* @visibleForTesting
*/
export function resetTrackImpressionPromiseForTesting() {
trackImpressionPromise = null;
}

/**
* Emit a HTTP request to a destination defined on the incoming URL.
* Protected by experiment.
* @param {!Window} win
*/
export function maybeTrackImpression(win) {
let resolveImpression;

trackImpressionPromise = new Promise(resolve => {
resolveImpression = resolve;
});

if (!isExperimentOn(win, 'alp')) {
resolveImpression();
return;
}

const viewer = viewerForDoc(win.document);
/** @const {string|undefined} */
const clickUrl = viewer.getParam('click');

if (!clickUrl) {
resolveImpression();
return;
}
if (clickUrl.indexOf('https://') != 0) {
user().warn('Impression',
'click fragment param should start with https://. Found ',
clickUrl);
resolveImpression();
return;
}
if (win.location.hash) {
Expand All @@ -47,15 +85,63 @@ export function maybeTrackImpression(win) {
// avoid duplicate tracking.
win.location.hash = '';
}

viewer.whenFirstVisible().then(() => {
invoke(win, clickUrl);
// TODO(@zhouyx) need test with a real response.
const promise = invoke(win, dev().assertString(clickUrl)).then(response => {
applyResponse(win, viewer, response);
});

// Timeout invoke promise after 8s and resolve trackImpressionPromise.
resolveImpression(timerFor(win).timeoutPromise(TIMEOUT_VALUE, promise,
'timeout waiting for ad server response').catch(() => {}));
});
}

/**
* Send the url to ad server and wait for its response
* @param {!Window} win
* @param {string} clickUrl
* @return {!Promise<!JSONType>}
*/
function invoke(win, clickUrl) {
xhrFor(win).fetchJson(clickUrl, {
if (getMode().localDev && !getMode().test) {
clickUrl = 'http://localhost:8000/impression-proxy?url=' + clickUrl;
}
return xhrFor(win).fetchJson(clickUrl, {
credentials: 'include',
requireAmpResponseSourceOrigin: true,
});
// TODO(@cramforce): Do something with the result.
}

/**
* parse the response back from ad server
* Set for analytics purposes
* @param {!Window} win
* @param {!Object} response
*/
function applyResponse(win, viewer, response) {
const adLocation = response['location'];
const adTracking = response['tracking_url'];

// If there is a tracking_url, need to track it
// Otherwise track the location
const trackUrl = adTracking || adLocation;

if (trackUrl && !isProxyOrigin(trackUrl)) {
// To request the provided trackUrl for tracking purposes.
new Image().src = trackUrl;
}

// Replace the location href params with new location params we get.
if (adLocation) {
if (!win.history.replaceState) {
return;
}
const currentHref = win.location.href;
const url = parseUrl(adLocation);
const params = parseQueryString(url.search);
const newHref = addParamsToUrl(currentHref, params);
win.history.replaceState(null, '', newHref);
}
}
40 changes: 32 additions & 8 deletions src/service/url-replacements-impl.js
Expand Up @@ -29,6 +29,7 @@ import {viewportForDoc} from '../viewport';
import {userNotificationManagerFor} from '../user-notification';
import {activityFor} from '../activity';
import {isExperimentOn} from '../experiments';
import {getTrackImpressionPromise} from '../impression.js';


/** @private @const {string} */
Expand Down Expand Up @@ -163,6 +164,14 @@ export class UrlReplacements {
return removeFragment(info.sourceUrl);
}));

this.setAsync_('SOURCE_URL', () => {
return getTrackImpressionPromise().then(() => {
return this.getDocInfoValue_(info => {
return removeFragment(info.sourceUrl);
});
});
});

// Returns the host of the Source URL for this AMP document.
this.set_('SOURCE_HOST', this.getDocInfoValue_.bind(this, info => {
return parseUrl(info.sourceUrl).host;
Expand All @@ -186,15 +195,13 @@ export class UrlReplacements {
}));

this.set_('QUERY_PARAM', (param, defaultValue = '') => {
user().assert(param,
'The first argument to QUERY_PARAM, the query string ' +
'param is required');
const url = parseUrl(this.ampdoc.win.location.href);
const params = parseQueryString(url.search);
return this.getQueryParamData_(param, defaultValue);
});

return (typeof params[param] !== 'undefined') ?
params[param] :
defaultValue;
this.setAsync_('QUERY_PARAM', (param, defaultValue = '') => {
return getTrackImpressionPromise().then(() => {
return this.getQueryParamData_(param, defaultValue);
});
});

/**
Expand Down Expand Up @@ -540,6 +547,23 @@ export class UrlReplacements {
return navigationInfo[attribute];
}

/**
* Return the QUERY_PARAM from the current location href
* @param {*} param
* @param {string} defaultValue
* @return {string}
*/
getQueryParamData_(param, defaultValue) {
user().assert(param,
'The first argument to QUERY_PARAM, the query string ' +
'param is required');
user().assert(typeof param == 'string', 'param should be a string');
const url = parseUrl(this.ampdoc.win.location.href);
const params = parseQueryString(url.search);
return (typeof params[param] !== 'undefined')
? params[param] : defaultValue;
}

/**
*
* Sets a synchronous value resolver for the variable with the specified name.
Expand Down
1 change: 0 additions & 1 deletion src/service/xhr-impl.js
Expand Up @@ -162,7 +162,6 @@ export class Xhr {
const init = opt_init || {};
init.method = normalizeMethod_(init.method);
setupJson_(init);

return this.fetchAmpCors_(input, init).then(response => {
return assertSuccess(response);
}).then(response => response.json());
Expand Down
22 changes: 20 additions & 2 deletions test/functional/test-document-info.js
Expand Up @@ -63,12 +63,30 @@ describe('document-info', () => {
};
win.document.defaultView = win;
installDocService(win, true);
expect(documentInfoForDoc(win.document).url).to.equal(
'https://cdn.ampproject.org/v/www.origin.com/foo/?f=0');
expect(documentInfoForDoc(win.document).sourceUrl).to.equal(
'http://www.origin.com/foo/?f=0');
});

it('should provide the updated sourceUrl', () => {
const win = {
document: {
nodeType: /* document */ 9,
querySelector() { return 'http://www.origin.com/foo/?f=0'; },
},
Math: {random() { return 0.123456789; }},
location: {
href: 'https://cdn.ampproject.org/v/www.origin.com/foo/?f=0',
},
};
win.document.defaultView = win;
installDocService(win, true);
expect(documentInfoForDoc(win.document).sourceUrl).to.equal(
'http://www.origin.com/foo/?f=0');
win.location.href = 'https://cdn.ampproject.org/v/www.origin.com/foo/?f=1';
expect(documentInfoForDoc(win.document).sourceUrl).to.equal(
'http://www.origin.com/foo/?f=1');
});

it('should provide the pageViewId', () => {
return getWin('https://twitter.com/').then(win => {
sandbox.stub(win.Math, 'random', () => 0.123456789);
Expand Down

0 comments on commit aa205dc

Please sign in to comment.