added six pack to web

This commit is contained in:
Henry Oswald 2015-10-27 10:58:27 +00:00
parent 5e19d710ba
commit 8f1d09beea
6 changed files with 704 additions and 2 deletions

View file

@ -56,7 +56,8 @@ html(itemscope, itemtype='http://schema.org/Product')
script.
window.sharelatex = {
siteUrl: '#{settings.siteUrl}',
jsPath: '#{jsPath}'
jsPath: '#{jsPath}',
sixpackDomain: '#{settings.sixpackDomain}'
};
window.systemMessages = !{JSON.stringify(systemMessages).replace(/\//g, '\\/')};
window.ab = {}

View file

@ -13,8 +13,13 @@ define [
"underscore"
"ngSanitize"
"ipCookie"
"mvdSixpack"
"ErrorCatcher"
"localStorage"
])
]).config (sixpackProvider)->
sixpackProvider.setOptions({
debug: true
baseUrl: window.sharelatex.sixpackDomain
})
return App

View file

@ -9,5 +9,8 @@ define [
"libs/fineuploader"
"libs/angular-sanitize-1.2.17"
"libs/angular-cookie"
"libs/angular-cookies"
"libs/passfield"
"libs/sixpack"
"libs/angular-sixpack"
], () ->

View file

@ -0,0 +1,206 @@
/**
* @license AngularJS v1.3.15
* (c) 2010-2014 Google, Inc. http://angularjs.org
* License: MIT
*/
(function(window, angular, undefined) {'use strict';
/**
* @ngdoc module
* @name ngCookies
* @description
*
* # ngCookies
*
* The `ngCookies` module provides a convenient wrapper for reading and writing browser cookies.
*
*
* <div doc-module-components="ngCookies"></div>
*
* See {@link ngCookies.$cookies `$cookies`} and
* {@link ngCookies.$cookieStore `$cookieStore`} for usage.
*/
angular.module('ngCookies', ['ng']).
/**
* @ngdoc service
* @name $cookies
*
* @description
* Provides read/write access to browser's cookies.
*
* Only a simple Object is exposed and by adding or removing properties to/from this object, new
* cookies are created/deleted at the end of current $eval.
* The object's properties can only be strings.
*
* Requires the {@link ngCookies `ngCookies`} module to be installed.
*
* @example
*
* ```js
* angular.module('cookiesExample', ['ngCookies'])
* .controller('ExampleController', ['$cookies', function($cookies) {
* // Retrieving a cookie
* var favoriteCookie = $cookies.myFavorite;
* // Setting a cookie
* $cookies.myFavorite = 'oatmeal';
* }]);
* ```
*/
factory('$cookies', ['$rootScope', '$browser', function($rootScope, $browser) {
var cookies = {},
lastCookies = {},
lastBrowserCookies,
runEval = false,
copy = angular.copy,
isUndefined = angular.isUndefined;
//creates a poller fn that copies all cookies from the $browser to service & inits the service
$browser.addPollFn(function() {
var currentCookies = $browser.cookies();
if (lastBrowserCookies != currentCookies) { //relies on browser.cookies() impl
lastBrowserCookies = currentCookies;
copy(currentCookies, lastCookies);
copy(currentCookies, cookies);
if (runEval) $rootScope.$apply();
}
})();
runEval = true;
//at the end of each eval, push cookies
//TODO: this should happen before the "delayed" watches fire, because if some cookies are not
// strings or browser refuses to store some cookies, we update the model in the push fn.
$rootScope.$watch(push);
return cookies;
/**
* Pushes all the cookies from the service to the browser and verifies if all cookies were
* stored.
*/
function push() {
var name,
value,
browserCookies,
updated;
//delete any cookies deleted in $cookies
for (name in lastCookies) {
if (isUndefined(cookies[name])) {
$browser.cookies(name, undefined);
}
}
//update all cookies updated in $cookies
for (name in cookies) {
value = cookies[name];
if (!angular.isString(value)) {
value = '' + value;
cookies[name] = value;
}
if (value !== lastCookies[name]) {
$browser.cookies(name, value);
updated = true;
}
}
//verify what was actually stored
if (updated) {
updated = false;
browserCookies = $browser.cookies();
for (name in cookies) {
if (cookies[name] !== browserCookies[name]) {
//delete or reset all cookies that the browser dropped from $cookies
if (isUndefined(browserCookies[name])) {
delete cookies[name];
} else {
cookies[name] = browserCookies[name];
}
updated = true;
}
}
}
}
}]).
/**
* @ngdoc service
* @name $cookieStore
* @requires $cookies
*
* @description
* Provides a key-value (string-object) storage, that is backed by session cookies.
* Objects put or retrieved from this storage are automatically serialized or
* deserialized by angular's toJson/fromJson.
*
* Requires the {@link ngCookies `ngCookies`} module to be installed.
*
* @example
*
* ```js
* angular.module('cookieStoreExample', ['ngCookies'])
* .controller('ExampleController', ['$cookieStore', function($cookieStore) {
* // Put cookie
* $cookieStore.put('myFavorite','oatmeal');
* // Get cookie
* var favoriteCookie = $cookieStore.get('myFavorite');
* // Removing a cookie
* $cookieStore.remove('myFavorite');
* }]);
* ```
*/
factory('$cookieStore', ['$cookies', function($cookies) {
return {
/**
* @ngdoc method
* @name $cookieStore#get
*
* @description
* Returns the value of given cookie key
*
* @param {string} key Id to use for lookup.
* @returns {Object} Deserialized cookie value.
*/
get: function(key) {
var value = $cookies[key];
return value ? angular.fromJson(value) : value;
},
/**
* @ngdoc method
* @name $cookieStore#put
*
* @description
* Sets a value for given cookie key
*
* @param {string} key Id for the `value`.
* @param {Object} value Value to be stored.
*/
put: function(key, value) {
$cookies[key] = angular.toJson(value);
},
/**
* @ngdoc method
* @name $cookieStore#remove
*
* @description
* Remove given cookie
*
* @param {string} key Id of the key-value pair to delete.
*/
remove: function(key) {
delete $cookies[key];
}
};
}]);
})(window, window.angular);

