Expose experiments and feature flags on /__frontend_version__ (#6340)
This commit is contained in:
Родитель
472a0c1a84
Коммит
2d0a256aa3
|
@ -215,6 +215,7 @@
|
|||
"extract-text-webpack-plugin": "3.0.2",
|
||||
"fastclick": "1.0.6",
|
||||
"focus-visible": "4.1.5",
|
||||
"fs-extra": "7.0.0",
|
||||
"full-icu": "1.2.1",
|
||||
"helmet": "3.13.0",
|
||||
"history": "4.7.2",
|
||||
|
|
|
@ -24,6 +24,7 @@ import * as middleware from 'core/middleware';
|
|||
import { loadErrorPage } from 'core/reducers/errorPage';
|
||||
import { dismissSurvey } from 'core/reducers/survey';
|
||||
import { addQueryParamsToHistory, convertBoolean } from 'core/utils';
|
||||
import { viewFrontendVersionHandler } from 'core/utils/server';
|
||||
import {
|
||||
setAuthToken,
|
||||
setClientApp,
|
||||
|
@ -182,26 +183,11 @@ function baseServer(
|
|||
app.use(middleware.serveAssetsLocally());
|
||||
}
|
||||
|
||||
// Show version/commit information as JSON.
|
||||
function viewVersion(req, res) {
|
||||
// This is a magic file that gets written by deployment scripts.
|
||||
const version = path.join(config.get('basePath'), 'version.json');
|
||||
|
||||
fs.stat(version, (error) => {
|
||||
if (error) {
|
||||
log.error(`Could not stat version file ${version}: ${error}`);
|
||||
res.sendStatus(415);
|
||||
} else {
|
||||
res.setHeader('Content-Type', 'application/json');
|
||||
fs.createReadStream(version).pipe(res);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Following the ops monitoring convention, return version info at this URL.
|
||||
app.get('/__version__', viewVersion);
|
||||
// Following the ops monitoring Dockerflow convention, return version info at
|
||||
// this URL. See: https://github.com/mozilla-services/Dockerflow
|
||||
app.get('/__version__', viewFrontendVersionHandler());
|
||||
// For AMO, this helps differentiate from /__version__ served by addons-server.
|
||||
app.get('/__frontend_version__', viewVersion);
|
||||
app.get('/__frontend_version__', viewFrontendVersionHandler());
|
||||
|
||||
// Return 200 for csp reports - this will need to be overridden when deployed.
|
||||
app.post('/__cspreport__', (req, res) => res.status(200).end('ok'));
|
||||
|
|
|
@ -0,0 +1,59 @@
|
|||
/* @flow */
|
||||
import path from 'path';
|
||||
|
||||
import fs from 'fs-extra';
|
||||
import config from 'config';
|
||||
import type { $Request, $Response } from 'express';
|
||||
|
||||
import log from 'core/logger';
|
||||
|
||||
const PREFIX = 'enableFeature';
|
||||
|
||||
type ExpressHandler = (req: $Request, res: $Response) => void;
|
||||
|
||||
type ViewFrontendVersionHandlerParams = {|
|
||||
_config?: typeof config,
|
||||
_log?: typeof log,
|
||||
versionFilename?: string,
|
||||
|};
|
||||
|
||||
export const viewFrontendVersionHandler = ({
|
||||
_config = config,
|
||||
_log = log,
|
||||
// This is a magic file that gets written by deployment scripts.
|
||||
versionFilename = 'version.json',
|
||||
}: ViewFrontendVersionHandlerParams = {}): ExpressHandler => {
|
||||
const version = path.join(_config.get('basePath'), versionFilename);
|
||||
|
||||
const featureFlags = Object.keys(_config)
|
||||
.filter((key) => {
|
||||
return key.startsWith(PREFIX);
|
||||
})
|
||||
.reduce((map, key) => {
|
||||
return {
|
||||
...map,
|
||||
[key]: _config.get(key),
|
||||
};
|
||||
}, {});
|
||||
|
||||
const experiments = _config.get('experiments');
|
||||
|
||||
return (req: $Request, res: $Response) => {
|
||||
fs.stat(version, async (error) => {
|
||||
if (error) {
|
||||
_log.error(
|
||||
`Could not stat version file ${version}: ${error.toString()}`,
|
||||
);
|
||||
res.sendStatus(415);
|
||||
} else {
|
||||
const versionJson = await fs.readJson(version);
|
||||
|
||||
res.json({
|
||||
...versionJson,
|
||||
experiments,
|
||||
feature_flags: featureFlags,
|
||||
});
|
||||
}
|
||||
});
|
||||
};
|
||||
};
|
|
@ -0,0 +1,6 @@
|
|||
{
|
||||
"build": "https://circleci.com/gh/mozilla/addons-frontend/10080",
|
||||
"commit": "afb29ad060fcdb84e300ebb16aea69cee06d4d5e",
|
||||
"source": "https://github.com/mozilla/addons-frontend",
|
||||
"version": ""
|
||||
}
|
|
@ -0,0 +1,82 @@
|
|||
import path from 'path';
|
||||
|
||||
import fs from 'fs-extra';
|
||||
import MockExpressResponse from 'mock-express-response';
|
||||
|
||||
import { viewFrontendVersionHandler } from 'core/utils/server';
|
||||
import { getFakeConfig } from 'tests/unit/helpers';
|
||||
|
||||
describe(__filename, () => {
|
||||
describe('viewFrontendVersionHandler', () => {
|
||||
const basePath = path.join(__dirname, 'fixtures');
|
||||
const versionJson = fs.readJsonSync(path.join(basePath, 'version.json'));
|
||||
|
||||
it('exposes the version.json file', (done) => {
|
||||
const _config = getFakeConfig({ basePath });
|
||||
const handler = viewFrontendVersionHandler({ _config });
|
||||
|
||||
const res = new MockExpressResponse();
|
||||
handler(null, res);
|
||||
|
||||
res.on('finish', () => {
|
||||
expect(res.statusCode).toEqual(200);
|
||||
expect(res.get('content-type')).toEqual(
|
||||
'application/json; charset=utf-8',
|
||||
);
|
||||
expect(res._getJSON()).toMatchObject(versionJson);
|
||||
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('exposes the experiments and feature flags', (done) => {
|
||||
const experiments = {
|
||||
ab_test_1: true,
|
||||
};
|
||||
const featureFlags = {
|
||||
enableFeatureFoo001: true,
|
||||
enableFeatureBar123: false,
|
||||
};
|
||||
|
||||
const _config = getFakeConfig(
|
||||
{ basePath, experiments, ...featureFlags },
|
||||
{ allowUnknownKeys: true },
|
||||
);
|
||||
const handler = viewFrontendVersionHandler({ _config });
|
||||
|
||||
const res = new MockExpressResponse();
|
||||
handler(null, res);
|
||||
|
||||
res.on('finish', () => {
|
||||
expect(res._getJSON()).toMatchObject({
|
||||
...versionJson,
|
||||
experiments,
|
||||
feature_flags: {
|
||||
...featureFlags,
|
||||
},
|
||||
});
|
||||
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('returns a 415 and logs an error when file does not exist', (done) => {
|
||||
const _config = getFakeConfig({ basePath: '/some/invalid/path' });
|
||||
const _log = {
|
||||
error: sinon.stub(),
|
||||
};
|
||||
|
||||
const handler = viewFrontendVersionHandler({ _config, _log });
|
||||
|
||||
const res = new MockExpressResponse();
|
||||
handler(null, res);
|
||||
|
||||
res.on('finish', () => {
|
||||
expect(res.statusCode).toEqual(415);
|
||||
sinon.assert.calledOnce(_log.error);
|
||||
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
|
@ -442,9 +442,12 @@ export function createFakeUserAbuseReport({
|
|||
// if (fakeConfig.get('isDevelopment')) {
|
||||
// ...
|
||||
// }
|
||||
export const getFakeConfig = (params = {}) => {
|
||||
export const getFakeConfig = (
|
||||
params = {},
|
||||
{ allowUnknownKeys = false } = {},
|
||||
) => {
|
||||
for (const key of Object.keys(params)) {
|
||||
if (!config.has(key)) {
|
||||
if (!config.has(key) && !allowUnknownKeys) {
|
||||
// This will help alert us when a test accidentally relies
|
||||
// on an invalid config key.
|
||||
throw new Error(
|
||||
|
|
|
@ -11,6 +11,7 @@ import { put, takeLatest } from 'redux-saga/effects';
|
|||
/* eslint-enable import/order */
|
||||
|
||||
import {
|
||||
getFakeConfig,
|
||||
matchingSagaAction,
|
||||
shallowUntilTarget,
|
||||
unexpectedSuccess,
|
||||
|
@ -274,4 +275,22 @@ describe(__filename, () => {
|
|||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getFakeConfig', () => {
|
||||
it('throws an error when key is invalid', () => {
|
||||
expect(() => {
|
||||
getFakeConfig({ thisIsAnInvalidKey: true });
|
||||
}).toThrow(/this key is invalid/);
|
||||
});
|
||||
|
||||
it('does not throw when key is invalid and allowUnknownKeys is set to true', () => {
|
||||
const value = 'some value';
|
||||
const config = getFakeConfig(
|
||||
{ thisIsAnInvalidKey: value },
|
||||
{ allowUnknownKeys: true },
|
||||
);
|
||||
|
||||
expect(config.get('thisIsAnInvalidKey')).toEqual(value);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -0,0 +1,6 @@
|
|||
{
|
||||
"build": "https://circleci.com/gh/mozilla/addons-frontend/10080",
|
||||
"commit": "afb29ad060fcdb84e300ebb16aea69cee06d4d5e",
|
||||
"source": "https://github.com/mozilla/addons-frontend",
|
||||
"version": ""
|
||||
}
|
18
yarn.lock
18
yarn.lock
|
@ -4840,6 +4840,14 @@ fs-copy-file-sync@^1.1.1:
|
|||
version "1.1.1"
|
||||
resolved "https://registry.yarnpkg.com/fs-copy-file-sync/-/fs-copy-file-sync-1.1.1.tgz#11bf32c096c10d126e5f6b36d06eece776062918"
|
||||
|
||||
fs-extra@7.0.0:
|
||||
version "7.0.0"
|
||||
resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-7.0.0.tgz#8cc3f47ce07ef7b3593a11b9fb245f7e34c041d6"
|
||||
dependencies:
|
||||
graceful-fs "^4.1.2"
|
||||
jsonfile "^4.0.0"
|
||||
universalify "^0.1.0"
|
||||
|
||||
fs-extra@^0.30.0:
|
||||
version "0.30.0"
|
||||
resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-0.30.0.tgz#f233ffcc08d4da7d432daa449776989db1df93f0"
|
||||
|
@ -6909,6 +6917,12 @@ jsonfile@^2.1.0:
|
|||
optionalDependencies:
|
||||
graceful-fs "^4.1.6"
|
||||
|
||||
jsonfile@^4.0.0:
|
||||
version "4.0.0"
|
||||
resolved "https://registry.yarnpkg.com/jsonfile/-/jsonfile-4.0.0.tgz#8771aae0799b64076b76640fca058f9c10e33ecb"
|
||||
optionalDependencies:
|
||||
graceful-fs "^4.1.6"
|
||||
|
||||
jsonify@~0.0.0:
|
||||
version "0.0.0"
|
||||
resolved "https://registry.yarnpkg.com/jsonify/-/jsonify-0.0.0.tgz#2c74b6ee41d93ca51b7b5aaee8f503631d252a73"
|
||||
|
@ -12099,6 +12113,10 @@ unist-util-visit@^1.1.0:
|
|||
dependencies:
|
||||
unist-util-is "^2.1.1"
|
||||
|
||||
universalify@^0.1.0:
|
||||
version "0.1.2"
|
||||
resolved "https://registry.yarnpkg.com/universalify/-/universalify-0.1.2.tgz#b646f69be3942dabcecc9d6639c80dc105efaa66"
|
||||
|
||||
unpipe@1.0.0, unpipe@~1.0.0:
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/unpipe/-/unpipe-1.0.0.tgz#b2bf4ee8514aae6165b4817829d21b2ef49904ec"
|
||||
|
|
Загрузка…
Ссылка в новой задаче