1
0
mirror of https://github.com/kremalicious/metamask-extension.git synced 2024-12-23 09:52:26 +01:00
metamask-extension/development/build/scripts.js
Mark Stacey 37dc19a352
Migrate Sentry settings to environment variables (#11085)
Sentry is now configured with environment variables, rather than with
hard-coded values. This makes it easier to test Sentry functionality
using a different Sentry account, as we did recently during QA of
v9.5.1.

The only change for the normal build process is the introduction of the
`SENTRY_DSN_DEV` variable, which can be set via `.metamaskrc` or via an
environment variable. This determines where error reports are sent. It
still defaults to our team Sentry account's `metamask-testing` project.

The `sentry:publish` script now requires SENTRY_ORG and SENTRY_PROJECT
to be set in order to publish release artifacts. The CircleCI
configuration has been updated with these values, so it should act the
same as it did before. Previously we had used a CLI flag to specify the
organization and project, but Sentry already natively supports these
environment variables [1].

[1]: https://docs.sentry.io/product/cli/configuration/#configuration-values
2021-05-18 13:56:22 -02:30

486 lines
14 KiB
JavaScript

const EventEmitter = require('events');
const gulp = require('gulp');
const watch = require('gulp-watch');
const source = require('vinyl-source-stream');
const buffer = require('vinyl-buffer');
const log = require('fancy-log');
const watchify = require('watchify');
const browserify = require('browserify');
const envify = require('loose-envify/custom');
const sourcemaps = require('gulp-sourcemaps');
const terser = require('gulp-terser-js');
const babelify = require('babelify');
const brfs = require('brfs');
const pify = require('pify');
const endOfStream = pify(require('end-of-stream'));
const labeledStreamSplicer = require('labeled-stream-splicer').obj;
const metamaskrc = require('rc')('metamask', {
INFURA_PROJECT_ID: process.env.INFURA_PROJECT_ID,
SEGMENT_HOST: process.env.SEGMENT_HOST,
SEGMENT_WRITE_KEY: process.env.SEGMENT_WRITE_KEY,
SEGMENT_LEGACY_WRITE_KEY: process.env.SEGMENT_LEGACY_WRITE_KEY,
SENTRY_DSN_DEV:
process.env.SENTRY_DSN_DEV ||
'https://f59f3dd640d2429d9d0e2445a87ea8e1@sentry.io/273496',
});
const { version } = require('../../package.json');
const packageJSON = require('../../package.json');
const {
createTask,
composeParallel,
composeSeries,
runInChildProcess,
} = require('./task');
module.exports = createScriptTasks;
const dependencies = Object.keys(
(packageJSON && packageJSON.dependencies) || {},
);
const materialUIDependencies = ['@material-ui/core'];
const reactDepenendencies = dependencies.filter((dep) => dep.match(/react/u));
const externalDependenciesMap = {
background: ['3box'],
ui: [...materialUIDependencies, ...reactDepenendencies],
};
function createScriptTasks({ browserPlatforms, livereload }) {
// internal tasks
const core = {
// dev tasks (live reload)
dev: createTasksForBuildJsExtension({
taskPrefix: 'scripts:core:dev',
devMode: true,
}),
testDev: createTasksForBuildJsExtension({
taskPrefix: 'scripts:core:test-live',
devMode: true,
testing: true,
}),
// built for CI tests
test: createTasksForBuildJsExtension({
taskPrefix: 'scripts:core:test',
testing: true,
}),
// production
prod: createTasksForBuildJsExtension({ taskPrefix: 'scripts:core:prod' }),
};
const deps = {
background: createTasksForBuildJsDeps({
label: 'bg-libs',
key: 'background',
}),
ui: createTasksForBuildJsDeps({ label: 'ui-libs', key: 'ui' }),
};
// high level tasks
const prod = composeParallel(deps.background, deps.ui, core.prod);
const { dev, testDev } = core;
const test = composeParallel(deps.background, deps.ui, core.test);
return { prod, dev, testDev, test };
function createTasksForBuildJsDeps({ key, label }) {
return createTask(
`scripts:deps:${key}`,
createNormalBundle({
label,
destFilepath: `${label}.js`,
modulesToExpose: externalDependenciesMap[key],
devMode: false,
browserPlatforms,
}),
);
}
function createTasksForBuildJsExtension({ taskPrefix, devMode, testing }) {
const standardBundles = [
'background',
'ui',
'phishing-detect',
'initSentry',
];
const standardSubtasks = standardBundles.map((label) => {
let extraEntries;
if (devMode && label === 'ui') {
extraEntries = ['./development/require-react-devtools.js'];
}
return createTask(
`${taskPrefix}:${label}`,
createBundleTaskForBuildJsExtensionNormal({
label,
devMode,
testing,
extraEntries,
}),
);
});
// inpage must be built before contentscript
// because inpage bundle result is included inside contentscript
const contentscriptSubtask = createTask(
`${taskPrefix}:contentscript`,
createTaskForBuildJsExtensionContentscript({ devMode, testing }),
);
// this can run whenever
const disableConsoleSubtask = createTask(
`${taskPrefix}:disable-console`,
createTaskForBuildJsExtensionDisableConsole({ devMode }),
);
// task for initiating browser livereload
const initiateLiveReload = async () => {
if (devMode) {
// trigger live reload when the bundles are updated
// this is not ideal, but overcomes the limitations:
// - run from the main process (not child process tasks)
// - after the first build has completed (thus the timeout)
// - build tasks never "complete" when run with livereload + child process
setTimeout(() => {
watch('./dist/*/*.js', (event) => {
livereload.changed(event.path);
});
}, 75e3);
}
};
// make each bundle run in a separate process
const allSubtasks = [
...standardSubtasks,
contentscriptSubtask,
disableConsoleSubtask,
].map((subtask) => runInChildProcess(subtask));
// const allSubtasks = [...standardSubtasks, contentscriptSubtask].map(subtask => (subtask))
// make a parent task that runs each task in a child thread
return composeParallel(initiateLiveReload, ...allSubtasks);
}
function createBundleTaskForBuildJsExtensionNormal({
label,
devMode,
testing,
extraEntries,
}) {
return createNormalBundle({
label,
entryFilepath: `./app/scripts/${label}.js`,
destFilepath: `${label}.js`,
extraEntries,
externalDependencies: devMode
? undefined
: externalDependenciesMap[label],
devMode,
testing,
browserPlatforms,
});
}
function createTaskForBuildJsExtensionDisableConsole({ devMode }) {
const label = 'disable-console';
return createNormalBundle({
label,
entryFilepath: `./app/scripts/${label}.js`,
destFilepath: `${label}.js`,
devMode,
browserPlatforms,
});
}
function createTaskForBuildJsExtensionContentscript({ devMode, testing }) {
const inpage = 'inpage';
const contentscript = 'contentscript';
return composeSeries(
createNormalBundle({
label: inpage,
entryFilepath: `./app/scripts/${inpage}.js`,
destFilepath: `${inpage}.js`,
externalDependencies: devMode
? undefined
: externalDependenciesMap[inpage],
devMode,
testing,
browserPlatforms,
}),
createNormalBundle({
label: contentscript,
entryFilepath: `./app/scripts/${contentscript}.js`,
destFilepath: `${contentscript}.js`,
externalDependencies: devMode
? undefined
: externalDependenciesMap[contentscript],
devMode,
testing,
browserPlatforms,
}),
);
}
}
function createNormalBundle({
destFilepath,
entryFilepath,
extraEntries = [],
modulesToExpose,
externalDependencies,
devMode,
testing,
browserPlatforms,
}) {
return async function () {
// create bundler setup and apply defaults
const buildConfiguration = createBuildConfiguration();
const { bundlerOpts, events } = buildConfiguration;
const envVars = getEnvironmentVariables({ devMode, testing });
setupBundlerDefaults(buildConfiguration, {
devMode,
envVars,
});
// set bundle entries
bundlerOpts.entries = [...extraEntries];
if (entryFilepath) {
bundlerOpts.entries.push(entryFilepath);
}
if (modulesToExpose) {
bundlerOpts.require = bundlerOpts.require.concat(modulesToExpose);
}
if (externalDependencies) {
// there doesnt seem to be a standard bify option for this
// so we'll put it here but manually call it after bundle
bundlerOpts.manualExternal = bundlerOpts.manualExternal.concat(
externalDependencies,
);
}
// instrument pipeline
events.on('configurePipeline', ({ pipeline }) => {
// convert bundle stream to gulp vinyl stream
// and ensure file contents are buffered
pipeline.get('vinyl').push(source(destFilepath));
pipeline.get('vinyl').push(buffer());
// setup bundle destination
browserPlatforms.forEach((platform) => {
const dest = `./dist/${platform}/`;
pipeline.get('dest').push(gulp.dest(dest));
});
});
await bundleIt(buildConfiguration);
};
}
function createBuildConfiguration() {
const events = new EventEmitter();
const bundlerOpts = {
entries: [],
transform: [],
plugin: [],
require: [],
// not a standard bify option
manualExternal: [],
};
return { bundlerOpts, events };
}
function setupBundlerDefaults(buildConfiguration, { devMode, envVars }) {
const { bundlerOpts } = buildConfiguration;
// devMode options
const reloadOnChange = Boolean(devMode);
const minify = Boolean(devMode) === false;
Object.assign(bundlerOpts, {
// source transforms
transform: [
// transpile top-level code
babelify,
// inline `fs.readFileSync` files
brfs,
],
// use entryFilepath for moduleIds, easier to determine origin file
fullPaths: devMode,
// for sourcemaps
debug: true,
});
// inject environment variables via node-style `process.env`
if (envVars) {
bundlerOpts.transform.push([envify(envVars), { global: true }]);
}
// setup reload on change
if (reloadOnChange) {
setupReloadOnChange(buildConfiguration);
}
if (minify) {
setupMinification(buildConfiguration);
}
// setup source maps
setupSourcemaps(buildConfiguration, { devMode });
}
function setupReloadOnChange({ bundlerOpts, events }) {
// add plugin to options
Object.assign(bundlerOpts, {
plugin: [...bundlerOpts.plugin, watchify],
// required by watchify
cache: {},
packageCache: {},
});
// instrument pipeline
events.on('configurePipeline', ({ bundleStream }) => {
// handle build error to avoid breaking build process
// (eg on syntax error)
bundleStream.on('error', (err) => {
gracefulError(err);
});
});
}
function setupMinification(buildConfiguration) {
const { events } = buildConfiguration;
events.on('configurePipeline', ({ pipeline }) => {
pipeline.get('minify').push(
terser({
mangle: {
reserved: ['MetamaskInpageProvider'],
},
sourceMap: {
content: true,
},
}),
);
});
}
function setupSourcemaps(buildConfiguration, { devMode }) {
const { events } = buildConfiguration;
events.on('configurePipeline', ({ pipeline }) => {
pipeline.get('sourcemaps:init').push(sourcemaps.init({ loadMaps: true }));
pipeline
.get('sourcemaps:write')
// Use inline source maps for development due to Chrome DevTools bug
// https://bugs.chromium.org/p/chromium/issues/detail?id=931675
.push(
devMode
? sourcemaps.write()
: sourcemaps.write('../sourcemaps', { addComment: false }),
);
});
}
async function bundleIt(buildConfiguration) {
const { bundlerOpts, events } = buildConfiguration;
const bundler = browserify(bundlerOpts);
// manually apply non-standard option
bundler.external(bundlerOpts.manualExternal);
// output build logs to terminal
bundler.on('log', log);
// forward update event (used by watchify)
bundler.on('update', () => performBundle());
await performBundle();
async function performBundle() {
// this pipeline is created for every bundle
// the labels are all the steps you can hook into
const pipeline = labeledStreamSplicer([
'vinyl',
[],
'sourcemaps:init',
[],
'minify',
[],
'sourcemaps:write',
[],
'dest',
[],
]);
const bundleStream = bundler.bundle();
// trigger build pipeline instrumentations
events.emit('configurePipeline', { pipeline, bundleStream });
// start bundle, send into pipeline
bundleStream.pipe(pipeline);
// nothing will consume pipeline, so let it flow
pipeline.resume();
await endOfStream(pipeline);
}
}
function getEnvironmentVariables({ devMode, testing }) {
const environment = getEnvironment({ devMode, testing });
if (environment === 'production' && !process.env.SENTRY_DSN) {
throw new Error('Missing SENTRY_DSN environment variable');
}
return {
METAMASK_DEBUG: devMode,
METAMASK_ENVIRONMENT: environment,
METAMASK_VERSION: version,
NODE_ENV: devMode ? 'development' : 'production',
IN_TEST: testing ? 'true' : false,
PUBNUB_SUB_KEY: process.env.PUBNUB_SUB_KEY || '',
PUBNUB_PUB_KEY: process.env.PUBNUB_PUB_KEY || '',
CONF: devMode ? metamaskrc : {},
SENTRY_DSN: process.env.SENTRY_DSN,
SENTRY_DSN_DEV: metamaskrc.SENTRY_DSN_DEV,
INFURA_PROJECT_ID: testing
? '00000000000000000000000000000000'
: metamaskrc.INFURA_PROJECT_ID,
SEGMENT_HOST: metamaskrc.SEGMENT_HOST,
// When we're in the 'production' environment we will use a specific key only set in CI
// Otherwise we'll use the key from .metamaskrc or from the environment variable. If
// the value of SEGMENT_WRITE_KEY that we envify is undefined then no events will be tracked
// in the build. This is intentional so that developers can contribute to MetaMask without
// inflating event volume.
SEGMENT_WRITE_KEY:
environment === 'production'
? process.env.SEGMENT_PROD_WRITE_KEY
: metamaskrc.SEGMENT_WRITE_KEY,
SEGMENT_LEGACY_WRITE_KEY:
environment === 'production'
? process.env.SEGMENT_PROD_LEGACY_WRITE_KEY
: metamaskrc.SEGMENT_LEGACY_WRITE_KEY,
};
}
function getEnvironment({ devMode, testing }) {
// get environment slug
if (devMode) {
return 'development';
} else if (testing) {
return 'testing';
} else if (process.env.CIRCLE_BRANCH === 'master') {
return 'production';
} else if (
/^Version-v(\d+)[.](\d+)[.](\d+)/u.test(process.env.CIRCLE_BRANCH)
) {
return 'release-candidate';
} else if (process.env.CIRCLE_BRANCH === 'develop') {
return 'staging';
} else if (process.env.CIRCLE_PULL_REQUEST) {
return 'pull-request';
}
return 'other';
}
function beep() {
process.stdout.write('\x07');
}
function gracefulError(err) {
console.warn(err);
beep();
}