View file

@ -0,0 +1,274 @@
(function(angular) {
// Just in case...
if (!Array.prototype.indexOf) {
Array.prototype.indexOf = function (searchElement, fromIndex) {
if ( this === undefined || this === null ) {
throw new TypeError( '"this" is null or not defined' );
}
var length = this.length >>> 0; // Hack to convert object.length to a UInt32
fromIndex = +fromIndex || 0;
if (Math.abs(fromIndex) === Infinity) {
fromIndex = 0;
}
if (fromIndex < 0) {
fromIndex += length;
if (fromIndex < 0) {
fromIndex = 0;
}
}
for (;fromIndex < length; fromIndex++) {
if (this[fromIndex] === searchElement) {
return fromIndex;
}
}
return -1;
};
}
angular.module('mvdSixpack', ['ngCookies'])
.provider('sixpack', function() {
var $body
, _tests = []
, _choices = {}
, _opts = {
baseUrl: '',
debug: false,
}
, sp = window.sixpack;
this.setOptions = function (options) {
angular.extend(_opts, options || {});
}
this.$get = ['$cookies','$timeout', '$log', function($cookies, $timeout, $log) {
var _cookiePrefix = 'sixpack-'
, _session
, _clientId;
var _getOrInitSession = function () {
if (!_session) {
if (_clientId = $cookies[_cookiePrefix + 'clientId']) {
_session = new sp.Session({client_id:_clientId, base_url:_opts.baseUrl});
} else {
_session = new sp.Session({client_id:undefined, base_url:_opts.baseUrl});
$cookies[_cookiePrefix + 'clientId'] = _clientId = _session.client_id;
}
if (_opts.debug) {
$log.debug('[sixpack] Initialized session with clientId', _clientId, 'and base url', _opts.baseUrl);
};
};
return _session;
}
var methods = {
participate : function (testName, variations, callback) {
if (_tests.indexOf(testName) < 0) {
_tests.push(testName);
} else if (angular.isDefined(_choices[testName])) {
var res = _choices[testName];
if (res === false) {
// Still loading
$timeout(function () {
methods.participate(testName, variations, callback);
}, 50);
return;
};
if (_opts.debug) {
$log.info('[sixpack] Using already chosen variation for test', testName, res);
};
$timeout(function () {
callback(res.alternative.name, res);
});
return;
}
_choices[testName] = false;
var session = _getOrInitSession();
if (_opts.debug) {
$log.info('[sixpack] Getting choice for', testName, 'out of', variations);
};
session.participate(testName, variations, function (err, res) {
if (err) {
if (_opts.debug) {
$log.warn('[sixpack] Received error', err);
};
$timeout(function () {
callback(false);
});
delete _choices[testName];
return;
};
_choices[testName] = res;
var choice = res.alternative.name;
if (_opts.debug) {
$log.info('[sixpack] Alternative chosen:', choice);
$log.debug('[sixpack] Full response', res);
};
if (!$body) {
$body = angular.element(document).find('body');
};
$body.addClass('sixpack-'+testName+' sixpack-'+testName+'-'+choice);
$timeout(function() {
callback(choice, res);
});
});
},
// Register a 'conversion'. If no testName, will call for all active tests
// Takes an optional callback that receives the raw response from sixpack (or undefined on error)
convert : function (testName, callback) {
var session = _getOrInitSession();
if (!testName) {
if (_opts.debug) {
$log.info("[sixpack] Recording conversion for all tests", _tests);
};
for (var i = 0, ii = _tests.length; i < ii; i++) {
var test = _tests[i]
, results = [];
session.convert(test, function (err, res) {
results.push(res);
if (err && _opts.debug) {
$log.warn("[sixpack] Error recording conversion for", test, err);
};
if (results.length == ii) {
if (_opts.debug) {
$log.debug('[sixpack] All results:', results);
};
if (callback) {
$timeout(function () {
callback(results);
});
}
};
});
}
} else {
if (_opts.debug) {
$log.info("[sixpack] Recording conversion for", testName);
};
session.convert(testName, function (err, res) {
if (err && _opts.debug) {
$log.warn('[sixpack] Error recording conversion:', err);
} else if (_opts.debug) {
$log.debug('[sixpack] Conversion result:', res);
};
if (callback) {
$timeout(function () {
callback(res);
});
}
});
}
}
}
return methods;
}];
})
.directive('sixpackSwitch', ['sixpack', function(sixpack) {
return {
controller : ['$element', function($element) {
var ctrl = this
, _testName
// Map of variation names to transclude fns
, _variations = {};
var _processChoice = function (choice) {
// Triggered if for some reason we get an error from sixpack,
// or optionally if a user is excluded from this test via configuration
if (!choice) {
_setContent(_variations['default']);
} else {
_setContent(_variations[choice]);
}
}
var _setContent = function (fn) {
if (!fn) {
return;
};
fn(function(clone) {
$element.replaceWith(clone);
});
}
// Pseudo-shim for '.keys' method
// Additionally, if obj has a 'default' property, sets that as the first element
// so sixpack will use it as the control
var _keys = function (obj) {
var keys = []
, prop;
for (prop in obj) {
if (!obj.hasOwnProperty(prop)) {
continue;
};
if (prop == 'default') {
keys.unshift(prop);
} else {
keys.push(prop);
}
}
return keys;
}
ctrl.registerSwitch = function (name) {
_testName = name;
sixpack.participate(_testName, _keys(_variations), _processChoice);
}
ctrl.registerVariation = function (variation, fn) {
_variations[variation] = fn;
}
return ctrl;
}],
require: 'sixpackSwitch',
link : function ($scope, $element, $attrs, ctrl) {
ctrl.registerSwitch($attrs.sixpackSwitch);
}
}
}])
// Register a variation for a test
.directive('sixpackWhen', ['$log', function ($log) {
return {
require: '^sixpackSwitch',
transclude: 'element',
link: function($scope, $element, $attrs, ctrl, transcludeFn) {
if ($attrs.sixpackWhen) {
ctrl.registerVariation($attrs.sixpackWhen, transcludeFn);
} else {
$log.debug('[sixpack] When directive initialized without a name, ignoring');
}
}
}
}])
// Register the 'default view, registered as the control variation, and
// always used if sixpack errors out or if user is excluded via configuration
.directive('sixpackDefault', function () {
return {
require: '^sixpackSwitch',
transclude: 'element',
link: function($scope, $element, $attrs, ctrl, transcludeFn) {
ctrl.registerVariation('default', transcludeFn);
}
}
})
.directive('sixpackConvert', ['sixpack', function (sixpack) {
return {
link : function ($scope, $element, $attrs) {
var test = $attrs.sixpackConvert || undefined
, eventType = $attrs.on || 'click';
$element.on(eventType, function () {
sixpack.convert(test);
});
}
}
}]);
})(window.angular, window.sixpack);

