Merge pull request #15808 from overleaf/ab-split-test-dev-toolbar

[web] Split Test Dev Toolbar

GitOrigin-RevId: 630116049a94aceb39d5afc9425b8ec6ee95b944
This commit is contained in:
Alexandre Bourdin 2023-11-23 14:55:06 +01:00 committed by Copybot
parent 094fa5bc38
commit 202196dde2
11 changed files with 171 additions and 11 deletions

View file

@ -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: {
splitTest: splitTest.name,
variant,
phase: currentVersion.phase,
versionNumber: currentVersion.versionNumber,
},
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,

View file

@ -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,
}

View file

@ -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

View file

@ -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: [],

View file

@ -0,0 +1,5 @@
import importOverleafModules from '../macros/import-overleaf-module.macro'
if (process.env.NODE_ENV === 'development') {
importOverleafModules('devToolbar')
}

View file

@ -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';

View file

@ -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;
}
}

View file

@ -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';

View file

@ -267,6 +267,12 @@ module.exports = {
test: {
counterInit: 0,
},
splitTest: {
devToolbar: {
enabled: false,
},
},
}
module.exports.mergeWith = function (overrides) {

View file

@ -46,6 +46,11 @@ describe('SplitTestHandler', function () {
this.Settings = {
moduleImportSequence: [],
overleaf: {},
splitTest: {
devToolbar: {
enabled: false,
},
},
}
this.AnalyticsManager = {
getIdsFromSession: sinon.stub(),

View file

@ -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',