kopia lustrzana https://github.com/c9/core
Merge remote-tracking branch 'origin/master' into vfs-fix-collab-code-reverting
commit
c353935450
|
@ -702,6 +702,7 @@ module.exports = function(options) {
|
|||
premium: options.project.premium,
|
||||
}
|
||||
},
|
||||
"plugins/c9.ide.abtesting/abtesting",
|
||||
{
|
||||
packagePath: "plugins/c9.ide.welcome/welcome",
|
||||
staticPrefix: staticPrefix + "/plugins/c9.ide.welcome",
|
||||
|
|
|
@ -158,7 +158,6 @@ module.exports = function(config, optimist) {
|
|||
"c9.vfs.client": true,
|
||||
"c9.cli.bridge": true,
|
||||
"c9.nodeapi": true,
|
||||
"c9.ide.experiment": true,
|
||||
"saucelabs.preview": true,
|
||||
"salesforce.sync": true,
|
||||
"salesforce.language": true
|
||||
|
@ -194,7 +193,6 @@ module.exports = function(config, optimist) {
|
|||
"./c9.vfs.server/statics",
|
||||
"./c9.analytics/mock_analytics",
|
||||
"./c9.metrics/mock_metrics",
|
||||
"./c9.ide.experiment/mock_experiment",
|
||||
{
|
||||
packagePath: "./c9.vfs.server/vfs.connect.standalone",
|
||||
workspaceDir: baseProc,
|
||||
|
|
|
@ -1,32 +1,46 @@
|
|||
var error = require("http-error");
|
||||
|
||||
var MAX_EXPIRE_INTERVAL = 5000;
|
||||
|
||||
/**
|
||||
* In memory rate limiter as connect middleware
|
||||
*/
|
||||
module.exports = ratelimit;
|
||||
|
||||
function ratelimit(key, duration, max) {
|
||||
var limit = {};
|
||||
var requests = Object.create(null); // in case there handles like 'constructor'
|
||||
setInterval(function() {
|
||||
Object.keys(requests).forEach(expireRequests);
|
||||
}, Math.min(duration * 0.75, MAX_EXPIRE_INTERVAL));
|
||||
|
||||
function expireRequests(handle) {
|
||||
var requestsForHandle = requests[handle];
|
||||
var totalToSplice = 0;
|
||||
var expireTime = Date.now() - duration;
|
||||
/* Requests are already sorted by date as they are appended, so we just loop
|
||||
until we find one that shouldn't have expired and splice them from the list */
|
||||
for (totalToSplice = 0; totalToSplice < requestsForHandle.length; totalToSplice++) {
|
||||
if (requestsForHandle[totalToSplice] >= expireTime) break;
|
||||
}
|
||||
requests[handle].splice(0, totalToSplice);
|
||||
if (requests[handle].length == 0) {
|
||||
delete requests[handle];
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
return function(req, res, next) {
|
||||
var handle = req.params[key];
|
||||
|
||||
var lim = limit[handle] || (limit[handle] = []);
|
||||
var now = Date.now();
|
||||
for (var i = 0; i < lim.length; i++) {
|
||||
if (now - lim[i] > duration) {
|
||||
lim.splice(i, 1);
|
||||
i--;
|
||||
}
|
||||
else break;
|
||||
}
|
||||
if (lim.length > max) {
|
||||
requests[handle] = requests[handle] || [];
|
||||
if (requests[handle].length >= max) {
|
||||
var err = new error.TooManyRequests("Rate limit exceeded");
|
||||
err.retryIn = duration - (Date.now() - lim[0]);
|
||||
next(err);
|
||||
return;
|
||||
err.retryIn = Math.min(duration, 5000);
|
||||
return next(err);
|
||||
}
|
||||
lim.push(Date.now());
|
||||
|
||||
requests[handle].push(Date.now());
|
||||
return next();
|
||||
};
|
||||
}
|
||||
|
|
|
@ -0,0 +1,104 @@
|
|||
"use server";
|
||||
|
||||
require("c9/inline-mocha")(module);
|
||||
|
||||
var ratelimit = require("./ratelimit");
|
||||
var assert = require("assert");
|
||||
var async = require("async");
|
||||
|
||||
describe("ratelimit", function() {
|
||||
|
||||
it("Should limit based on key", function (done) {
|
||||
var limiter = ratelimit("username", 10, 1);
|
||||
limiter({params: {username: "super"}}, null, function (err) {
|
||||
assert(!err, err);
|
||||
limiter({params: {username: "super"}}, null, function (err) {
|
||||
assert(err);
|
||||
assert.equal(err.code, 429);
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it("Should work with different keys", function (done) {
|
||||
var limiter = ratelimit("username", 10, 1);
|
||||
limiter({params: {username: "super"}}, null, function (err) {
|
||||
assert(!err, err);
|
||||
limiter({params: {username: "aloha"}}, null, function (err) {
|
||||
assert(!err, err);
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
|
||||
it("Should work again after a delay", function (done) {
|
||||
var limiter = ratelimit("username", 10, 1);
|
||||
limiter({params: {username: "super"}}, null, function (err) {
|
||||
assert(!err, err);
|
||||
setTimeout(function() {
|
||||
limiter({params: {username: "super"}}, null, function (err) {
|
||||
assert(!err, err);
|
||||
done();
|
||||
});
|
||||
}, 25);
|
||||
});
|
||||
});
|
||||
|
||||
it("Should work with many requests", function (done) {
|
||||
var MAX_REQUESTS = 5;
|
||||
var limiter = ratelimit("username", 10, MAX_REQUESTS);
|
||||
var successfulRequests = 0;
|
||||
async.times(10, function(n, next) {
|
||||
limiter({params: {username: "super"}}, null, function (err) {
|
||||
if (err) return next(err);
|
||||
successfulRequests++;
|
||||
next();
|
||||
});
|
||||
}, function (err) {
|
||||
assert.equal(successfulRequests, MAX_REQUESTS);
|
||||
setTimeout(function() {
|
||||
limiter({params: {username: "super"}}, null, function (err) {
|
||||
assert(!err, err);
|
||||
done();
|
||||
});
|
||||
}, 25);
|
||||
});
|
||||
});
|
||||
|
||||
it("Should expire keys at the correct times", function (done) {
|
||||
var limiter = ratelimit("username", 50, 2);
|
||||
async.series([
|
||||
function(next) {
|
||||
limiter({params: {username: "mario"}}, null, function(err) {
|
||||
assert(!err, err);
|
||||
setTimeout(next, 25);
|
||||
});
|
||||
},
|
||||
function (next) {
|
||||
limiter({params: {username: "mario"}}, null, function(err) {
|
||||
assert(!err, err);
|
||||
setTimeout(next, 40);
|
||||
});
|
||||
},
|
||||
function (next) {
|
||||
limiter({params: {username: "mario"}}, null, function(err) {
|
||||
assert(!err, err);
|
||||
limiter({params: {username: "mario"}}, null, function(err) {
|
||||
assert(err);
|
||||
assert.equal(err.code, 429);
|
||||
setTimeout(next, 20);
|
||||
});
|
||||
});
|
||||
},
|
||||
function (next) {
|
||||
limiter({params: {username: "mario"}}, null, function(err) {
|
||||
assert(!err, err);
|
||||
next();
|
||||
});
|
||||
}
|
||||
], done);
|
||||
});
|
||||
|
||||
});
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"name": "c9",
|
||||
"description": "New Cloud9 Client",
|
||||
"version": "3.1.2449",
|
||||
"version": "3.1.2532",
|
||||
"author": "Ajax.org B.V. <info@ajax.org>",
|
||||
"private": true,
|
||||
"main": "bin/c9",
|
||||
|
@ -71,7 +71,7 @@
|
|||
"c9.ide.language.javascript.infer": "#18acb93a3a",
|
||||
"c9.ide.language.jsonalyzer": "#d8183d84b4",
|
||||
"c9.ide.language.codeintel": "#fc867feec4",
|
||||
"c9.ide.collab": "#85ce41adac",
|
||||
"c9.ide.collab": "#eda7de90a9",
|
||||
"c9.ide.local": "#10eb45842a",
|
||||
"c9.ide.find": "#e33fbaed2f",
|
||||
"c9.ide.find.infiles": "#c0a13737ef",
|
||||
|
@ -97,7 +97,7 @@
|
|||
"c9.ide.installer": "#b2e4ba0a92",
|
||||
"c9.ide.language.python": "#aff0772c78",
|
||||
"c9.ide.language.go": "#6ce1c7a7ef",
|
||||
"c9.ide.mount": "#4c39359b87",
|
||||
"c9.ide.mount": "#6ddfd05db3",
|
||||
"c9.ide.navigate": "#5d5707058c",
|
||||
"c9.ide.newresource": "#981a408a7b",
|
||||
"c9.ide.openfiles": "#2ae85a9e33",
|
||||
|
|
|
@ -0,0 +1,86 @@
|
|||
/**
|
||||
* Server-side support for A/B testing experiments.
|
||||
*
|
||||
* Note that this plugin demands users and user IDs for most of its API,
|
||||
* where this is optional in the client-side implementation.
|
||||
*/
|
||||
main.consumes = ["analytics"];
|
||||
main.provides = ["abtesting"];
|
||||
module.exports = main;
|
||||
|
||||
function main(options, imports, register) {
|
||||
var analytics = imports["analytics"];
|
||||
var outplan = require("outplan");
|
||||
|
||||
var MS_PER_DAY = 1000 * 60 * 60 * 24;
|
||||
|
||||
outplan.configure({
|
||||
logFunction: function(e) {
|
||||
var label = e.name + " - " + e.event;
|
||||
analytics.track(label, { variation: e.params.name });
|
||||
}
|
||||
});
|
||||
|
||||
function create(name, choices, options) {
|
||||
return outplan.create(name, choices, options);
|
||||
}
|
||||
|
||||
function expose(experimentName, userId, options) {
|
||||
return outplan.expose(experimentName, userId, options);
|
||||
}
|
||||
|
||||
function isUserCreatedAfter(experimentDate, user) {
|
||||
if (!user || !user.date_add)
|
||||
throw new Error("Expected: user");
|
||||
var diffDays = (experimentDate - Date.now()) / MS_PER_DAY;
|
||||
if (diffDays > 20) {
|
||||
// Sanity check: new Date() takes zero-based month argument, one-based day argument
|
||||
throw new Error("Passed a date far in the future to isUserCreatedAfter()");
|
||||
}
|
||||
return user.date_add > experimentDate;
|
||||
}
|
||||
|
||||
register(null, {
|
||||
"abtesting": {
|
||||
/**
|
||||
* Create a new experiment. Alias for require("outplan").create()
|
||||
*
|
||||
* @param {String} name
|
||||
* The name of the experiment.
|
||||
* @param {String[]|Object[]} choices
|
||||
* A list of variations, e.g. ["A", "B"],
|
||||
* or variation objects, e.g. [{ name: "A", color: "#AAA" }, { name: "B", color: "#BBB" }]
|
||||
* @param {Object} [option]
|
||||
* Options for the experiment. This may also include
|
||||
* arguments for the distribution operator, e.g. weight.
|
||||
* @param {Function} [options.operator]
|
||||
* The distribution operator, e.g. outplan.WeightedChoice.
|
||||
*/
|
||||
create: create,
|
||||
|
||||
/**
|
||||
* Get the selected variation of an experiment, and call the log function with
|
||||
* an "expose" event to track its exposure.
|
||||
*
|
||||
* @param {String} name The experiment name.
|
||||
* @param {Number} userId A unique identifier for the current user.
|
||||
* @param {Object} [options] Options
|
||||
* @param {Boolean} [options.log=true] Whether to log an "exposure event"
|
||||
*/
|
||||
expose: expose,
|
||||
|
||||
/**
|
||||
* Helper to determine if the current user was created after the start of an experiment.
|
||||
*
|
||||
* @throws {Error} when a date in the future (~20 days from now) is passed.
|
||||
* This error is thrown as a sanity check to make sure `new Date()`
|
||||
* is called with a zero-based month argument (and a one-based day).
|
||||
*
|
||||
* @param {Date} experimentDate
|
||||
* @param {Object} user A user object
|
||||
* @param {Number} user.date_add
|
||||
*/
|
||||
isUserCreatedAfter: isUserCreatedAfter,
|
||||
}
|
||||
});
|
||||
}
|
|
@ -13,7 +13,7 @@
|
|||
</p>
|
||||
|
||||
<a href="http://status.c9.io">Status Page</a> |
|
||||
<a href="mailto:support@c9.io">Support</a> |
|
||||
<a href="https://c9.io/support">Support</a> |
|
||||
<a href="https://c9.io/dashboard.html">Dashboard</a> |
|
||||
<a href="https://c9.io">Home</a>
|
||||
</div>
|
||||
|
|
|
@ -13,7 +13,7 @@
|
|||
</p>
|
||||
|
||||
<a href="http://status.c9.io">Status Page</a> |
|
||||
<a href="mailto:support@c9.io">Support</a> |
|
||||
<a href="https://c9.io/support">Support</a> |
|
||||
<a href="https://c9.io/dashboard.html">Dashboard</a> |
|
||||
<a href="https://c9.io/">Home</a>
|
||||
</div>
|
||||
|
|
|
@ -30,7 +30,7 @@
|
|||
<% } %>
|
||||
|
||||
<a href="http://status.c9.io">Status Page</a> |
|
||||
<a href="mailto:support@c9.io">Support</a> |
|
||||
<a href="https://c9.io/support">Support</a> |
|
||||
<a href="https://c9.io/dashboard.html">Dashboard</a> |
|
||||
<a href="https://c9.io/">Home</a>
|
||||
</div>
|
||||
|
|
|
@ -29,7 +29,7 @@
|
|||
<% } %>
|
||||
|
||||
<a href="http://status.c9.io">Status Page</a> |
|
||||
<a href="mailto:support@c9.io">Support</a> |
|
||||
<a href="https://c9.io/support">Support</a> |
|
||||
<a href="https://c9.io/dashboard.html">Dashboard</a> |
|
||||
<a href="https://c9.io/">Home</a>
|
||||
</div>
|
||||
|
|
|
@ -27,7 +27,7 @@
|
|||
<% } %>
|
||||
|
||||
<a href="http://status.c9.io">Status Page</a> |
|
||||
<a href="mailto:support@c9.io">Support</a> |
|
||||
<a href="https://c9.io/support">Support</a> |
|
||||
<a href="https://c9.io/dashboard.html">Dashboard</a> |
|
||||
<a href="https://c9.io">Home</a>
|
||||
</div>
|
||||
|
|
|
@ -0,0 +1,105 @@
|
|||
/**
|
||||
* Client-side support for A/B testing experiments.
|
||||
*/
|
||||
define(function(require, exports, module) {
|
||||
"use strict";
|
||||
|
||||
main.consumes = ["Plugin", "info", "c9.analytics"];
|
||||
main.provides = ["abtesting"];
|
||||
return main;
|
||||
|
||||
function main(options, imports, register) {
|
||||
var Plugin = imports.Plugin;
|
||||
var plugin = new Plugin("Ajax.org", main.consumes);
|
||||
var info = imports.info;
|
||||
var analytics = imports["c9.analytics"];
|
||||
var outplan = require("outplan");
|
||||
|
||||
var MS_PER_DAY = 1000 * 60 * 60 * 24;
|
||||
|
||||
var userId;
|
||||
|
||||
plugin.on("load", function() {
|
||||
userId = info.getUser().id;
|
||||
outplan.configure({
|
||||
logFunction: function(e) {
|
||||
var label = e.name + " - " + e.event;
|
||||
analytics.track(label, { variation: e.params.name });
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
plugin.on("unload", function() {
|
||||
userId = undefined;
|
||||
});
|
||||
|
||||
function create(name, choices, options) {
|
||||
var experiment = outplan.create(name, choices, options);
|
||||
experiment.expose = expose.bind(null, name);
|
||||
return experiment;
|
||||
}
|
||||
|
||||
function expose(experimentName, overrideUserId, options) {
|
||||
if (overrideUserId && typeof overrideUserId === "object")
|
||||
return expose(experimentName, null, options);
|
||||
|
||||
return outplan.expose(experimentName, overrideUserId == null ? userId : overrideUserId, options);
|
||||
}
|
||||
|
||||
function isUserCreatedAfter(experimentDate) {
|
||||
var diffDays = (experimentDate - Date.now()) / MS_PER_DAY;
|
||||
if (diffDays > 20) {
|
||||
// Sanity check: new Date() takes zero-based month argument, one-based day argument
|
||||
throw new Error("Passed a date far in the future to isUserCreatedAfter()");
|
||||
}
|
||||
return info.getUser().date_add > experimentDate;
|
||||
}
|
||||
|
||||
/**
|
||||
* Support for A/B testing experiments.
|
||||
*/
|
||||
plugin.freezePublicAPI({
|
||||
/**
|
||||
* Create a new experiment. Alias for require("outplan").create()
|
||||
*
|
||||
* @param {String} name
|
||||
* The name of the experiment.
|
||||
* @param {String[]|Object[]} choices
|
||||
* A list of variations, e.g. ["A", "B"],
|
||||
* or variation objects, e.g. [{ name: "A", color: "#AAA" }, { name: "B", color: "#BBB" }]
|
||||
* @param {Object} [option]
|
||||
* Options for the experiment. This may also include
|
||||
* arguments for the distribution operator, e.g. weight.
|
||||
* @param {Function} [options.operator]
|
||||
* The distribution operator, e.g. outplan.WeightedChoice.
|
||||
*/
|
||||
create: create,
|
||||
|
||||
/**
|
||||
* Get the selected variation of an experiment, and call the log function with
|
||||
* an "expose" event to track its exposure.
|
||||
*
|
||||
* @param {String} name The experiment name.
|
||||
* @param {Number} [userId] A unique identifier for the current user.
|
||||
* @param {Object} [options] Options
|
||||
* @param {Boolean} [options.log=true] Whether to log an "exposure event"
|
||||
*/
|
||||
expose: expose,
|
||||
|
||||
/**
|
||||
* Helper to determine if the current user was created after the start of an experiment.
|
||||
*
|
||||
* @throws {Error} when a date in the future (~20 days from now) is passed.
|
||||
* This error is thrown as a sanity check to make sure `new Date()`
|
||||
* is called with a zero-based month argument (and a one-based day).
|
||||
*
|
||||
* @param {Date} experimentDate
|
||||
*/
|
||||
isUserCreatedAfter: isUserCreatedAfter,
|
||||
});
|
||||
|
||||
register(null, {
|
||||
"abtesting": plugin
|
||||
});
|
||||
}
|
||||
});
|
|
@ -1,26 +0,0 @@
|
|||
/**
|
||||
* Dummy implementation of experiments.
|
||||
*/
|
||||
"use strict";
|
||||
|
||||
plugin.consumes = [];
|
||||
plugin.provides = ["experiment"];
|
||||
|
||||
module.exports = plugin;
|
||||
|
||||
function plugin(options, imports, register) {
|
||||
|
||||
register(null, {
|
||||
"experiment": {
|
||||
configure: function() {},
|
||||
onStart: function() {
|
||||
var chain = {
|
||||
variation: function() {
|
||||
return chain;
|
||||
}
|
||||
};
|
||||
return chain;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
|
@ -41,6 +41,7 @@ define(function(require, exports, module) {
|
|||
ui: "lib/ui",
|
||||
c9: "lib/c9",
|
||||
frontdoor: "lib/frontdoor",
|
||||
outplan: "lib/outplan/dist/outplan",
|
||||
};
|
||||
|
||||
if (whitelist === "*") {
|
||||
|
@ -70,6 +71,7 @@ define(function(require, exports, module) {
|
|||
"ui",
|
||||
"emmet",
|
||||
"frontdoor",
|
||||
"outplan",
|
||||
"mocha", // TESTING
|
||||
"chai", // TESTING
|
||||
].forEach(function(name) {
|
||||
|
|
|
@ -12,7 +12,7 @@
|
|||
</p>
|
||||
|
||||
<a href="http://status.c9.io">Status Page</a> |
|
||||
<a href="mailto:support@c9.io">Support</a> |
|
||||
<a href="https://c9.io/support">Support</a> |
|
||||
<a href="https://c9.io/dashboard.html">Dashboard</a> |
|
||||
<a href="https://c9.io">Home</a>
|
||||
</div>
|
||||
|
|
Ładowanie…
Reference in New Issue