View file

@ -0,0 +1,213 @@
(function () {
// Object.assign polyfill from https://developer.mozilla.org/en/docs/Web/JavaScript/Reference/Global_Objects/Object/assign#Polyfill
Object.assign||Object.defineProperty(Object,"assign",{enumerable:!1,configurable:!0,writable:!0,value:function(e){"use strict";if(void 0===e||null===e)throw new TypeError("Cannot convert first argument to object");for(var r=Object(e),t=1;t<arguments.length;t++){var n=arguments[t];if(void 0!==n&&null!==n){n=Object(n);for(var o=Object.keys(Object(n)),a=0,c=o.length;c>a;a++){var i=o[a],b=Object.getOwnPropertyDescriptor(n,i);void 0!==b&&b.enumerable&&(r[i]=n[i])}}}return r}});
var sixpack = {base_url: "http://localhost:5000", ip_address: null, user_agent: null, timeout: 1000};
// check for node module loader
var on_node = false;
if (typeof module !== "undefined" && typeof require !== "undefined") {
on_node = true;
module.exports = sixpack;
} else {
window["sixpack"] = sixpack;
}
sixpack.generate_client_id = function () {
// from http://stackoverflow.com/questions/105034
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {
var r = Math.random()*16|0, v = c == 'x' ? r : (r&0x3|0x8);
return v.toString(16);
});
};
sixpack.Session = function (options) {
Object.assign(this, sixpack, options);
console.log("creating new session", options)
if (!this.client_id) {
this.client_id = this.generate_client_id();
}
if (!on_node) {
this.user_agent = this.user_agent || (window && window.navigator && window.navigator.userAgent);
}
};
sixpack.Session.prototype = {
participate: function(experiment_name, alternatives, traffic_fraction, force, callback) {
console.log("processing new participate", this.base_url)
if (typeof traffic_fraction === "function") {
callback = traffic_fraction;
traffic_fraction = null;
force = null;
}
else if (typeof traffic_fraction === "string") {
callback = force;
force = traffic_fraction;
traffic_fraction = null;
}
if (typeof force === "function") {
callback = force;
force = null;
}
if (!(/^[a-z0-9][a-z0-9\-_ ]*$/).test(experiment_name)) {
return callback(new Error("Bad experiment_name"));
}
if (alternatives.length < 2) {
return callback(new Error("Must specify at least 2 alternatives"));
}
for (var i = 0; i < alternatives.length; i += 1) {
if (!(/^[a-z0-9][a-z0-9\-_ ]*$/).test(alternatives[i])) {
return callback(new Error("Bad alternative name: " + alternatives[i]));
}
}
var params = {client_id: this.client_id,
experiment: experiment_name,
alternatives: alternatives};
if (!on_node && force == null) {
var regex = new RegExp("[\\?&]sixpack-force-" + experiment_name + "=([^&#]*)");
var results = regex.exec(window.location.search);
if(results != null) {
force = decodeURIComponent(results[1].replace(/\+/g, " "));
}
}
if (traffic_fraction !== null && !isNaN(traffic_fraction)) {
params.traffic_fraction = traffic_fraction;
}
if (force != null && _in_array(alternatives, force)) {
return callback(null, {"status": "ok", "alternative": {"name": force}, "experiment": {"version": 0, "name": experiment_name}, "client_id": this.client_id});
}
if (this.ip_address) {
params.ip_address = this.ip_address;
}
if (this.user_agent) {
params.user_agent = this.user_agent;
}
console.log(this.base_url, "participate")
return _request(this.base_url + "/participate", params, this.timeout, function(err, res) {
if (err) {
res = {status: "failed",
error: err,
alternative: {name: alternatives[0]}};
}
return callback(null, res);
});
},
convert: function(experiment_name, kpi, callback) {
if (typeof kpi === 'function') {
callback = kpi;
kpi = null;
}
if (!(/^[a-z0-9][a-z0-9\-_ ]*$/).test(experiment_name)) {
return callback(new Error("Bad experiment_name"));
}
var params = {client_id: this.client_id,
experiment: experiment_name};
if (this.ip_address) {
params.ip_address = this.ip_address;
}
if (this.user_agent) {
params.user_agent = this.user_agent;
}
if (kpi) {
params.kpi = kpi;
}
return _request(this.base_url + "/convert", params, this.timeout, function(err, res) {
console.log(res)
if (err) {
res = {status: "failed",
error: err,};
}
return callback(null, res);
});
}
};
var counter = 0;
var _request = function(uri, params, timeout, callback) {
var timed_out = false;
var timeout_handle = setTimeout(function () {
timed_out = true;
return callback(new Error("request timed out"));
}, timeout);
if (!on_node) {
var cb = "callback" + (++counter);
params.callback = "sixpack." + cb
sixpack[cb] = function (res) {
if (!timed_out) {
clearTimeout(timeout_handle);
return callback(null, res);
}
}
}
var url = _request_uri(uri, params);
console.log(url)
if (!on_node) {
script = document.createElement('script');
script.type = 'text/javascript';
script.src = url;
script.async = true;
document.body.appendChild(script);
} else {
var http = require('http');
var req = http.get(url, function(res) {
var body = "";
res.on('data', function(chunk) {
return body += chunk;
});
return res.on('end', function() {
var data;
if (res.statusCode == 500) {
data = {status: "failed", response: body};
} else {
data = JSON.parse(body);
}
if (!timed_out) {
clearTimeout(timeout_handle);
return callback(null, data);
}
});
});
req.on('error', function(err) {
if (!timed_out) {
clearTimeout(timeout_handle);
return callback(err);
}
});
}
};
var _request_uri = function(endpoint, params) {
var query_string = [];
var e = encodeURIComponent;
for (var key in params) {
if (params.hasOwnProperty(key)) {
var vals = params[key];
if (Object.prototype.toString.call(vals) !== '[object Array]') {
vals = [vals];
}
for (var i = 0; i < vals.length; i += 1) {
query_string.push(e(key) + '=' + e(vals[i]));
}
}
}
if (query_string.length) {
endpoint += '?' + query_string.join('&');
}
return endpoint;
};
var _in_array = function(a, v) {
for(var i = 0; i < a.length; i++) {
if(a[i] === v) {
return true;
}
}
return false;
};
})();