Skip to content

Commit

Permalink
Merge pull request #1270 from jridgewell/dynamic-user-agent-classes
Browse files Browse the repository at this point in the history
Normalize Referrer classes across devices
  • Loading branch information
jridgewell committed Jan 13, 2016
2 parents f8431bd + 8e71f6f commit 3f6282a
Show file tree
Hide file tree
Showing 5 changed files with 120 additions and 26 deletions.
54 changes: 50 additions & 4 deletions extensions/amp-dynamic-css-classes/0.1/amp-dynamic-css-classes.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,28 @@ const TAG = 'AmpDynamicCssClasses';
/** @const */
const EXPERIMENT = 'dynamic-css-classes';

/**
* Strips everything but the domain from referrer string.
* @param {!Window} win
* @returns {string}
*/
function referrerDomain(win) {
const referrer = win.document.referrer;
if (referrer) {
return parseUrl(referrer).hostname;
}
return '';
}

/**
* Grabs the User Agent string.
* @param {!Window} win
* @returns {string}
*/
function userAgent(win) {
return win.navigator.userAgent;
}

/**
* Returns an array of referrers which vary in level of subdomain specificity.
*
Expand All @@ -33,20 +55,42 @@ const EXPERIMENT = 'dynamic-css-classes';
* @private Visible for testing only!
*/
export function referrers_(referrer) {
referrer = parseUrl(referrer).hostname;
const domains = referrer.split('.');
let domainBase = '';

return domains.reduceRight((referrers, domain) => {
if (domainBase) {
domain += '-' + domainBase;
domain += '.' + domainBase;
}
domainBase = domain;
referrers.push(domain);
return referrers;
}, []);
}

/**
* Normalizes certain referrers across devices.
* @param {!Window} win
* @returns {!Array<string>}
*/
function normalizedReferrers(win) {
const referrer = referrerDomain(win);

// Normalize t.co names to twitter.com
if (referrer === 't.co') {
return referrers_('twitter.com');
}

// Pinterest does not reliably set the referrer on Android
// Instead, we inspect the User Agent string.
if (!referrer && /Pinterest/.test(userAgent(win))) {
return referrers_('www.pinterest.com');
}

return referrers_(referrer);
}


