Skip to content
This repository was archived by the owner on Jan 17, 2023. It is now read-only.
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions addon/data/postmessageProxyFrame.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
<!doctype html>
<html>
<head>
<meta charset="utf-8"/>
</head>
<body>
<iframe id="tp-proxy" src="http://testpilot.dev:8000/postmessage-proxy"></iframe>
</body>
</html>
25 changes: 25 additions & 0 deletions addon/data/postmessageProxyFrame.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
const TELEMETRY_QUEUE_POLL_INTERVAL = 5000;
const proxyFrame = document.getElementById('tp-proxy');

// Periodically poll for queued telemetry pings
setInterval(function() {
proxyFrame.contentWindow.postMessage({
op: 'fetchTelemetryPings'
}, '*');
}, TELEMETRY_QUEUE_POLL_INTERVAL);

// Listen for postMessage
window.addEventListener('message', function(ev) {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

And would we want to check for ev.origin == ? here? (Though I'm not sure what the origin will be when the message comes from the add-on and/or web extension.)

Copy link
Copy Markdown
Member

@groovecoder groovecoder Jun 22, 2016

Choose a reason for hiding this comment

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

Ah, so this addon event listener will be getting the messages from testpilot.firefox.com ? So maybe that's the one we want to verify?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Same as above - the messages can come from one of prod / stage / dev / local and I don't have a way to configure that yet

if (ev.data.op === 'telemetryPings') {
// Relay telemetryPings on to the add-on
self.port.emit('telemetryPings', ev.data.data);
return;
}
});

// Listen for message from add-on to update iframe src
self.port.on('updateIFrameSrc', src => {
proxyFrame.src = src;
});

self.port.emit('ready');
3 changes: 3 additions & 0 deletions addon/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ Mustache.parse(templates.installed);
Mustache.parse(templates.experimentList);

const Metrics = require('./lib/metrics');
const PostmessageProxy = require('./lib/postmessage-proxy');
const survey = require('./lib/survey');

const PANEL_WIDTH = 300;
Expand Down Expand Up @@ -571,6 +572,7 @@ exports.main = function(options) {

initServerEnvironmentPreference();
Metrics.init();
PostmessageProxy.init();
};

exports.onUnload = function(reason) {
Expand All @@ -579,6 +581,7 @@ exports.onUnload = function(reason) {
panel.destroy();
button.destroy();
Metrics.destroy();
PostmessageProxy.destroy();
survey.destroy();

if (reason === 'uninstall' || reason === 'disable') {
Expand Down
9 changes: 9 additions & 0 deletions addon/lib/metrics.js
Original file line number Diff line number Diff line change
Expand Up @@ -167,6 +167,15 @@ module.exports = {
});
},

onPostmessageProxyFramePings: function(ev) {
const pings = JSON.parse(ev);
pings.forEach(ping => this.onExperimentPing({
subject: ping.subject,
// HACK: Re-encode data because onExperimentPing expects a string
data: JSON.stringify(ping.data)
}));
},

onExperimentPing: function(ev) {
const { subject, data } = ev;
const dataParsed = JSON.parse(data);
Expand Down
41 changes: 41 additions & 0 deletions addon/lib/postmessage-proxy.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
const {Page} = require('sdk/page-worker');
const Metrics = require('./metrics');
const simplePrefs = require('sdk/simple-prefs');

let postmessageProxyFrameWorker;

const iframeURLs = {
local: 'http://testpilot.dev:8000/postmessage-proxy',
dev: 'http://testpilot.dev.mozaws.net/postmessage-proxy',
stage: 'https://testpilot.stage.mozaws.net/postmessage-proxy',
production: 'https://testpilot.firefox.com/postmessage-proxy'
};

module.exports = {

init: function() {
postmessageProxyFrameWorker = Page({ // eslint-disable-line new-cap
contentURL: './postmessageProxyFrame.html',
contentScriptFile: './postmessageProxyFrame.js'
});

postmessageProxyFrameWorker.port.on('telemetryPings',
ev => Metrics.onPostmessageProxyFramePings(ev));

postmessageProxyFrameWorker.port.on('ready', () => this.updatePrefs());

simplePrefs.on('SERVER_ENVIRONMENT', () => this.updatePrefs());
},

updatePrefs: function() {
const envName = simplePrefs.prefs.SERVER_ENVIRONMENT;
const src = (envName in iframeURLs) ?
iframeURLs[envName] : iframeURLs.production;
postmessageProxyFrameWorker.port.emit('updateIFrameSrc', src);
},

destroy: function() {
postmessageProxyFrameWorker.destroy();
}

};
6 changes: 6 additions & 0 deletions docs/examples/webextension/background.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
<!doctype html>
<html>
<body>
<iframe id="tp-proxy" src="http://testpilot.dev:8000/postmessage-proxy"></iframe>
</body>
</html>
18 changes: 18 additions & 0 deletions docs/examples/webextension/background.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
var EXPERIMENT_ID = 'webextension-example-1';

function sendTelemetryPing(data) {
document.getElementById('tp-proxy').contentWindow.postMessage({
op: 'queueTelemetryPing',
data: {
subject: EXPERIMENT_ID,
data: data
}
}, '*');
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

In the "real world" implementation, I assume we would tell devs to only post the message to targetOrigin of testpilot.firefox.com ?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Yeah, but until/unless I figure out a way to configure this for dev/stage/prod environment it's * for now

}

// Start sending a ping with fake data every second.
setInterval(function () {
sendTelemetryPing({
timesThingClicked: parseInt(Math.random() * 100)
});
}, 1000);
20 changes: 20 additions & 0 deletions docs/examples/webextension/manifest.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
{
"manifest_version": 2,
"name": "Test Pilot WebExtension Example",
"version": "1.0",
"description": "This is a WebExtension built as an example Test Pilot experiment",
"icons": {
"32": "icons/icon-32.png"
},
"permissions": ["background"],
"applications": {
"gecko": {
"id": "testpilotexample1@mozilla.org",
"strict_min_version": "45.0"
}
},
"background": {
"page": "background.html",
"scripts": ["background.js"]
}
}
8 changes: 8 additions & 0 deletions gulpfile.js
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,12 @@ gulp.task('scripts', shouldLint('js-lint', 'lint'), function extraScriptsTask()
.pipe(gulp.dest(DEST_PATH + 'scripts'));
});

gulp.task('vendor', shouldLint('js-lint', 'lint'), function extraVendorTask() {
return gulp.src(SRC_PATH + 'vendor/**/*')
.pipe(gulpif(!IS_DEBUG, uglify()))
.pipe(gulp.dest(DEST_PATH + 'vendor'));
});

gulp.task('styles', shouldLint('sass-lint', 'sass-lint'), function stylesTask() {
return gulp.src(SRC_PATH + 'styles/**/*.scss')
.pipe(sourcemaps.init())
Expand Down Expand Up @@ -171,6 +177,7 @@ gulp.task('build', function buildTask(done) {
'app-vendor',
'app-main',
'scripts',
'vendor',
'styles',
'images',
'locales',
Expand All @@ -186,6 +193,7 @@ gulp.task('watch', ['build'], function watchTask() {
gulp.watch(SRC_PATH + 'app/**/*.js', ['app-main']);
gulp.watch('./package.json', ['app-vendor']);
gulp.watch(SRC_PATH + 'scripts/**/*.js', ['scripts']);
gulp.watch(SRC_PATH + 'vendor/**/*.js', ['vendor']);
gulp.watch(SRC_PATH + 'addon/**/*', ['addon']);
gulp.watch(['./legal-copy/*.md', './legal-copy/*.js'], ['legal']);
gulp.watch('./locales/**/*', ['locales']);
Expand Down
41 changes: 41 additions & 0 deletions testpilot/frontend/static-src/scripts/proxy.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
var TELEMETRY_PINGS_KEY = 'telemetryPings';
var TELEMETRY_PINGS_MAX_COUNT = 10;

// Parse queued telemetry pings from a cookie
function parseTelemetryPings() {
var pings;
try { pings = JSON.parse(Cookies.get(TELEMETRY_PINGS_KEY)); }
catch (e) { pings = []; }
return pings;
}

window.addEventListener('message', function(ev) {
// Only listen for messages from add-ons and webextensions
if (ev.origin.indexOf('moz-extension://') !== 0 &&
ev.origin.indexOf('resource://testpilot-addon') !== 0) {
return;
}

if (ev.data.op === 'queueTelemetryPing') {
// Parse the current list of pings, add this new one.
var pings = parseTelemetryPings();
pings.push(ev.data.data);
// Drop some older pings, if we have too many.
while (pings.length > TELEMETRY_PINGS_MAX_COUNT) { pings.shift(); }
// Serialize the queue back into a cookie.
Cookies.set(TELEMETRY_PINGS_KEY, JSON.stringify(pings));
return;
}

if (ev.data.op === 'fetchTelemetryPings') {
// Parse the current list of pings, clear the queue.
var pings = parseTelemetryPings();
Cookies.set(TELEMETRY_PINGS_KEY, JSON.stringify([]));
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Should we wait until we've (successfully) posted the message before we empty the queue?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Problem with that, is there's no way to keep more queue items from rolling in while we're waiting for an acknowledgement. Also would require a significantly more complex system of messaging that we probably don't need for Telemetry pings.

// Send the list of pings back to the requester
ev.source.postMessage({
op: 'telemetryPings',
data: JSON.stringify(pings)
}, '*');
return;
}
});
151 changes: 151 additions & 0 deletions testpilot/frontend/static-src/vendor/js.cookie.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
/*!
* JavaScript Cookie v2.1.2
* https://github.com/js-cookie/js-cookie
*
* Copyright 2006, 2015 Klaus Hartl & Fagner Brack
* Released under the MIT license
*/
;(function (factory) {
if (typeof define === 'function' && define.amd) {
define(factory);
} else if (typeof exports === 'object') {
module.exports = factory();
} else {
var OldCookies = window.Cookies;
var api = window.Cookies = factory();
api.noConflict = function () {
window.Cookies = OldCookies;
return api;
};
}
}(function () {
function extend () {
var i = 0;
var result = {};
for (; i < arguments.length; i++) {
var attributes = arguments[ i ];
for (var key in attributes) {
result[key] = attributes[key];
}
}
return result;
}

function init (converter) {
function api (key, value, attributes) {
var result;
if (typeof document === 'undefined') {
return;
}

// Write

if (arguments.length > 1) {
attributes = extend({
path: '/'
}, api.defaults, attributes);

if (typeof attributes.expires === 'number') {
var expires = new Date();
expires.setMilliseconds(expires.getMilliseconds() + attributes.expires * 864e+5);
attributes.expires = expires;
}

try {
result = JSON.stringify(value);
if (/^[\{\[]/.test(result)) {
value = result;
}
} catch (e) {}

if (!converter.write) {
value = encodeURIComponent(String(value))
.replace(/%(23|24|26|2B|3A|3C|3E|3D|2F|3F|40|5B|5D|5E|60|7B|7D|7C)/g, decodeURIComponent);
} else {
value = converter.write(value, key);
}

key = encodeURIComponent(String(key));
key = key.replace(/%(23|24|26|2B|5E|60|7C)/g, decodeURIComponent);
key = key.replace(/[\(\)]/g, escape);

return (document.cookie = [
key, '=', value,
attributes.expires ? '; expires=' + attributes.expires.toUTCString() : '', // use expires attribute, max-age is not supported by IE
attributes.path ? '; path=' + attributes.path : '',
attributes.domain ? '; domain=' + attributes.domain : '',
attributes.secure ? '; secure' : ''
].join(''));
}

// Read

if (!key) {
result = {};
}

// To prevent the for loop in the first place assign an empty array
// in case there are no cookies at all. Also prevents odd result when
// calling "get()"
var cookies = document.cookie ? document.cookie.split('; ') : [];
var rdecode = /(%[0-9A-Z]{2})+/g;
var i = 0;

for (; i < cookies.length; i++) {
var parts = cookies[i].split('=');
var cookie = parts.slice(1).join('=');

if (cookie.charAt(0) === '"') {
cookie = cookie.slice(1, -1);
}

try {
var name = parts[0].replace(rdecode, decodeURIComponent);
cookie = converter.read ?
converter.read(cookie, name) : converter(cookie, name) ||
cookie.replace(rdecode, decodeURIComponent);

if (this.json) {
try {
cookie = JSON.parse(cookie);
} catch (e) {}
}

if (key === name) {
result = cookie;
break;
}

if (!key) {
result[name] = cookie;
}
} catch (e) {}
}

return result;
}

api.set = api;
api.get = function (key) {
return api(key);
};
api.getJSON = function () {
return api.apply({
json: true
}, [].slice.call(arguments));
};
api.defaults = {};

api.remove = function (key, attributes) {
api(key, '', extend(attributes, {
expires: -1
}));
};

api.withConverter = init;

return api;
}

return init(function () {});
}));
7 changes: 7 additions & 0 deletions testpilot/frontend/templates/testpilot/frontend/proxy.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
<!doctype html>
<html>
<body>
<script src="{{ static('vendor/js.cookie.js') }}"></script>
<script src="{{ static('scripts/proxy.js') }}"></script>
</body>
</html>
Loading