mirror of
https://github.com/overleaf/overleaf.git
synced 2024-11-21 20:47:08 -05:00
Merge pull request #15808 from overleaf/ab-split-test-dev-toolbar
[web] Split Test Dev Toolbar GitOrigin-RevId: 630116049a94aceb39d5afc9425b8ec6ee95b944
This commit is contained in:
parent
094fa5bc38
commit
202196dde2
11 changed files with 171 additions and 11 deletions
|
@ -58,6 +58,8 @@ async function getAssignment(req, res, splitTestName, { sync = false } = {}) {
|
||||||
if (!Features.hasFeature('saas')) {
|
if (!Features.hasFeature('saas')) {
|
||||||
assignment = _getNonSaasAssignment(splitTestName)
|
assignment = _getNonSaasAssignment(splitTestName)
|
||||||
} else {
|
} else {
|
||||||
|
await _loadSplitTestInfoInLocals(res.locals, splitTestName, req.session)
|
||||||
|
|
||||||
// Check the query string for an override, ignoring an invalid value
|
// Check the query string for an override, ignoring an invalid value
|
||||||
const queryVariant = query[splitTestName]
|
const queryVariant = query[splitTestName]
|
||||||
if (queryVariant) {
|
if (queryVariant) {
|
||||||
|
@ -91,7 +93,7 @@ async function getAssignment(req, res, splitTestName, { sync = false } = {}) {
|
||||||
splitTestName,
|
splitTestName,
|
||||||
assignment.variant
|
assignment.variant
|
||||||
)
|
)
|
||||||
await _loadSplitTestInfoInLocals(res.locals, splitTestName)
|
|
||||||
return assignment
|
return assignment
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -244,6 +246,14 @@ async function _getAssignment(
|
||||||
|
|
||||||
const splitTest = await _getSplitTest(splitTestName)
|
const splitTest = await _getSplitTest(splitTestName)
|
||||||
const currentVersion = SplitTestUtils.getCurrentVersion(splitTest)
|
const currentVersion = SplitTestUtils.getCurrentVersion(splitTest)
|
||||||
|
|
||||||
|
if (Settings.splitTest.devToolbar.enabled) {
|
||||||
|
const override = session?.splitTestOverrides?.[splitTestName]
|
||||||
|
if (override) {
|
||||||
|
return _makeAssignment(splitTest, override, currentVersion)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (!currentVersion?.active) {
|
if (!currentVersion?.active) {
|
||||||
return DEFAULT_ASSIGNMENT
|
return DEFAULT_ASSIGNMENT
|
||||||
}
|
}
|
||||||
|
@ -352,6 +362,20 @@ function getPercentile(analyticsId, splitTestName, splitTestPhase) {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function setOverrideInSession(session, splitTestName, variantName) {
|
||||||
|
if (!Settings.splitTest.devToolbar.enabled) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (!session.splitTestOverrides) {
|
||||||
|
session.splitTestOverrides = {}
|
||||||
|
}
|
||||||
|
session.splitTestOverrides[splitTestName] = variantName
|
||||||
|
}
|
||||||
|
|
||||||
|
function clearOverridesInSession(session) {
|
||||||
|
delete session.splitTestOverrides
|
||||||
|
}
|
||||||
|
|
||||||
function _getVariantFromPercentile(variants, percentile) {
|
function _getVariantFromPercentile(variants, percentile) {
|
||||||
for (const variant of variants) {
|
for (const variant of variants) {
|
||||||
for (const stripe of variant.rolloutStripes) {
|
for (const stripe of variant.rolloutStripes) {
|
||||||
|
@ -425,12 +449,14 @@ function _makeAssignment(splitTest, variant, currentVersion) {
|
||||||
return {
|
return {
|
||||||
variant,
|
variant,
|
||||||
analytics: {
|
analytics: {
|
||||||
segmentation: {
|
segmentation: splitTest
|
||||||
|
? {
|
||||||
splitTest: splitTest.name,
|
splitTest: splitTest.name,
|
||||||
variant,
|
variant,
|
||||||
phase: currentVersion.phase,
|
phase: currentVersion.phase,
|
||||||
versionNumber: currentVersion.versionNumber,
|
versionNumber: currentVersion.versionNumber,
|
||||||
},
|
}
|
||||||
|
: {},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -492,18 +518,33 @@ async function _getUser(id, splitTestName) {
|
||||||
return user
|
return user
|
||||||
}
|
}
|
||||||
|
|
||||||
async function _loadSplitTestInfoInLocals(locals, splitTestName) {
|
async function _loadSplitTestInfoInLocals(locals, splitTestName, session) {
|
||||||
const splitTest = await _getSplitTest(splitTestName)
|
const splitTest = await _getSplitTest(splitTestName)
|
||||||
if (splitTest) {
|
if (splitTest) {
|
||||||
|
const override = session?.splitTestOverrides?.[splitTestName]
|
||||||
|
|
||||||
const currentVersion = SplitTestUtils.getCurrentVersion(splitTest)
|
const currentVersion = SplitTestUtils.getCurrentVersion(splitTest)
|
||||||
if (!currentVersion.active) {
|
if (!currentVersion.active && !Settings.splitTest.devToolbar.enabled) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const phase = currentVersion.phase
|
const phase = currentVersion.phase
|
||||||
LocalsHelper.setSplitTestInfo(locals, splitTestName, {
|
const info = {
|
||||||
phase,
|
phase,
|
||||||
badgeInfo: splitTest.badgeInfo?.[phase],
|
badgeInfo: splitTest.badgeInfo?.[phase],
|
||||||
|
}
|
||||||
|
if (Settings.splitTest.devToolbar.enabled) {
|
||||||
|
info.active = currentVersion.active
|
||||||
|
info.variants = currentVersion.variants.map(variant => ({
|
||||||
|
name: variant.name,
|
||||||
|
rolloutPercent: variant.rolloutPercent,
|
||||||
|
}))
|
||||||
|
info.hasOverride = !!override
|
||||||
|
}
|
||||||
|
LocalsHelper.setSplitTestInfo(locals, splitTestName, info)
|
||||||
|
} else if (Settings.splitTest.devToolbar.enabled) {
|
||||||
|
LocalsHelper.setSplitTestInfo(locals, splitTestName, {
|
||||||
|
missing: true,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -555,6 +596,8 @@ module.exports = {
|
||||||
getAssignmentForUser: callbackify(getAssignmentForUser),
|
getAssignmentForUser: callbackify(getAssignmentForUser),
|
||||||
getActiveAssignmentsForUser: callbackify(getActiveAssignmentsForUser),
|
getActiveAssignmentsForUser: callbackify(getActiveAssignmentsForUser),
|
||||||
sessionMaintenance: callbackify(sessionMaintenance),
|
sessionMaintenance: callbackify(sessionMaintenance),
|
||||||
|
setOverrideInSession,
|
||||||
|
clearOverridesInSession,
|
||||||
promises: {
|
promises: {
|
||||||
getAssignment,
|
getAssignment,
|
||||||
getAssignmentForMongoUser,
|
getAssignmentForMongoUser,
|
||||||
|
|
|
@ -1,7 +1,9 @@
|
||||||
const { SplitTest } = require('../../models/SplitTest')
|
const { SplitTest } = require('../../models/SplitTest')
|
||||||
const SplitTestUtils = require('./SplitTestUtils')
|
const SplitTestUtils = require('./SplitTestUtils')
|
||||||
const OError = require('@overleaf/o-error')
|
const OError = require('@overleaf/o-error')
|
||||||
|
const Settings = require('@overleaf/settings')
|
||||||
const _ = require('lodash')
|
const _ = require('lodash')
|
||||||
|
const { CacheFlow } = require('cache-flow')
|
||||||
|
|
||||||
const ALPHA_PHASE = 'alpha'
|
const ALPHA_PHASE = 'alpha'
|
||||||
const BETA_PHASE = 'beta'
|
const BETA_PHASE = 'beta'
|
||||||
|
@ -315,6 +317,10 @@ async function archive(name, userId) {
|
||||||
return _saveSplitTest(splitTest)
|
return _saveSplitTest(splitTest)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function clearCache() {
|
||||||
|
await CacheFlow.reset('split-test')
|
||||||
|
}
|
||||||
|
|
||||||
function _checkNewVariantsConfiguration(variants, newVariantsConfiguration) {
|
function _checkNewVariantsConfiguration(variants, newVariantsConfiguration) {
|
||||||
const totalRolloutPercentage = _getTotalRolloutPercentage(
|
const totalRolloutPercentage = _getTotalRolloutPercentage(
|
||||||
newVariantsConfiguration
|
newVariantsConfiguration
|
||||||
|
@ -410,7 +416,7 @@ async function _saveSplitTest(splitTest) {
|
||||||
* since deleting all records in staging or prod would be very bad...
|
* since deleting all records in staging or prod would be very bad...
|
||||||
*/
|
*/
|
||||||
function _checkEnvIsSafe(operation) {
|
function _checkEnvIsSafe(operation) {
|
||||||
if (process.env.NODE_ENV !== 'development') {
|
if (Settings.splitTest.devToolbar.enabled) {
|
||||||
throw OError.tag(
|
throw OError.tag(
|
||||||
`attempted to ${operation} all feature flags outside of local env`
|
`attempted to ${operation} all feature flags outside of local env`
|
||||||
)
|
)
|
||||||
|
@ -459,4 +465,5 @@ module.exports = {
|
||||||
archive,
|
archive,
|
||||||
replaceSplitTests,
|
replaceSplitTests,
|
||||||
mergeSplitTests,
|
mergeSplitTests,
|
||||||
|
clearCache,
|
||||||
}
|
}
|
||||||
|
|
|
@ -79,9 +79,15 @@ html(
|
||||||
|
|
||||||
block body
|
block body
|
||||||
|
|
||||||
|
if (settings.splitTest.devToolbar.enabled)
|
||||||
|
div#dev-toolbar
|
||||||
|
|
||||||
block foot-scripts
|
block foot-scripts
|
||||||
each file in entrypointScripts(entrypoint)
|
each file in entrypointScripts(entrypoint)
|
||||||
script(type="text/javascript", nonce=scriptNonce, src=file)
|
script(type="text/javascript", nonce=scriptNonce, src=file)
|
||||||
|
if (settings.splitTest.devToolbar.enabled)
|
||||||
|
each file in entrypointScripts("devToolbar")
|
||||||
|
script(type="text/javascript", nonce=scriptNonce, src=file)
|
||||||
|
|
||||||
script(type="text/javascript", nonce=scriptNonce).
|
script(type="text/javascript", nonce=scriptNonce).
|
||||||
//- Look for bundle
|
//- Look for bundle
|
||||||
|
|
|
@ -271,6 +271,12 @@ module.exports = {
|
||||||
algorithm: process.env.OT_JWT_AUTH_ALG || 'HS256',
|
algorithm: process.env.OT_JWT_AUTH_ALG || 'HS256',
|
||||||
},
|
},
|
||||||
|
|
||||||
|
splitTest: {
|
||||||
|
devToolbar: {
|
||||||
|
enabled: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
splitTests: [],
|
splitTests: [],
|
||||||
|
|
||||||
// Where your instance of ShareLaTeX can be found publically. Used in emails
|
// Where your instance of ShareLaTeX can be found publically. Used in emails
|
||||||
|
@ -828,6 +834,7 @@ module.exports = {
|
||||||
overleafModuleImports: {
|
overleafModuleImports: {
|
||||||
// modules to import (an empty array for each set of modules)
|
// modules to import (an empty array for each set of modules)
|
||||||
createFileModes: [],
|
createFileModes: [],
|
||||||
|
devToolbar: [],
|
||||||
gitBridge: [],
|
gitBridge: [],
|
||||||
publishModal: [],
|
publishModal: [],
|
||||||
tprFileViewInfo: [],
|
tprFileViewInfo: [],
|
||||||
|
|
5
services/web/frontend/js/dev-toolbar.js
Normal file
5
services/web/frontend/js/dev-toolbar.js
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
import importOverleafModules from '../macros/import-overleaf-module.macro'
|
||||||
|
|
||||||
|
if (process.env.NODE_ENV === 'development') {
|
||||||
|
importOverleafModules('devToolbar')
|
||||||
|
}
|
|
@ -24,6 +24,7 @@
|
||||||
@import 'components/card.less';
|
@import 'components/card.less';
|
||||||
//@import "components/code.less";
|
//@import "components/code.less";
|
||||||
@import 'components/component-animations.less';
|
@import 'components/component-animations.less';
|
||||||
|
@import 'components/dev-toolbar.less';
|
||||||
@import 'components/dropdowns.less';
|
@import 'components/dropdowns.less';
|
||||||
@import 'components/button-groups.less';
|
@import 'components/button-groups.less';
|
||||||
@import 'components/input-groups.less';
|
@import 'components/input-groups.less';
|
||||||
|
|
|
@ -0,0 +1,78 @@
|
||||||
|
.dev-toolbar {
|
||||||
|
position: fixed;
|
||||||
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
z-index: 100;
|
||||||
|
height: 40px;
|
||||||
|
background-color: @neutral-90;
|
||||||
|
padding: 5px 12px;
|
||||||
|
|
||||||
|
button.widget {
|
||||||
|
color: @neutral-10;
|
||||||
|
margin: 0 4px;
|
||||||
|
padding: 0 4px;
|
||||||
|
border: none;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.dev-toolbar-tooltip {
|
||||||
|
a {
|
||||||
|
color: @blue-20;
|
||||||
|
&.btn {
|
||||||
|
color: @neutral-10;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.tooltip.top {
|
||||||
|
margin-top: -10px;
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
.tooltip-inner {
|
||||||
|
padding: 2px 8px 8px 8px;
|
||||||
|
text-align: left;
|
||||||
|
min-width: 300px;
|
||||||
|
max-height: 800px;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.title {
|
||||||
|
margin-top: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.test-card {
|
||||||
|
text-align: left;
|
||||||
|
color: @neutral-10;
|
||||||
|
padding: 6px;
|
||||||
|
border: 2px solid @neutral-70;
|
||||||
|
background-color: @neutral-80;
|
||||||
|
border-radius: 4px;
|
||||||
|
margin-top: 8px;
|
||||||
|
|
||||||
|
&.override {
|
||||||
|
border-color: @blue-40;
|
||||||
|
}
|
||||||
|
|
||||||
|
.test-name {
|
||||||
|
font-family: monospace;
|
||||||
|
font-size: @font-size-extra-small;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
ul {
|
||||||
|
li.variant-row {
|
||||||
|
line-height: 24px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.actions {
|
||||||
|
margin-top: 8px;
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
|
||||||
|
ul {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
}
|
|
@ -41,6 +41,7 @@
|
||||||
@import 'components/buttons.less';
|
@import 'components/buttons.less';
|
||||||
@import 'components/card.less';
|
@import 'components/card.less';
|
||||||
@import 'components/component-animations.less';
|
@import 'components/component-animations.less';
|
||||||
|
@import 'components/dev-toolbar.less';
|
||||||
@import 'components/dropdowns.less';
|
@import 'components/dropdowns.less';
|
||||||
@import 'components/button-groups.less';
|
@import 'components/button-groups.less';
|
||||||
@import 'components/input-groups.less';
|
@import 'components/input-groups.less';
|
||||||
|
|
|
@ -267,6 +267,12 @@ module.exports = {
|
||||||
test: {
|
test: {
|
||||||
counterInit: 0,
|
counterInit: 0,
|
||||||
},
|
},
|
||||||
|
|
||||||
|
splitTest: {
|
||||||
|
devToolbar: {
|
||||||
|
enabled: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports.mergeWith = function (overrides) {
|
module.exports.mergeWith = function (overrides) {
|
||||||
|
|
|
@ -46,6 +46,11 @@ describe('SplitTestHandler', function () {
|
||||||
this.Settings = {
|
this.Settings = {
|
||||||
moduleImportSequence: [],
|
moduleImportSequence: [],
|
||||||
overleaf: {},
|
overleaf: {},
|
||||||
|
splitTest: {
|
||||||
|
devToolbar: {
|
||||||
|
enabled: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
}
|
}
|
||||||
this.AnalyticsManager = {
|
this.AnalyticsManager = {
|
||||||
getIdsFromSession: sinon.stub(),
|
getIdsFromSession: sinon.stub(),
|
||||||
|
|
|
@ -13,6 +13,7 @@ const PackageVersions = require('./app/src/infrastructure/PackageVersions')
|
||||||
// Generate a hash of entry points, including modules
|
// Generate a hash of entry points, including modules
|
||||||
const entryPoints = {
|
const entryPoints = {
|
||||||
tracing: './frontend/js/tracing.js',
|
tracing: './frontend/js/tracing.js',
|
||||||
|
devToolbar: './frontend/js/dev-toolbar.js',
|
||||||
main: './frontend/js/main.js',
|
main: './frontend/js/main.js',
|
||||||
ide: './frontend/js/ide.js',
|
ide: './frontend/js/ide.js',
|
||||||
'ide-detached': './frontend/js/ide-detached.js',
|
'ide-detached': './frontend/js/ide-detached.js',
|
||||||
|
|
Loading…
Reference in a new issue