/**
* Adds CSS classes onto the HTML element.
* @param {!Window} win
Expand All @@ -68,8 +112,9 @@ function addDynamicCssClasses(win, classes) {
* @param {!Window} win
*/
function addReferrerClasses(win) {
const classes = referrers_(win.document.referrer).map(referrer => {
return `amp-referrer-${referrer}`;
const referrers = normalizedReferrers(win);
const classes = referrers.map(referrer => {
return `amp-referrer-${referrer.replace(/\./g, '-')}`;
});
addDynamicCssClasses(win, classes);
}
Expand All @@ -86,6 +131,7 @@ function addViewerClass(win) {
}
}


/**
* @param {!Window} win
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,44 +19,44 @@ import {referrers_} from '../amp-dynamic-css-classes';
describe('amp-dynamic-css-classes', () => {
describe('referrers_', () => {
describe('when referrer is TLD-less', () => {
const referrer = 'http://localhost/test/ing?this#referrer';
const referrer = 'localhost';

it('contains the domain', () => {
expect(referrers_(referrer)).to.deep.equal(['localhost']);
});
});

describe('when referrer has no subdomains', () => {
const referrer = 'http://google.com/test/ing?this#referrer';
const referrer = 'google.com';
const referrers = referrers_(referrer);

it('contains the TLD', () => {
expect(referrers).to.contain('com');
});

it('contains the domain', () => {
expect(referrers).to.contain('google-com');
expect(referrers).to.contain('google.com');
expect(referrers.length).to.equal(2);
});
});

describe('when referrer has subdomains', () => {
const referrer = 'http://a.b.c.google.com/test/ing?this#referrer';
const referrer = 'a.b.c.google.com';
const referrers = referrers_(referrer);

it('contains the TLD', () => {
expect(referrers).to.contain('com');
});

it('contains the domain', () => {
expect(referrers).to.contain('google-com');
expect(referrers).to.contain('google.com');
});

it('contains each subdomain', () => {
expect(referrers).to.include.members([
'c-google-com',
'b-c-google-com',
'a-b-c-google-com'
'c.google.com',
'b.c.google.com',
'a.b.c.google.com'
]);
expect(referrers.length).to.equal(5);
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,21 +17,46 @@
import {createServedIframe} from '../../../../testing/iframe';
import {toggleExperiment} from '../../../../src/experiments';

function overwrite(object, property, value) {
Object.defineProperty(object, property, {
enumerable: true,
writeable: false,
configurable: true,
value: value
});
}

const iframeSrc = '/base/test/fixtures/served/amp-dynamic-css-classes.html';

const tcoReferrer = 'http://t.co/xyzabc123';
const PinterestUA = 'Mozilla/5.0 (Linux; Android 5.1.1; SM-G920F' +
' Build/LMY47X; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0' +
' Chrome/47.0.2526.100 Mobile Safari/537.36 [Pinterest/Android]';

describe('dynamic classes are inserted at runtime', () => {
let documentElement, win;
beforeEach(() => {
let documentElement;

function setup(enabled, userAgent, referrer) {
return createServedIframe(iframeSrc).then(fixture => {
win = fixture.win;
const win = fixture.win;
documentElement = fixture.doc.documentElement;

toggleExperiment(win, 'dynamic-css-classes', enabled);

if (userAgent !== undefined) {
overwrite(win.navigator, 'userAgent', userAgent);
}
if (referrer !== undefined) {
overwrite(fixture.doc, 'referrer', referrer);
}

return win.insertDynamicCssScript();
});
});
}

describe('when experiment is disabled', () => {
beforeEach(() => {
toggleExperiment(win, 'dynamic-css-classes', false);
return win.insertDynamicCssScript();
return setup(false);
});

it('should not include referrer classes', () => {
Expand All @@ -45,8 +70,7 @@ describe('dynamic classes are inserted at runtime', () => {

describe('when experiment is enabled', () => {
beforeEach(() => {
toggleExperiment(win, 'dynamic-css-classes', true);
return win.insertDynamicCssScript();
return setup(true);
});

it('should include referrer classes', () => {
Expand All @@ -57,4 +81,21 @@ describe('dynamic classes are inserted at runtime', () => {
expect(documentElement).to.have.class('amp-viewer');
});
});

describe('Normalizing Referrers', () => {
it('should normalize twitter shortlinks to twitter', () => {
return setup(true, '', tcoReferrer).then(() => {
expect(documentElement).to.have.class('amp-referrer-com');
expect(documentElement).to.have.class('amp-referrer-twitter-com');
});
});

it('should normalize pinterest on android', () => {
return setup(true, PinterestUA, '').then(() => {
expect(documentElement).to.have.class('amp-referrer-com');
expect(documentElement).to.have.class('amp-referrer-pinterest-com');
expect(documentElement).to.have.class('amp-referrer-www-pinterest-com');
});
});
});
});
7 changes: 7 additions & 0 deletions extensions/amp-dynamic-css-classes/amp-dynamic-css-classes.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,13 @@ subdomain specificity. For example, `www.google.com` will add three
classes: `amp-referrer-www-google-com`, `amp-referrer-google-com`, and
`amp-referrer-com`.

We currently have a few special cases:

- When the user came through a Twitter `t.co` short link, we instead use
`twitter.com` as the referrer.
- When the string "Pinterest" is present in the User Agent string and
there is no referrer, we use `www.pinterest.com` as the referrer.

**amp-viewer**

The `amp-viewer` class will be set if the current document is being
Expand Down
12 changes: 6 additions & 6 deletions src/platform.js
Original file line number Diff line number Diff line change
Expand Up @@ -27,33 +27,33 @@ export class Platform {
* @param {!Window} win
*/
constructor(win) {
/** @const {!Window} */
this.win = win;
/** @const {!Navigator} */
this.navigator = win.navigator;
}

/**
* Whether the current platform an iOS device.
* @return {boolean}
*/
isIos() {
return /iPhone|iPad|iPod/i.test(this.win.navigator.userAgent);
return /iPhone|iPad|iPod/i.test(this.navigator.userAgent);
}

/**
* Whether the current browser is Safari.
* @return {boolean}
*/
isSafari() {
return /Safari/i.test(this.win.navigator.userAgent) && !this.isChrome();
return /Safari/i.test(this.navigator.userAgent) && !this.isChrome();
}

/**
* Whether the current browser a Chrome browser.
* Whether the current browser is a Chrome browser.
* @return {boolean}
*/
isChrome() {
// Also true for MS Edge :)
return /Chrome|CriOS/i.test(this.win.navigator.userAgent);
return /Chrome|CriOS/i.test(this.navigator.userAgent);
}
};

Expand Down

0 comments on commit 3f6282a

Please sign in to comment.