Skip to content

Commit

Permalink
Ask user to share after having an experiment installed for 3 days.
Browse files Browse the repository at this point in the history
  • Loading branch information
Chuck Harmston committed Sep 17, 2016
1 parent 622e528 commit ef8ef00
Show file tree
Hide file tree
Showing 5 changed files with 309 additions and 16 deletions.
10 changes: 7 additions & 3 deletions addon/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,14 @@ const tabs = require('sdk/tabs');
const request = require('sdk/request').Request;
const { PrefsTarget } = require('sdk/preferences/event-target');
const URL = require('sdk/url').URL;

const { App } = require('./lib/app');
const ExperimentNotifications = require('./lib/experiment-notifications');
const Metrics = require('./lib/metrics');
const SharePrompt = require('./lib/share-prompt');
const survey = require('./lib/survey');
const WebExtensionChannels = require('./lib/webextension-channels');
const ToolbarButton = require('./lib/toolbar-button');
const ExperimentNotifications = require('./lib/experiment-notifications');
const { App } = require('./lib/app');
const WebExtensionChannels = require('./lib/webextension-channels');

const settings = {};
let prefs;
Expand Down Expand Up @@ -355,6 +357,7 @@ exports.main = function(options) {
WebExtensionChannels.init();
ToolbarButton.init(settings);
ExperimentNotifications.init();
SharePrompt.init(settings);

if (reason === 'install') {
openOnboardingTab();
Expand All @@ -369,6 +372,7 @@ exports.onUnload = function(reason) {
WebExtensionChannels.destroy();
ToolbarButton.destroy();
ExperimentNotifications.destroy();
SharePrompt.destroy(reason);

if (reason === 'uninstall' || reason === 'disable') {
Metrics.onDisable();
Expand Down
75 changes: 75 additions & 0 deletions addon/lib/share-prompt.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
/*
* This Source Code is subject to the terms of the Mozilla Public License
* version 2.0 (the 'License'). You can obtain a copy of the License at
* http://mozilla.org/MPL/2.0/.
*/

const store = require('sdk/simple-storage').storage;
const tabs = require('sdk/tabs');

let SETTINGS;


module.exports = {
DELAY: 3 * 24 * 60 * 60 * 1000, // 3 days
LOCAL_DELAY: 1000, // 1 second
PATH: 'share',
QS: 'utm_source=testpilot-addon&utm_medium=firefox-browser&utm_campaign=share-page',

makeUrl: function() {
return `${SETTINGS.BASE_URL}/${this.path}?${this.qs}`;
},

hasExperimentInstalled: function() {
try {
return Object.keys(store.installedAddons).length > 0;
} catch (err) {
return false;
}
},

openShareTab: function() {
tabs.open({
url: this.makeUrl()
});
},

calculateDelay: function() {
return SETTINGS.env === 'local' ? this.LOCAL_DELAY : this.DELAY;
},

shouldOpenTab: function() {
return (
store.sharePrompt.hasBeenShown === false &&
store.sharePrompt.showAt &&
store.sharePrompt.showAt <= Date.now()
);
},

setup: function() {
if (typeof store.sharePrompt === 'undefined') {
store.sharePrompt = {
showAt: 0,
hasBeenShown: false
};
}
if (this.hasExperimentInstalled() && !store.sharePrompt.showAt) {
store.sharePrompt.showAt = Date.now() + this.calculateDelay();
}
},

destroy: function(reason) {
if (reason === 'uninstall') {
delete store.sharePrompt;
}
},

init: function(settings) {
SETTINGS = settings;
this.setup();
if (this.shouldOpenTab()) {
this.openShareTab();
store.sharePrompt.hasBeenShown = true;
}
}
};
191 changes: 191 additions & 0 deletions addon/test/test-share-prompt.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,191 @@
/*
* This Source Code is subject to the terms of the Mozilla Public License
* version 2.0 (the "License"). You can obtain a copy of the License at
* http://mozilla.org/MPL/2.0/.
*/
const { before } = require('sdk/test/utils');
const MockUtils = require('./lib/mock-utils');


MockUtils.setDebug(true);

const mocks = {
store: {},
callbacks: MockUtils.callbacks({
Tabs: ['open']
})
};
const mockLoader = MockUtils.loader(module, './lib/share-prompt.js', {
'sdk/simple-storage': {storage: mocks.store},
'sdk/tabs': mocks.callbacks.Tabs
});
const SharePrompt = mockLoader.require('../lib/share-prompt');
const mockSettings = {
BASE_URL: 'https://foo.bar',
env: 'production'
};


exports['test hasExperimentInstalled'] = assert => {
// If store.installedAddons is not defined.
assert.equal(SharePrompt.hasExperimentInstalled(), false,
'Gracefully handles error conditions.');

// If store.installedAddons is defined, but empty.
mocks.store.installedAddons = {};
assert.equal(SharePrompt.hasExperimentInstalled(), false,
'Returns false when no experiments installed.');

// If store.installedAddons is defined and populated.
mocks.store.installedAddons = {foo: 'bar'};
assert.equal(SharePrompt.hasExperimentInstalled(), true,
'Returns true when experiments installed.');
};


exports['test openShareTab'] = assert => {
SharePrompt.init(mockSettings);
SharePrompt.openShareTab();

const tabOpenCalls = mocks.callbacks.Tabs.open.calls();
const tabOpenUrl = tabOpenCalls[0][0].url;
assert.equal(1, tabOpenCalls.length, 'A share tab opened as expected.');
assert.equal(tabOpenUrl, SharePrompt.makeUrl(),
'Tab opens correct URL.');
};


exports['test calculateDelay'] = assert => {
// Normal delay returned.
SharePrompt.init(mockSettings);
assert.equal(SharePrompt.calculateDelay(), SharePrompt.DELAY,
'Normal delay returned when ENV is "production".');

// Shortened delay in local environment returned.
const localSettings = mockSettings;
localSettings.env = 'local';
SharePrompt.init(localSettings);
assert.equal(SharePrompt.calculateDelay(), SharePrompt.LOCAL_DELAY,
'Shortened delay applied when ENV is "local".');
};


exports['test shouldOpenTab'] = assert => {
// User has already seen share prompt.
mocks.store.sharePrompt = {hasBeenShown: true};
assert.equal(SharePrompt.shouldOpenTab(), false,
'Do not open share tab if already shown to user.');

// Date to show prompt hasn't been set (e.g. no experiments installed)
mocks.store.sharePrompt = {hasBeenShown: false, showAt: 0};
assert.equal(SharePrompt.shouldOpenTab(), false,
'Do not open share tab if showAt has not been set.');

// Date to show prompt is in the past.
mocks.store.sharePrompt = {hasBeenShown: false, showAt: Date.now() - 10000};
assert.equal(SharePrompt.shouldOpenTab(), true,
'Open share tab if not shown to user and showAt is in the past');

// Date to show prompt is in the future.
mocks.store.sharePrompt = {hasBeenShown: false, showAt: Date.now() + 10000};
assert.equal(SharePrompt.shouldOpenTab(), false,
'Do not open share tab if not shown to user and showAt is in the future.');
};


exports['test setup => sharePrompt state'] = assert => {
// Set the sharePrompt state if undefined.
delete mocks.store.sharePrompt;
SharePrompt.setup();
assert.ok(typeof mocks.store.sharePrompt !== 'undefined',
'sharePrompt state initialized if not already set.');

// Don't reset sharePrompt state if already defined.
const MOCK = 'not changed';
mocks.store.sharePrompt = MOCK;
SharePrompt.setup();
assert.equal(mocks.store.sharePrompt, MOCK,
'Does not reset sharePrompt state if already set.');
};


exports['test setup => set showAt date'] = assert => {
mocks.store.sharePrompt = {showAt: 0};
mocks.store.installedAddons = {};

// If no experiments are installed and not already set.
SharePrompt.setup();
assert.equal(mocks.store.sharePrompt.showAt, 0,
'showAt date not set if no experiments installed.');

// If experiments are installed and not already set.
mocks.store.installedAddons = {foo: 'bar'};
SharePrompt.setup();
assert.ok(mocks.store.sharePrompt.showAt > 0,
'showAt date correctly set.');

// If showAt already set.
const MOCK = 12345;
mocks.store.sharePrompt.showAt = MOCK;
SharePrompt.setup();
assert.equal(mocks.store.sharePrompt.showAt, MOCK,
'showAt date not set if already set.');
};


exports['test destroy'] = assert => {
const MOCK = {foo: 'bar'};
mocks.store.sharePrompt = MOCK;

// On shutdown.
SharePrompt.destroy('shutdown');
assert.equal(mocks.store.sharePrompt, MOCK,
'sharePrompt state was deleted on shutdown.');

// On upgrade.
SharePrompt.destroy('upgrade');
assert.equal(mocks.store.sharePrompt, MOCK,
'sharePrompt state was deleted on upgrade.');

// On downgrade.
SharePrompt.destroy('downgrade');
assert.equal(mocks.store.sharePrompt, MOCK,
'sharePrompt state was deleted on downgrade.');

// On disable.
SharePrompt.destroy('disable');
assert.equal(mocks.store.sharePrompt, MOCK,
'sharePrompt state was deleted on disable.');

// On destroy.
SharePrompt.destroy('uninstall');
assert.equal(typeof mocks.store.sharePrompt, 'undefined',
'sharePrompt state was not deleted on uninstall.');
};


exports['test init'] = assert => {
// First run.
SharePrompt.init(mockSettings);
assert.ok(typeof mocks.store.sharePrompt !== 'undefined', 'setup called');
assert.equal(mocks.store.sharePrompt.hasBeenShown, false,
'Share tab not opened on first run.');

// Tab should open.
mocks.store.sharePrompt.showAt = Date.now();
SharePrompt.init(mockSettings);
assert.equal(1, mocks.callbacks.Tabs.open.calls().length,
'A share tab was opened.');
assert.equal(mocks.store.sharePrompt.hasBeenShown, true,
'Share tab opening was correctly recorded.');
};


before(module.exports, function(name, assert, done) {
MockUtils.resetCallbacks(mocks.callbacks);
Object.keys(mocks.store).forEach(key => delete mocks.store[key]);
done();
});


require('sdk/test').run(exports);
19 changes: 11 additions & 8 deletions docs/metrics/ga.md
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@ Here are the current events on the website as of this writing
| Click take survey after Leave | RetirePage Interactions | button click | take survey |
| Click on Twitter link in footer | FooterView Interactions | social link clicked | Twitter |
| Click on GitHub link in footer | FooterView Interactions | social link clicked | GitHub |
| Click on a button in the Share section | ShareView Interactions | button click | {facebook,twitter,email,copy} |

## Pageviews

Expand Down Expand Up @@ -104,12 +105,12 @@ Here is a list of dimensions we are currently using

| Page | Description | dimension | values |
|-----------------------------------|---------------------------------------------------|-----------|--------|
| Home Page, Experiment Detail Page | Does the user have the add-on installed | 1 | {0,1} |
| Home Page | Does the user have any experiments installed | 2 | {0,1} |
| Home Page | How many experiments does the user have installed | 3 | {n} |
| Experiment Detail Page | Is the experiment enabled | 4 | {0,1} |
| Experiment Detail Page | Experiment title | 5 | "xyz" |
| Experiment Detail Page | Installation count | 6 | {n} |
| Home Page, Experiment Detail Page, Share Page | Does the user have the add-on installed | 1 | {0,1} |
| Home Page, Share Page | Does the user have any experiments installed | 2 | {0,1} |
| Home Page, Share Page | How many experiments does the user have installed | 3 | {n} |
| Experiment Detail Page | Is the experiment enabled | 4 | {0,1} |
| Experiment Detail Page | Experiment title | 5 | "xyz" |
| Experiment Detail Page | Installation count | 6 | {n} |

### Tagged Links

Expand All @@ -128,5 +129,7 @@ We should maintain a consistent convention when using campaign parameters.

| Description | utm_source | utm_medium | utm_campaign | utm_content |
|---------------------------------------------------------------|-----------------|-----------------|----------------------|--------------------|
| Clicking on an experiment (or "view all") from the doorhanger | testpilot-addon | firefox-browser | testpilot-doorhanger | 'badged' or 'not badged' depending on presence of 'New' badge on add-on toolbar button |
| Clicking on an experiment from the in-product messaging | testpilot-addon | firefox-browser | push notification | {messageID} |
| Clicking on an experiment (or "view all") from the doorhanger | testpilot-addon | firefox-browser | testpilot-doorhanger | 'badged' or 'not badged' depending on presence of 'New' badge on add-on toolbar button |
| Clicking on an experiment from the in-product messaging | testpilot-addon | firefox-browser | push notification | {messageID} |
| Tab opens after user has tried an experiment for n days (#1292) | testpilot-addon | firefox-browser | share-page | |
| Links that get shared from /share | {facebook,twitter,email,copy} | social | share-page | |
30 changes: 25 additions & 5 deletions frontend/src/app/components/SharePage.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ import React from 'react';

import Clipboard from 'clipboard';

import { sendToGA } from '../lib/utils';

import Header from './Header';
import Footer from './Footer';

Expand All @@ -10,6 +12,24 @@ clipboard.on('success', e => {
console && console.debug('Copied', e.text);
});


function shareUrl(medium, urlencode) {
const url = `https://testpilot.firefox.com/?utm_source=${medium}&utm_medium=social&utm_campaign=share-page`;
return urlencode ? encodeURIComponent(url) : url;
}


function handleClick(label) {
return () => {
sendToGA('event', {
eventCategory: 'ShareView Interactions',
eventAction: 'button click',
eventLabel: label
});
};
}


export default function SharePage() {
return (
<div className="full-page-wrapper space-between">
Expand All @@ -19,15 +39,15 @@ export default function SharePage() {
<div className="modal-content">
<p data-l10n-id="sharePrimary">Love Test Pilot? Help us find some new recruits.</p>
<ul className="share-list">
<li className="share-facebook"><a href="https://www.facebook.com/sharer/sharer.php?u=https%3A//testpilot.firefox.com" target="_blank">Facebook</a></li>
<li className="share-twitter"><a href="https://twitter.com/home?status=https%3A//testpilot.firefox.com" target="_blank">Twitter</a></li>
<li className="share-email"><a href="mailto:?body=https%3A//testpilot.firefox.com" data-l10n-id="shareEmail">E-mail</a></li>
<li className="share-facebook"><a href={'https://www.facebook.com/sharer/sharer.php?u=' + shareUrl('facebook', true)} onClick={handleClick('facebook')} target="_blank">Facebook</a></li>
<li className="share-twitter"><a href={'https://twitter.com/home?status=' + shareUrl('twitter', true)} onClick={handleClick('twitter')} target="_blank">Twitter</a></li>
<li className="share-email"><a href={'mailto:?body=' + shareUrl('email', true)} data-l10n-id="shareEmail" onClick={handleClick('email')}>E-mail</a></li>
</ul>
<p data-l10n-id="shareSecondary">or just copy and paste this link...</p>
<fieldset className="share-url-wrapper">
<div className="share-url">
<input type="text" readOnly value="https://testpilot.firefox.com" />
<button data-l10n-id="shareCopy" data-clipboard-target=".share-url input">Copy</button>
<input type="text" readOnly value={shareUrl('copy', false)} />
<button data-l10n-id="shareCopy" onClick={handleClick('copy')} data-clipboard-target=".share-url input">Copy</button>
</div>
</fieldset>
</div>
Expand Down

0 comments on commit ef8ef00

Please sign in to comment.