فهرست منبع

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

[web] Split Test Dev Toolbar

GitOrigin-RevId: 630116049a94aceb39d5afc9425b8ec6ee95b944
Alexandre Bourdin 2 سال پیش
والد
کامیت
202196dde2

+ 53 - 10
services/web/app/src/Features/SplitTests/SplitTestHandler.js

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

+ 8 - 1
services/web/app/src/Features/SplitTests/SplitTestManager.js

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

+ 6 - 0
services/web/app/views/layout-base.pug

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

+ 7 - 0
services/web/config/settings.defaults.js

@@ -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 - 0
services/web/frontend/js/dev-toolbar.js

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

+ 1 - 0
services/web/frontend/stylesheets/_style_includes.less

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

+ 78 - 0
services/web/frontend/stylesheets/components/dev-toolbar.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;
+  }
+}

+ 1 - 0
services/web/frontend/stylesheets/main-style.less

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

+ 6 - 0
services/web/test/acceptance/config/settings.test.defaults.js

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

+ 5 - 0
services/web/test/unit/src/SplitTests/SplitTestHandlerTests.js

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

+ 1 - 0
services/web/webpack.config.js

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