Skip to content

Commit

Permalink
Added initial version of amp-anlaytics variable filters. (ampproject#…
Browse files Browse the repository at this point in the history
…5621)

* Added initial version of amp-anlaytics variable filters.

Fixes ampproject#2198
  • Loading branch information
avimehta authored and Vanessa Pasque committed Dec 22, 2016
1 parent be1049f commit 8f9f715
Show file tree
Hide file tree
Showing 7 changed files with 551 additions and 153 deletions.
9 changes: 8 additions & 1 deletion examples/analytics.amp.html
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@
"transport": {"beacon": false, "xhrpost": false},
"requests": {
"endpoint": "https://raw.githubusercontent.com/ampproject/amphtml/master/examples/img/ampicon.png",
"base": "${endpoint}?${type}&path=${canonicalPath}",
"base": "${endpoint}?${type|default:foo}&path=${canonicalPath}",
"event": "${base}&scrollY=${scrollTop}&scrollX=${scrollLeft}&height=${availableScreenHeight}&width=${availableScreenWidth}&scrollBoundV=${verticalScrollBoundary}&scrollBoundH=${horizontalScrollBoundary}",
"visibility": "${base}&a=${maxContinuousVisibleTime}&b=${totalVisibleTime}&c=${firstSeenTime}&d=${lastSeenTime}&e=${fistVisibleTime}&f=${lastVisibleTime}&g=${minVisiblePercentage}&h=${maxVisiblePercentage}&i=${elementX}&j=${elementY}&k=${elementWidth}&l=${elementHeight}&m=${totalTime}&n=${loadTimeVisibility}&o=${backgroundedAtStart}&p=${backgrounded}&subTitle=${subTitle}",
"timer": "${base}&backgroundState=${backgroundState}"
Expand All @@ -60,6 +60,13 @@
"param1": "Another value"
}
},
"pageview": {
"on": "visible",
"request": "base",
"vars": {
"type": "${random}${title}"
}
},
"visibility": {
"on": "visible",
"request": "visibility",
Expand Down
160 changes: 57 additions & 103 deletions extensions/amp-analytics/0.1/amp-analytics.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ import {isJsonScriptTag} from '../../../src/dom';
import {assertHttpsUrl, appendEncodedParamStringToUrl} from '../../../src/url';
import {dev, user} from '../../../src/log';
import {expandTemplate} from '../../../src/string';
import {isArray, isObject} from '../../../src/types';
import {isArray, isObject, map} from '../../../src/types';
import {sendRequest, sendRequestUsingIframe} from './transport';
import {urlReplacementsForDoc} from '../../../src/url-replacements';
import {userNotificationManagerFor} from '../../../src/user-notification';
Expand All @@ -32,6 +32,7 @@ import {
InstrumentationService,
instrumentationServiceForDoc,
} from './instrumentation';
import {ExpansionOptions, variableServiceFor} from './variables';
import {ANALYTICS_CONFIG} from './vendors';

// Register doc-service factory.
Expand All @@ -41,6 +42,7 @@ AMP.registerServiceForDoc('activity', Activity);

installCidService(AMP.win);
installCryptoService(AMP.win);
variableServiceFor(AMP.win);

const MAX_REPLACES = 16; // The maximum number of entries in a extraUrlParamsReplaceMap

Expand Down Expand Up @@ -90,6 +92,9 @@ export class AmpAnalytics extends AMP.BaseElement {

/** @private {?./instrumentation.InstrumentationService} */
this.instrumentation_ = null;

/** @private {!./variables.VariableService} */
this.variableService_ = variableServiceFor(this.win);
}

/** @override */
Expand Down Expand Up @@ -167,8 +172,9 @@ export class AmpAnalytics extends AMP.BaseElement {
// Trigger callback can be synchronous. Do the registration at the end.
for (const k in this.config_['triggers']) {
if (this.config_['triggers'].hasOwnProperty(k)) {
let trigger = null;
trigger = this.config_['triggers'][k];
const trigger = this.config_['triggers'][k];
const expansionOptions = this.expansionOptions_(
{}, trigger, undefined, true);
const TAG = this.getName_();
if (!trigger) {
user().error(TAG, 'Trigger should be an object: ', k);
Expand All @@ -188,11 +194,14 @@ export class AmpAnalytics extends AMP.BaseElement {

if (trigger['selector']) {
// Expand the selector using variable expansion.
trigger['selector'] = this.expandTemplate_(trigger['selector'],
trigger, /* arg*/ undefined, /* arg */ undefined,
/* arg*/ false);
this.instrumentation_.addListener(
trigger, this.handleEvent_.bind(this, trigger), this.element);
return this.variableService_.expandTemplate(
trigger['selector'], expansionOptions)
.then(selector => {
trigger['selector'] = selector;
this.instrumentation_.addListener(
trigger, this.handleEvent_.bind(this, trigger),
this.element);
});
} else {
this.instrumentation_.addListener(
trigger, this.handleEvent_.bind(this, trigger), this.element);
Expand Down Expand Up @@ -421,28 +430,36 @@ export class AmpAnalytics extends AMP.BaseElement {
return Promise.resolve();
}

const requestPromises = [];
const params = map();
// Add any given extraUrlParams as query string param
if (this.config_['extraUrlParams'] || trigger['extraUrlParams']) {
const params = Object.create(null);
const expansionOptions = this.expansionOptions_(event, trigger);
Object.assign(params, this.config_['extraUrlParams'],
trigger['extraUrlParams']);
for (const k in params) {
if (typeof params[k] == 'string') {
params[k] = this.expandTemplate_(params[k], trigger, event);
requestPromises.push(
this.variableService_.expandTemplate(params[k], expansionOptions)
.then(value => { params[k] = value; }));
}
}
request = this.addParamsToUrl_(request, params);
}

this.config_['vars']['requestCount']++;
request = this.expandTemplate_(request, trigger, event);

// For consistency with amp-pixel we also expand any url replacements.
return urlReplacementsForDoc(this.element).expandAsync(request)
.then(request => {
this.sendRequest_(request, trigger);
return request;
});
return Promise.all(requestPromises)
.then(() => {
request = this.addParamsToUrl_(request, params);
this.config_['vars']['requestCount']++;
const expansionOptions = this.expansionOptions_(event, trigger);
return this.variableService_.expandTemplate(request, expansionOptions);
})
.then(request =>
// For consistency with amp-pixel we also expand any url replacements.
urlReplacementsForDoc(this.element).expandAsync(request))
.then(request => {
this.sendRequest_(request, trigger);
return request;
});
}

/**
Expand All @@ -465,9 +482,9 @@ export class AmpAnalytics extends AMP.BaseElement {
}
const threshold = parseFloat(spec['threshold']); // Threshold can be NaN.
if (threshold >= 0 && threshold <= 100) {
const key = this.expandTemplate_(spec['sampleOn'], trigger);
const keyPromise = urlReplacementsForDoc(this.element)
.expandAsync(key);
const keyPromise = this.variableService_.expandTemplate(
spec['sampleOn'], this.expansionOptions_({}, trigger))
.then(key => urlReplacementsForDoc(this.element).expandAsync(key));
const cryptoPromise = cryptoFor(this.win);
return Promise.all([keyPromise, cryptoPromise])
.then(results => results[1].uniform(results[0]))
Expand All @@ -477,84 +494,6 @@ export class AmpAnalytics extends AMP.BaseElement {
return resolve;
}

/**
* @param {string} template The template to expand.
* @param {!JSONType} trigger The object to use for variable value lookups.
* @param {!Object=} opt_event Object with details about the event.
* @param {number=} opt_iterations Number of recursive expansions to perform.
* Defaults to 2 substitutions.
* @param {boolean=} opt_encode Used to determine if the vars should be
* encoded or not. Defaults to true.
* @return {string} The expanded string.
* @private
*/
expandTemplate_(template, trigger, opt_event, opt_iterations, opt_encode) {
opt_iterations = opt_iterations === undefined ? 2 : opt_iterations;
opt_encode = opt_encode === undefined ? true : opt_encode;
if (opt_iterations < 0) {
user().error('AMP-ANALYTICS', 'Maximum depth reached while expanding ' +
'variables. Please ensure that the variables are not recursive.');
return template;
}

// Replace placeholders with URI encoded values.
// Precedence is opt_event.vars > trigger.vars > config.vars.
// Nested expansion not supported.
return expandTemplate(template, key => {
const {name, argList} = this.getNameArgs_(key);
let raw = (opt_event && opt_event['vars'] && opt_event['vars'][name]) ||
(trigger['vars'] && trigger['vars'][name]) ||
(this.config_['vars'] && this.config_['vars'][name]) ||
'';

// Values can also be arrays and objects. Don't expand them.
if (typeof raw == 'string') {
raw = this.expandTemplate_(raw, trigger, opt_event, opt_iterations - 1);
}
const val = opt_encode ? this.encodeVars_(raw, name) : raw;
return val ? val + argList : val;
});
}

/**
* Returns an array containing two values: name and args parsed from the key.
*
* @param {string} key The key to be parsed.
* @return {!Object<string>}
* @private
*/
getNameArgs_(key) {
if (!key) {
return {name: '', argList: ''};
}
const match = key.match(/([^(]*)(\([^)]*\))?/);
if (!match) {
const TAG = this.getName_();
user().error(TAG,
'Variable with invalid format found: ' + key);
}
return {name: match[1], argList: match[2] || ''};
}

/**
* @param {string|!Array<string>} raw The values to URI encode.
* @param {string} unusedName Name of the variable.
* @return {string} The encoded value.
* @private
*/
encodeVars_(raw, unusedName) {
if (!raw) {
return '';
}

if (isArray(raw)) {
return raw.map(encodeURIComponent).join(',');
}
// Separate out names and arguments from the value and encode the value.
const {name, argList} = this.getNameArgs_(String(raw));
return encodeURIComponent(name) + argList;
}

/**
* Adds parameters to URL. Similar to the function defined in url.js but with
* a different encoding method.
Expand All @@ -570,12 +509,12 @@ export class AmpAnalytics extends AMP.BaseElement {
if (v == null) {
continue;
} else {
const sv = this.encodeVars_(v, k);
const sv = this.variableService_.encodeVars(v, k);
s.push(`${encodeURIComponent(k)}=${sv}`);
}
}

const paramString = s.join('&');
const paramString = s.length > 0 ? s.join('&') : '';
if (request.indexOf('${extraUrlParams}') >= 0) {
return request.replace('${extraUrlParams}', paramString);
} else {
Expand Down Expand Up @@ -651,6 +590,21 @@ export class AmpAnalytics extends AMP.BaseElement {
}
return to;
}

/**
* @param {!Object<string, Object<string, string|Array<string>>>} source1
* @param {!Object<string, Object<string, string|Array<string>>>} source2
* @param {number=} opt_iterations
* @param {boolean=} opt_noEncode
* @return {!ExpansionOptions}
*/
expansionOptions_(source1, source2, opt_iterations, opt_noEncode) {
const vars = map();
this.mergeObjects_(this.config_['vars'], vars);
this.mergeObjects_(source2['vars'], vars);
this.mergeObjects_(source1['vars'], vars);
return new ExpansionOptions(vars, opt_iterations, opt_noEncode);
}
}

AMP.registerElement('amp-analytics', AmpAnalytics);
50 changes: 4 additions & 46 deletions extensions/amp-analytics/0.1/test/test-amp-analytics.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import {ANALYTICS_CONFIG} from '../vendors';
import {AmpAnalytics} from '../amp-analytics';
import {Crypto} from '../crypto-impl';
import {InstrumentationService} from '../instrumentation';
import {variableServiceFor} from '../variables';
import {
installUserNotificationManager,
} from '../../../amp-user-notification/0.1/amp-user-notification';
Expand Down Expand Up @@ -199,8 +200,9 @@ describe('amp-analytics', function() {
expect(this.replacements_).to.have.property(name);
return {sync: '_' + name.toLowerCase() + '_'};
});
const encodeVars = analytics.encodeVars_;
sandbox.stub(analytics, 'encodeVars_', function(val, name) {
const variables = variableServiceFor(analytics.win);
const encodeVars = variables.encodeVars;
sandbox.stub(variables, 'encodeVars', function(val, name) {
val = encodeVars.call(this, val, name);
if (val == '') {
return '$' + name;
Expand Down Expand Up @@ -567,15 +569,6 @@ describe('amp-analytics', function() {
});
});

it('correctly encodes scalars and arrays', () => {
const a = getAnalyticsTag();
expect(a.encodeVars_('abc %&')).to.equal('abc%20%25%26');
const array = ['abc %&', 'a b'];
expect(a.encodeVars_(array)).to.equal('abc%20%25%26,a%20b');
// Test non-inplace semantics but testing again.
expect(a.encodeVars_(array)).to.equal('abc%20%25%26,a%20b');
});

it('expands url-replacements vars', () => {
const analytics = getAnalyticsTag({
'requests': {
Expand Down Expand Up @@ -933,41 +926,6 @@ describe('amp-analytics', function() {
});
});

describe('expandTemplate_', () => {
const vars = {
'vars': {'1': '1${2}', '2': '2${3}', '3': '3${4}', '4': '4${1}'}};
let analytics;

beforeEach(() => {
analytics = getAnalyticsTag(trivialConfig);
});

it('expands nested vars', () => {
const actual = analytics.expandTemplate_('${1}', vars);
expect(actual).to.equal('123%252524%25257B4%25257D');
});

it('limits the recursion to n', () => {
let actual = analytics.expandTemplate_('${1}', vars, {}, 3);
expect(actual).to.equal('1234%25252524%2525257B1%2525257D');

actual = analytics.expandTemplate_('${1}', vars, {}, 5);
expect(actual).to.equal('123412%252525252524%25252525257B3%25252525257D');
});

it('works with complex params (1)', () => {
const vars = {'vars': {'fooParam': 'QUERY_PARAM(foo,bar)'}};
const actual = analytics.expandTemplate_('${fooParam}', vars);
expect(actual).to.equal('QUERY_PARAM(foo,bar)');
});

it('works with complex params (2)', () => {
const vars = {'vars': {'fooParam': 'QUERY_PARAM'}};
const actual = analytics.expandTemplate_('${fooParam(foo,bar)}', vars);
expect(actual).to.equal('QUERY_PARAM(foo,bar)');
});
});

describe('iframePing', () => {
it('fails for iframePing config outside of vendor config', function() {
const analytics = getAnalyticsTag({
Expand Down
Loading

0 comments on commit 8f9f715

Please sign in to comment.