Skip to content

Commit 02fbb44

Browse files
authored
fix: introduce robust env configuration (#165)
The #81 pull request introduced a nasty bug where we weren't using prod. The TL;DR of the bug is that `sed` was replacing both the initial assignment and the pattern, such that this code: ```javascript const placeholder = 'MLAB_PROJECT_PLACEHOLDER'; return placeholder === 'MLAB_PROJECT_PLACEHOLDER' ? 'mlab-staging' : placeholder; ``` became, at runtime: ```javascript const placeholder = 'mlab-oti'; return placeholder === 'mlab-oti' ? 'mlab-staging' : placeholder; ``` This was fixed in #164 but `sed` substitution is fragile. We need more robustness. I spent time thinking about this and this is what I concluded: 1. The biggest issue of the `sed` substitution is not the substitution per se, but a *lack of debuggability* by which the substitution only happens when deploying and cannot be tested locally. 2. Any solution that only applies when merging a pull request would be morally as bad as that one. It could fail without notice. 3. We now have `scripts/build.js` so we can take advantage of that. 4. Using `scripts/build.js` to select a committed `env.prod.js` _or_ `env.staging.js` is a *bad idea* because they might drift, thus putting us *back again* into a scenario where we only know something is wrong after merging a pull request. Based on this reasoning, with this diff I am proposing what I think is the most robust solution to the problem at hand: 1. We autogenerate `dist/js/env.js` from `scripts/build.js` 2. `scripts/build.js` *requires* a command line argument identifying the target deployment environment and this value is *parsed* and we reject invalid configurations (furthermore, it is always required to specify the argument so prod and staging cannot drift) 3. `dist/js/env.js` *only* contains a *single* variable named `mlabEnvName` and that can be *either* "prod" or "staging" (and it must always be like that, so drift is *not possible*) 4. The codebase *continues* to select the correct configuration inline depending on `mlabEnvName` values (which preserves the principle of locality and ensures the code that selects the config is *close to* the code it has effects upon) 5. Now the codebase *rejects* invalid or missing `mlabEnvName` and the "not configured case" leads to *hard failure* 6. Both deployment paths use the same pattern `npm run build -- $name` where `$name` is either "prod" or "staging" (this is arguably better than before where the staging deployment relied on a default) As a bonus, deploy `console.log` aggressively. This follows the *rule of transparency*. The operator must be able to easily and clearly see what URLs we're using. Easy inspection solves many problems and empowers to perform better manual testing. (Also, a user can now more easily report bugs.)
1 parent c583f65 commit 02fbb44

File tree

6 files changed

+46
-16
lines changed

6 files changed

+46
-16
lines changed

.github/workflows/firebase-hosting-merge.yml

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -23,9 +23,7 @@ jobs:
2323
with:
2424
node-version: '20'
2525
- run: npm ci
26-
- run: npm run build
27-
- name: Substitute M-Lab project placeholder
28-
run: sed -i 's/MLAB_PROJECT_PLACEHOLDER/mlab-oti/g' dist/js/app.js
26+
- run: npm run build -- prod
2927
- uses: FirebaseExtended/action-hosting-deploy@v0
3028
with:
3129
repoToken: '${{ secrets.GITHUB_TOKEN }}'

.github/workflows/firebase-hosting-pull-request.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ jobs:
2222
with:
2323
node-version: '20'
2424
- run: npm ci
25-
- run: npm run build
25+
- run: npm run build -- staging
2626
- uses: FirebaseExtended/action-hosting-deploy@v0
2727
with:
2828
repoToken: '${{ secrets.GITHUB_TOKEN }}'

README.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ The `mlab-speedtest` repository implements the https://speed.measurementlab.net/
2222
* Install website build dependencies for your operating system and environment
2323
* Node >= v20.0 - Using NVM: `nvm install v20.0`
2424
* Install libraries - `npm install`
25-
* Build dist - `npm run build`
25+
* Build dist - `npm run build -- staging`
2626
* Serving locally - `python3 -m http.server -d dist`
2727
* Firebase tools - `npm install -g firebase-tools`
2828

@@ -37,7 +37,7 @@ Translations for this site are managed in the [Open Technology Fund's Localizati
3737
2. If the filename uses a locale code (e.g., `de_DE`), add a mapping in `scripts/po-to-json.js` so it maps to the short code (e.g., `de_DE` -> `de`)
3838
3. Add the short language code to the `supported` array in `src/js/i18n.js`
3939
* If the language is RTL, also add it to the `rtlLanguages` array
40-
4. Rebuild: `npm run build`
40+
4. Rebuild: `npm run build -- staging`
4141

4242
The build converts `.po` files to JSON in `dist/translations/`. At runtime, `i18n.js` selects the language automatically via the `?lang=` query parameter, `localStorage`, or browser preference.
4343

@@ -47,7 +47,7 @@ To preview the site locally, we recommend using Python:
4747

4848
```
4949
npm install
50-
npm run build
50+
npm run build -- staging
5151
python3 -m http.server -d dist
5252
```
5353

scripts/build.js

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,23 @@ function copyFile(src, dest) {
4949
* Main build process
5050
*/
5151
function build() {
52-
console.log('Building M-Lab Speed Test...\n');
52+
// Read command line arguments
53+
const argv = process.argv;
54+
if (argv.length !== 3) {
55+
console.log('usage: node scripts/build.js prod|staging');
56+
process.exit(1);
57+
}
58+
const mlabEnvName = argv[2];
59+
if (mlabEnvName !== 'prod' && mlabEnvName !== 'staging') {
60+
console.log('usage: node scripts/build.js prod|staging');
61+
process.exit(1);
62+
}
63+
console.log('Building M-Lab Speed Test...');
64+
console.log('');
65+
console.log(`DIST: ${DIST}`);
66+
console.log(`Environment: ${mlabEnvName}`);
67+
console.log(`SRC: ${SRC}`);
68+
console.log('');
5369

5470
// Clean dist directory
5571
if (fs.existsSync(DIST)) {
@@ -86,6 +102,11 @@ function build() {
86102
const msakPkg = path.join(NODE_MODULES, '@m-lab', 'msak', 'dist');
87103
copyFile(path.join(msakPkg, 'msak.min.js'), path.join(libDest, 'msak.min.js'));
88104

105+
// Write dist/js/env.js
106+
const envJsPath = path.join(DIST, 'js', 'env.js');
107+
console.log(`Writing ${envJsPath}...`)
108+
fs.writeFileSync(envJsPath, `const mlabEnvName = "${mlabEnvName}";\n`);
109+
89110
console.log('\nBuild complete! Output in dist/');
90111
}
91112

src/index.html

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -200,6 +200,7 @@ <h3 data-i18n>Social</h3>
200200
<script src="libraries/ndt7.js"></script>
201201
<script src="libraries/msak.min.js"></script>
202202

203+
<script src="js/env.js"></script>
203204
<script src="js/i18n.js"></script>
204205
<script src="js/gauge.js"></script>
205206
<script src="js/app.js"></script>

src/js/app.js

Lines changed: 18 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -129,12 +129,14 @@ const SpeedTest = {
129129
},
130130

131131
/**
132-
* Determine the M-Lab project based on a placeholder that is substituted
133-
* during deployment.
132+
* Determine the M-Lab project based on js/env.js, which we
133+
* generate using `./scripts/build.js`.
134134
*/
135135
mlabProject() {
136-
const placeholder = 'MLAB_PROJECT_PLACEHOLDER';
137-
return placeholder.includes('PLACEHOLDER') ? 'mlab-staging' : placeholder;
136+
if (mlabEnvName !== 'prod' && mlabEnvName !== 'staging') {
137+
throw Error('The mlabEnvName variable has not been configured!');
138+
}
139+
return mlabEnvName === 'prod' ? 'mlab-oti' : 'mlab-staging';
138140
},
139141

140142
/**
@@ -149,22 +151,30 @@ const SpeedTest = {
149151

150152
async runNdt7(sid) {
151153
const project = this.mlabProject();
154+
console.log(`[ndt7] mlabProject: ${project}`);
155+
152156
const tokenURL = `https://speed-backend.${project}.measurementlab.net/v0/token`;
157+
console.log(`[ndt7] tokenURL: ${tokenURL}`);
158+
153159
const locatePriorityURL = this.locatePriorityURLForProject(project);
160+
console.log(`[ndt7] locatePriorityURL: ${locatePriorityURL}`);
154161

155162
let token = null;
156163
try {
157164
const tokenResp = await fetch(tokenURL);
158165
const tokenData = await tokenResp.json();
159166
token = tokenData.token;
160167
} catch (err) {
161-
console.warn('Failed to fetch token, running without priority access:', err);
168+
console.warn('[ndt7] Failed to fetch token, running without priority access:', err);
162169
}
163170

171+
const loadbalancer = token ? locatePriorityURL : null;
172+
console.log(`[ndt7] loadbalancer: ${loadbalancer}`);
173+
164174
return ndt7.test(
165175
{
166176
clientRegistrationToken: token,
167-
loadbalancer: token ? locatePriorityURL : null,
177+
loadbalancer: loadbalancer,
168178
userAcceptedDataPolicy: true,
169179
uploadworkerfile: '/libraries/ndt7-upload-worker.js',
170180
downloadworkerfile: '/libraries/ndt7-download-worker.js',
@@ -176,7 +186,7 @@ const SpeedTest = {
176186
{
177187
serverChosen: (server) => {
178188
this.els.location.textContent = server.location.city + ', ' + server.location.country;
179-
console.log('Testing to:', {
189+
console.log('[ndt7] Testing to:', {
180190
machine: server.machine,
181191
locations: server.location,
182192
});
@@ -234,7 +244,7 @@ const SpeedTest = {
234244
async runMSAK(sid) {
235245
const client = new msak.Client('speed-measurementlab-net', '1.0.0', {
236246
onDownloadStart: (server) => {
237-
console.log('Server: ' + server.machine);
247+
console.log('[msak] Server: ' + server.machine);
238248
this.els.msakLocation.textContent = server.location.city + ', ' + server.location.country;
239249
},
240250
onDownloadResult: (result) => {

0 commit comments

Comments
 (0)