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')) {
|
||||
assignment = _getNonSaasAssignment(splitTestName)
|
||||
} else {
|
||||
await _loadSplitTestInfoInLocals(res.locals, splitTestName, req.session)
|
||||
|
||||
// Check the query string for an override, ignoring an invalid value
|
||||
const queryVariant = query[splitTestName]
|
||||
if (queryVariant) {
|
||||
|
@ -91,7 +93,7 @@ async function getAssignment(req, res, splitTestName, { sync = false } = {}) {
|
|||
splitTestName,
|
||||
assignment.variant
|
||||
)
|
||||
await _loadSplitTestInfoInLocals(res.locals, splitTestName)
|
||||
|
||||
return assignment
|
||||
}
|
||||
|
||||
|
@ -244,6 +246,14 @@ async function _getAssignment(
|
|||
|
||||
const splitTest = await _getSplitTest(splitTestName)
|
||||
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) {
|
||||
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) {
|
||||
for (const variant of variants) {
|
||||
for (const stripe of variant.rolloutStripes) {
|
||||
|
@ -425,12 +449,14 @@ function _makeAssignment(splitTest, variant, currentVersion) {
|
|||
return {
|
||||
variant,
|
||||
analytics: {
|
||||
segmentation: {
|
||||
segmentation: splitTest
|
||||
? {
|
||||
splitTest: splitTest.name,
|
||||
variant,
|
||||
phase: currentVersion.phase,
|
||||
versionNumber: currentVersion.versionNumber,
|
||||
},
|
||||
}
|
||||
: {},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
@ -492,18 +518,33 @@ async function _getUser(id, splitTestName) {
|
|||
return user
|
||||
}
|
||||
|
||||
async function _loadSplitTestInfoInLocals(locals, splitTestName) {
|
||||
async function _loadSplitTestInfoInLocals(locals, splitTestName, session) {
|
||||
const splitTest = await _getSplitTest(splitTestName)
|
||||
if (splitTest) {
|
||||
const override = session?.splitTestOverrides?.[splitTestName]
|
||||
|
||||
const currentVersion = SplitTestUtils.getCurrentVersion(splitTest)
|
||||
if (!currentVersion.active) {
|
||||
if (!currentVersion.active && !Settings.splitTest.devToolbar.enabled) {
|
||||
return
|
||||
}
|
||||
|
||||
const phase = currentVersion.phase
|
||||
LocalsHelper.setSplitTestInfo(locals, splitTestName, {
|
||||
const info = {
|
||||
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),
|
||||
getActiveAssignmentsForUser: callbackify(getActiveAssignmentsForUser),
|
||||
sessionMaintenance: callbackify(sessionMaintenance),
|
||||
setOverrideInSession,
|
||||
clearOverridesInSession,
|
||||
promises: {
|
||||
getAssignment,
|
||||
getAssignmentForMongoUser,
|
||||
|
|
|
@ -1,7 +1,9 @@
|
|||
const { SplitTest } = require('../../models/SplitTest')
|
||||
const SplitTestUtils = require('./SplitTestUtils')
|
||||
const OError = require('@overleaf/o-error')
|
||||
const Settings = require('@overleaf/settings')
|
||||
const _ = require('lodash')
|
||||
const { CacheFlow } = require('cache-flow')
|
||||
|
||||
const ALPHA_PHASE = 'alpha'
|
||||
const BETA_PHASE = 'beta'
|
||||
|
@ -315,6 +317,10 @@ async function archive(name, userId) {
|
|||
return _saveSplitTest(splitTest)
|
||||
}
|
||||
|
||||
async function clearCache() {
|
||||
await CacheFlow.reset('split-test')
|
||||
}
|
||||
|
||||
function _checkNewVariantsConfiguration(variants, newVariantsConfiguration) {
|
||||
const totalRolloutPercentage = _getTotalRolloutPercentage(
|
||||
newVariantsConfiguration
|
||||
|
@ -410,7 +416,7 @@ async function _saveSplitTest(splitTest) {
|
|||
* since deleting all records in staging or prod would be very bad...
|
||||
*/
|
||||
function _checkEnvIsSafe(operation) {
|
||||
if (process.env.NODE_ENV !== 'development') {
|
||||
if (Settings.splitTest.devToolbar.enabled) {
|
||||
throw OError.tag(
|
||||
`attempted to ${operation} all feature flags outside of local env`
|
||||
)
|
||||
|
@ -459,4 +465,5 @@ module.exports = {
|
|||
archive,
|
||||
replaceSplitTests,
|
||||
mergeSplitTests,
|
||||
clearCache,
|
||||
}
|
||||
|
|
|
@ -79,9 +79,15 @@ html(
|
|||
|
||||
block body
|
||||
|
||||
if (settings.splitTest.devToolbar.enabled)
|
||||
div#dev-toolbar
|
||||
|
||||
block foot-scripts
|
||||
each file in entrypointScripts(entrypoint)
|
||||
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).
|
||||
//- Look for bundle
|
||||
|
|
|
@ -271,6 +271,12 @@ module.exports = {
|
|||
algorithm: process.env.OT_JWT_AUTH_ALG || 'HS256',
|
||||
},
|
||||
|
||||
splitTest: {
|
||||
devToolbar: {
|
||||
enabled: false,
|
||||
},
|
||||
},
|
||||
|
||||
splitTests: [],
|
||||
|
||||
// Where your instance of ShareLaTeX can be found publically. Used in emails
|
||||
|
@ -828,6 +834,7 @@ module.exports = {
|
|||
overleafModuleImports: {
|
||||
// modules to import (an empty array for each set of modules)
|
||||
createFileModes: [],
|
||||
devToolbar: [],
|
||||
gitBridge: [],
|
||||
publishModal: [],
|
||||
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/code.less";
|
||||
@import 'components/component-animations.less';
|
||||
@import 'components/dev-toolbar.less';
|
||||
@import 'components/dropdowns.less';
|
||||
@import 'components/button-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/card.less';
|
||||
@import 'components/component-animations.less';
|
||||
@import 'components/dev-toolbar.less';
|
||||
@import 'components/dropdowns.less';
|
||||
@import 'components/button-groups.less';
|
||||
@import 'components/input-groups.less';
|
||||
|
|
|
@ -267,6 +267,12 @@ module.exports = {
|
|||
test: {
|
||||
counterInit: 0,
|
||||
},
|
||||
|
||||
splitTest: {
|
||||
devToolbar: {
|
||||
enabled: false,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
module.exports.mergeWith = function (overrides) {
|
||||
|
|
|
@ -46,6 +46,11 @@ describe('SplitTestHandler', function () {
|
|||
this.Settings = {
|
||||
moduleImportSequence: [],
|
||||
overleaf: {},
|
||||
splitTest: {
|
||||
devToolbar: {
|
||||
enabled: false,
|
||||
},
|
||||
},
|
||||
}
|
||||
this.AnalyticsManager = {
|
||||
getIdsFromSession: sinon.stub(),
|
||||
|
|
|
@ -13,6 +13,7 @@ const PackageVersions = require('./app/src/infrastructure/PackageVersions')
|
|||
// Generate a hash of entry points, including modules
|
||||
const entryPoints = {
|
||||
tracing: './frontend/js/tracing.js',
|
||||
devToolbar: './frontend/js/dev-toolbar.js',
|
||||
main: './frontend/js/main.js',
|
||||
ide: './frontend/js/ide.js',
|
||||
'ide-detached': './frontend/js/ide-detached.js',
|
||||
|
|
Loading…
Reference in a new issue