@financial-times/dotcom-build-sass 9.2.1 → 9.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +17 -0
- package/dist/node/index.js +1 -1
- package/dist/node/monitored-sass-loader.d.ts +2 -0
- package/dist/node/monitored-sass-loader.js +208 -0
- package/dist/tsconfig.tsbuildinfo +14 -2
- package/package.json +1 -1
- package/src/index.ts +1 -1
- package/src/monitored-sass-loader.ts +229 -0
package/README.md
CHANGED
@@ -71,3 +71,20 @@ The CSS loader has `@import` and `url()` resolution disabled as these should be
|
|
71
71
|
| `webpackImporter` | Boolean | `false` | See https://github.com/webpack-contrib/sass-loader#webpackimporter |
|
72
72
|
| `prependData` | String | `''` | See https://webpack.js.org/loaders/sass-loader/#prependdata |
|
73
73
|
| `includePaths` | String[] | `[]` | See https://sass-lang.com/documentation/js-api#includepaths |
|
74
|
+
|
75
|
+
## Sass build monitoring
|
76
|
+
|
77
|
+
Sass build times are stored locally and remotely, where your project sets relevant API keys. Alternatively, you may turn both these features off using environment variable.
|
78
|
+
|
79
|
+
- Local reporting: A running total of your local Sass build times are stored in a temporary file on your machine. This statistic is reported periodically for your interest, along with a prompt to support FT efforts to move away from Sass.
|
80
|
+
- Alongside this, your local Sass build times are sent to the [biz-ops metrics api](https://github.com/Financial-Times/biz-ops-metrics-api), provided the below environment variables are set.
|
81
|
+
|
82
|
+
|
83
|
+
| Environment Variable | Required | Default | Description |
|
84
|
+
|--------------------------------------------|------------|------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
85
|
+
| `FT_SASS_STATS_NOTICE` | no | `throttle` | How often to log Sass statistics out to terminal. One of `throttle`, `never`, `always` |
|
86
|
+
| `FT_SASS_STATS_NOTICE_THROTTLE_SECONDS` | no | `1800` | How many seconds to wait between logging Sass statistics out to terminal. |
|
87
|
+
| `FT_SASS_STATS_NOTICE_THROTTLE_PERCENTAGE` | no | `30` | A percentage increase in total Sass build time in which to log out statistics to the terminal regardless of time. |
|
88
|
+
| `FT_SASS_STATS_MONITOR` | no | `off` | Set to `on` to send Sass build time statistics to [biz-ops metrics api](https://github.com/Financial-Times/biz-ops-metrics-api) Requires `FT_SASS_BIZ_OPS_API_KEY` and `FT_SASS_BIZ_OPS_SYSTEM_CODE`. |
|
89
|
+
| `FT_SASS_BIZ_OPS_API_KEY` | no | `` | A [Biz-Ops Metrics API Key](https://github.com/Financial-Times/biz-ops-metrics-api/blob/main/docs/API_DEFINITION.md#authentication) for your system. |
|
90
|
+
| `FT_SASS_BIZ_OPS_SYSTEM_CODE` | no | `` | The [biz-ops](https://biz-ops.in.ft.com/) system code of your project. Use `page-kit` if your system does not have a biz-ops code yet. |
|
package/dist/node/index.js
CHANGED
@@ -101,7 +101,7 @@ class PageKitSassPlugin {
|
|
101
101
|
// Enable use of Sass for CSS preprocessing
|
102
102
|
// https://github.com/webpack-contrib/sass-loader
|
103
103
|
{
|
104
|
-
loader: require.resolve('sass-loader'),
|
104
|
+
loader: require.resolve('./monitored-sass-loader'),
|
105
105
|
options: sassLoaderOptions
|
106
106
|
}
|
107
107
|
]
|
@@ -0,0 +1,208 @@
|
|
1
|
+
"use strict";
|
2
|
+
var __classPrivateFieldGet = (this && this.__classPrivateFieldGet) || function (receiver, privateMap) {
|
3
|
+
if (!privateMap.has(receiver)) {
|
4
|
+
throw new TypeError("attempted to get private field on non-instance");
|
5
|
+
}
|
6
|
+
return privateMap.get(receiver);
|
7
|
+
};
|
8
|
+
var __classPrivateFieldSet = (this && this.__classPrivateFieldSet) || function (receiver, privateMap, value) {
|
9
|
+
if (!privateMap.has(receiver)) {
|
10
|
+
throw new TypeError("attempted to set private field on non-instance");
|
11
|
+
}
|
12
|
+
privateMap.set(receiver, value);
|
13
|
+
return value;
|
14
|
+
};
|
15
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
16
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
17
|
+
};
|
18
|
+
var _monitorRemotely, _noticeStrategies, _noticeStrategy, _noticeThrottleSeconds, _noticeThrottlePercentage, _stats, _directory, _file, _startTime, _endTime, _read, _write, _report;
|
19
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
20
|
+
const fs_1 = __importDefault(require("fs"));
|
21
|
+
const path_1 = __importDefault(require("path"));
|
22
|
+
const os_1 = __importDefault(require("os"));
|
23
|
+
const sass_loader_1 = __importDefault(require("sass-loader"));
|
24
|
+
const https_1 = __importDefault(require("https"));
|
25
|
+
const logError = (message) => {
|
26
|
+
// eslint-disable-next-line no-console
|
27
|
+
console.log(`\n⛔️😭dotcom-build-sass: ${message}. Please report to #origami-support in Slack, so we can help move us away from Sass.\n`);
|
28
|
+
};
|
29
|
+
class SassStats {
|
30
|
+
constructor() {
|
31
|
+
_monitorRemotely.set(this, process.env.FT_SASS_STATS_MONITOR === 'on');
|
32
|
+
_noticeStrategies.set(this, ['throttle', 'never', 'always']);
|
33
|
+
_noticeStrategy.set(this, __classPrivateFieldGet(this, _noticeStrategies).includes(process.env.FT_SASS_STATS_NOTICE)
|
34
|
+
? process.env.FT_SASS_STATS_NOTICE
|
35
|
+
: 'throttle');
|
36
|
+
_noticeThrottleSeconds.set(this, typeof process.env.FT_SASS_STATS_NOTICE_THROTTLE_SECONDS === 'number'
|
37
|
+
? process.env.FT_SASS_STATS_NOTICE_THROTTLE_SECONDS
|
38
|
+
: 60 * 60 * 0.5); // show throttled notice given 30 mins since last notice
|
39
|
+
_noticeThrottlePercentage.set(this, typeof process.env.FT_SASS_STATS_NOTICE_THROTTLE_PERCENTAGE === 'number'
|
40
|
+
? process.env.FT_SASS_STATS_NOTICE_THROTTLE_PERCENTAGE
|
41
|
+
: 30); // show throttled notice given a 30% increase
|
42
|
+
_stats.set(this, { totalTime: 0, noticeDate: null, totalTimeAtLastNotice: 0 });
|
43
|
+
_directory.set(this, path_1.default.join(os_1.default.tmpdir(), 'dotcom-build-sass'));
|
44
|
+
_file.set(this, path_1.default.join(__classPrivateFieldGet(this, _directory), 'sass-stats.json'));
|
45
|
+
_startTime.set(this, void 0);
|
46
|
+
_endTime.set(this, void 0);
|
47
|
+
this.start = () => {
|
48
|
+
__classPrivateFieldGet(this, _read).call(this);
|
49
|
+
__classPrivateFieldSet(this, _startTime, performance.now());
|
50
|
+
};
|
51
|
+
this.end = () => {
|
52
|
+
__classPrivateFieldSet(this, _endTime, performance.now());
|
53
|
+
const updatedTotal = (__classPrivateFieldGet(this, _stats).totalTime += __classPrivateFieldGet(this, _endTime) - __classPrivateFieldGet(this, _startTime));
|
54
|
+
__classPrivateFieldGet(this, _write).call(this, { totalTime: updatedTotal });
|
55
|
+
};
|
56
|
+
_read.set(this, () => {
|
57
|
+
try {
|
58
|
+
// Restore stats from a temporary file if it exists.
|
59
|
+
// Reading from disk ensures that we can track stats across builds.
|
60
|
+
const statsFile = fs_1.default.readFileSync(__classPrivateFieldGet(this, _file), 'utf-8');
|
61
|
+
__classPrivateFieldSet(this, _stats, JSON.parse(statsFile));
|
62
|
+
}
|
63
|
+
catch (_a) { }
|
64
|
+
return __classPrivateFieldGet(this, _stats);
|
65
|
+
});
|
66
|
+
_write.set(this, (stats) => {
|
67
|
+
__classPrivateFieldSet(this, _stats, Object.assign(__classPrivateFieldGet(this, _stats), stats));
|
68
|
+
fs_1.default.writeFileSync(__classPrivateFieldGet(this, _file), JSON.stringify(__classPrivateFieldGet(this, _stats)));
|
69
|
+
});
|
70
|
+
this.sendMetric = () => {
|
71
|
+
if (!__classPrivateFieldGet(this, _monitorRemotely)) {
|
72
|
+
return;
|
73
|
+
}
|
74
|
+
if (!process.env.FT_SASS_BIZ_OPS_API_KEY) {
|
75
|
+
logError('We couldn\'t share your Sass build time, we\'re missing the environment variable "FT_SASS_BIZ_OPS_API_KEY". Please contact #origami-support with any questions.');
|
76
|
+
return;
|
77
|
+
}
|
78
|
+
if (!process.env.FT_SASS_BIZ_OPS_SYSTEM_CODE) {
|
79
|
+
logError('We couldn\'t share your Sass build time, we\'re missing the environment variable "FT_SASS_BIZ_OPS_SYSTEM_CODE". Please contact #origami-support with any questions.');
|
80
|
+
return;
|
81
|
+
}
|
82
|
+
const date = new Date();
|
83
|
+
const postData = JSON.stringify({
|
84
|
+
type: 'System',
|
85
|
+
metric: 'sass-build-time',
|
86
|
+
value: (__classPrivateFieldGet(this, _endTime) - __classPrivateFieldGet(this, _startTime)) / 1000,
|
87
|
+
date: date.toISOString(),
|
88
|
+
code: process.env.FT_SASS_BIZ_OPS_SYSTEM_CODE,
|
89
|
+
metadata: {
|
90
|
+
'node-env': process.env.NODE_ENV
|
91
|
+
}
|
92
|
+
});
|
93
|
+
const options = {
|
94
|
+
hostname: 'api.ft.com',
|
95
|
+
port: 443,
|
96
|
+
path: '/biz-ops-metrics/metric/add',
|
97
|
+
method: 'POST',
|
98
|
+
headers: {
|
99
|
+
'x-api-key': process.env.FT_SASS_BIZ_OPS_API_KEY,
|
100
|
+
'client-id': 'page-kit',
|
101
|
+
'Content-Type': 'application/json',
|
102
|
+
'Content-Length': postData.length
|
103
|
+
}
|
104
|
+
};
|
105
|
+
const request = https_1.default
|
106
|
+
.request(options, (response) => {
|
107
|
+
if (response.statusCode !== 200) {
|
108
|
+
logError(`We couldn\'t send your Sass build time metrics to biz-ops. Status code: ${response.statusCode}.`);
|
109
|
+
}
|
110
|
+
})
|
111
|
+
.on('error', (error) => {
|
112
|
+
logError(`We couldn\'t send your Sass build time metrics to biz-ops. Error: ${error}.`);
|
113
|
+
});
|
114
|
+
request.write(postData);
|
115
|
+
request.end();
|
116
|
+
};
|
117
|
+
this.reportAccordingToNoticeStrategy = () => {
|
118
|
+
let shouldReport;
|
119
|
+
switch (__classPrivateFieldGet(this, _noticeStrategy)) {
|
120
|
+
case 'never':
|
121
|
+
shouldReport = false;
|
122
|
+
break;
|
123
|
+
case 'always':
|
124
|
+
shouldReport = true;
|
125
|
+
break;
|
126
|
+
case 'throttle':
|
127
|
+
// Throttle notices to show a limited number per hour, or if the total sass build time
|
128
|
+
// has increased by a significant percentage. This favours more frequent reports to begin with.
|
129
|
+
const noticeTimeThrottle = Date.now() >= __classPrivateFieldGet(this, _stats).noticeDate + __classPrivateFieldGet(this, _noticeThrottleSeconds) * 1000;
|
130
|
+
const percentageTotalTimeThrottle = __classPrivateFieldGet(this, _stats).totalTime > 0 &&
|
131
|
+
(__classPrivateFieldGet(this, _stats).totalTime / __classPrivateFieldGet(this, _stats).totalTimeAtLastNotice - 1) * 100 >= __classPrivateFieldGet(this, _noticeThrottlePercentage); // % increase
|
132
|
+
shouldReport = !__classPrivateFieldGet(this, _stats).noticeDate || noticeTimeThrottle || percentageTotalTimeThrottle;
|
133
|
+
break;
|
134
|
+
default:
|
135
|
+
break;
|
136
|
+
}
|
137
|
+
if (shouldReport) {
|
138
|
+
__classPrivateFieldGet(this, _report).call(this);
|
139
|
+
}
|
140
|
+
};
|
141
|
+
_report.set(this, () => {
|
142
|
+
const seconds = __classPrivateFieldGet(this, _stats).totalTime / 1000;
|
143
|
+
const minutes = seconds / 60;
|
144
|
+
const hours = seconds / 3600;
|
145
|
+
const time = hours > 1
|
146
|
+
? `${hours.toFixed(1)} hours`
|
147
|
+
: minutes > 1
|
148
|
+
? `${minutes.toFixed(0)} minutes`
|
149
|
+
: `${seconds.toFixed(0)} seconds`;
|
150
|
+
const emoji = hours > 2 ? ['🔥', '😭', '😱'] : hours >= 1 ? ['🔥', '😱'] : minutes > 10 ? ['⏱️', '😬'] : ['⏱️'];
|
151
|
+
let cta = `Share your pain in Slack #sass-to-css, and help fix that! 🎉\n` +
|
152
|
+
`https://origami.ft.com/blog/2024/01/24/sass-build-times/\n\n`;
|
153
|
+
if (!__classPrivateFieldGet(this, _monitorRemotely)) {
|
154
|
+
cta =
|
155
|
+
`Help us improve build times by setting the "FT_SASS_STATS_MONITOR" environment variable.\n` +
|
156
|
+
`https://github.com/Financial-Times/biz-ops-metrics-api/blob/main/docs/API_DEFINITION.md#sass-build-monitoring \n\n`;
|
157
|
+
}
|
158
|
+
// eslint-disable-next-line no-console
|
159
|
+
console.log(`\n\ndotcom-build-sass:\nYou have spent at least ${emoji.join(' ')} ${time} ${emoji
|
160
|
+
.reverse()
|
161
|
+
.join(' ')} waiting on FT Sass to compile.\n${cta}`);
|
162
|
+
__classPrivateFieldGet(this, _write).call(this, { noticeDate: Date.now(), totalTimeAtLastNotice: __classPrivateFieldGet(this, _stats).totalTime });
|
163
|
+
});
|
164
|
+
fs_1.default.mkdirSync(__classPrivateFieldGet(this, _directory), { recursive: true });
|
165
|
+
}
|
166
|
+
}
|
167
|
+
_monitorRemotely = new WeakMap(), _noticeStrategies = new WeakMap(), _noticeStrategy = new WeakMap(), _noticeThrottleSeconds = new WeakMap(), _noticeThrottlePercentage = new WeakMap(), _stats = new WeakMap(), _directory = new WeakMap(), _file = new WeakMap(), _startTime = new WeakMap(), _endTime = new WeakMap(), _read = new WeakMap(), _write = new WeakMap(), _report = new WeakMap();
|
168
|
+
// We're proxying a few functions for monitoring purposes,
|
169
|
+
// we want to catch any monitoring errors silently.
|
170
|
+
const forgivingProxy = (target, task) => {
|
171
|
+
return new Proxy(target, {
|
172
|
+
apply(...args) {
|
173
|
+
try {
|
174
|
+
return task(...args);
|
175
|
+
}
|
176
|
+
catch (error) {
|
177
|
+
Reflect.apply(...args);
|
178
|
+
logError(`Failed to monitor Sass build. Error: ${error}`);
|
179
|
+
}
|
180
|
+
}
|
181
|
+
});
|
182
|
+
};
|
183
|
+
const stats = new SassStats();
|
184
|
+
const monitoredSassLoaderProxy = forgivingProxy(sass_loader_1.default, (target, sassLoaderThis, argumentsList) => {
|
185
|
+
// Start the timer, sass-loader has been called with Sass content.
|
186
|
+
// https://github.com/webpack-contrib/sass-loader/blob/03773152760434a2dd845008c504a09c0eb3fd91/src/index.js#L19
|
187
|
+
stats.start();
|
188
|
+
// Assign our proxy to sass-loaders async function.
|
189
|
+
// https://github.com/webpack-contrib/sass-loader/blob/03773152760434a2dd845008c504a09c0eb3fd91/src/index.js#L29
|
190
|
+
const sassLoaderAsyncProxy = forgivingProxy(sassLoaderThis.async, (target, thisArg, argumentsList) => {
|
191
|
+
// Run sass-loader's async function as normal.
|
192
|
+
// Proxy the callback it returns.
|
193
|
+
// https://github.com/webpack-contrib/sass-loader/blob/03773152760434a2dd845008c504a09c0eb3fd91/src/index.js#L113
|
194
|
+
const sassLoaderCallback = Reflect.apply(target, thisArg, argumentsList);
|
195
|
+
return forgivingProxy(sassLoaderCallback, (target, thisArg, argumentsList) => {
|
196
|
+
// sass-loader's callback has been... called.
|
197
|
+
// Either we have sass, or the build failed.
|
198
|
+
stats.end();
|
199
|
+
stats.reportAccordingToNoticeStrategy();
|
200
|
+
stats.sendMetric();
|
201
|
+
return Reflect.apply(target, thisArg, argumentsList);
|
202
|
+
});
|
203
|
+
});
|
204
|
+
sassLoaderThis.async = sassLoaderAsyncProxy;
|
205
|
+
// Run sass-loader as normal.
|
206
|
+
return Reflect.apply(target, sassLoaderThis, argumentsList);
|
207
|
+
});
|
208
|
+
exports.default = monitoredSassLoaderProxy;
|
@@ -452,10 +452,15 @@
|
|
452
452
|
"affectsGlobalScope": false
|
453
453
|
},
|
454
454
|
"../src/index.ts": {
|
455
|
-
"version": "
|
455
|
+
"version": "7fb00d27970b40b6bb55e0082ba7f14114cc4e5d23fc9912cea1da42e24ba5bc",
|
456
456
|
"signature": "38296bee587ffb2a33b5f3e8c31f22f263d27c65a68666824e3b2f717e212a84",
|
457
457
|
"affectsGlobalScope": false
|
458
458
|
},
|
459
|
+
"../src/monitored-sass-loader.ts": {
|
460
|
+
"version": "b4a57f13235d705d5db658149a2725d718016ceb68726c565632f5f9b1e9d084",
|
461
|
+
"signature": "d6c7e7fe7e8f3b80e77d715b5d0d632a6bdb41bfa6c96d464997d2f698366e9b",
|
462
|
+
"affectsGlobalScope": false
|
463
|
+
},
|
459
464
|
"../../../node_modules/@types/aria-query/index.d.ts": {
|
460
465
|
"version": "ae77d81a5541a8abb938a0efedf9ac4bea36fb3a24cc28cfa11c598863aba571",
|
461
466
|
"signature": "ae77d81a5541a8abb938a0efedf9ac4bea36fb3a24cc28cfa11c598863aba571",
|
@@ -1828,6 +1833,12 @@
|
|
1828
1833
|
],
|
1829
1834
|
"../src/index.ts": [
|
1830
1835
|
"../../../node_modules/@types/webpack/index.d.ts"
|
1836
|
+
],
|
1837
|
+
"../src/monitored-sass-loader.ts": [
|
1838
|
+
"../../../node_modules/@types/node/fs.d.ts",
|
1839
|
+
"../../../node_modules/@types/node/https.d.ts",
|
1840
|
+
"../../../node_modules/@types/node/os.d.ts",
|
1841
|
+
"../../../node_modules/@types/node/path.d.ts"
|
1831
1842
|
]
|
1832
1843
|
},
|
1833
1844
|
"exportedModulesMap": {
|
@@ -2792,7 +2803,8 @@
|
|
2792
2803
|
"../../../node_modules/typescript/lib/lib.es2020.bigint.d.ts",
|
2793
2804
|
"../../../node_modules/typescript/lib/lib.es5.d.ts",
|
2794
2805
|
"../../../node_modules/typescript/lib/lib.esnext.intl.d.ts",
|
2795
|
-
"../src/index.ts"
|
2806
|
+
"../src/index.ts",
|
2807
|
+
"../src/monitored-sass-loader.ts"
|
2796
2808
|
]
|
2797
2809
|
},
|
2798
2810
|
"version": "4.1.6"
|
package/package.json
CHANGED
package/src/index.ts
CHANGED
@@ -115,7 +115,7 @@ export class PageKitSassPlugin {
|
|
115
115
|
// Enable use of Sass for CSS preprocessing
|
116
116
|
// https://github.com/webpack-contrib/sass-loader
|
117
117
|
{
|
118
|
-
loader: require.resolve('sass-loader'),
|
118
|
+
loader: require.resolve('./monitored-sass-loader'),
|
119
119
|
options: sassLoaderOptions
|
120
120
|
}
|
121
121
|
]
|
@@ -0,0 +1,229 @@
|
|
1
|
+
import fs from 'fs'
|
2
|
+
import path from 'path'
|
3
|
+
import os from 'os'
|
4
|
+
import sassLoader from 'sass-loader'
|
5
|
+
import https from 'https'
|
6
|
+
|
7
|
+
const logError = (message) => {
|
8
|
+
// eslint-disable-next-line no-console
|
9
|
+
console.log(
|
10
|
+
`\n⛔️😭dotcom-build-sass: ${message}. Please report to #origami-support in Slack, so we can help move us away from Sass.\n`
|
11
|
+
)
|
12
|
+
}
|
13
|
+
|
14
|
+
class SassStats {
|
15
|
+
#monitorRemotely = process.env.FT_SASS_STATS_MONITOR === 'on'
|
16
|
+
#noticeStrategies = ['throttle', 'never', 'always']
|
17
|
+
#noticeStrategy = this.#noticeStrategies.includes(process.env.FT_SASS_STATS_NOTICE)
|
18
|
+
? process.env.FT_SASS_STATS_NOTICE
|
19
|
+
: 'throttle'
|
20
|
+
#noticeThrottleSeconds =
|
21
|
+
typeof process.env.FT_SASS_STATS_NOTICE_THROTTLE_SECONDS === 'number'
|
22
|
+
? process.env.FT_SASS_STATS_NOTICE_THROTTLE_SECONDS
|
23
|
+
: 60 * 60 * 0.5 // show throttled notice given 30 mins since last notice
|
24
|
+
#noticeThrottlePercentage =
|
25
|
+
typeof process.env.FT_SASS_STATS_NOTICE_THROTTLE_PERCENTAGE === 'number'
|
26
|
+
? process.env.FT_SASS_STATS_NOTICE_THROTTLE_PERCENTAGE
|
27
|
+
: 30 // show throttled notice given a 30% increase
|
28
|
+
#stats = { totalTime: 0, noticeDate: null, totalTimeAtLastNotice: 0 }
|
29
|
+
#directory = path.join(os.tmpdir(), 'dotcom-build-sass')
|
30
|
+
#file = path.join(this.#directory, 'sass-stats.json')
|
31
|
+
#startTime
|
32
|
+
#endTime
|
33
|
+
|
34
|
+
constructor() {
|
35
|
+
fs.mkdirSync(this.#directory, { recursive: true })
|
36
|
+
}
|
37
|
+
|
38
|
+
start = () => {
|
39
|
+
this.#read()
|
40
|
+
this.#startTime = performance.now()
|
41
|
+
}
|
42
|
+
|
43
|
+
end = () => {
|
44
|
+
this.#endTime = performance.now()
|
45
|
+
const updatedTotal = (this.#stats.totalTime += this.#endTime - this.#startTime)
|
46
|
+
this.#write({ totalTime: updatedTotal })
|
47
|
+
}
|
48
|
+
|
49
|
+
#read = () => {
|
50
|
+
try {
|
51
|
+
// Restore stats from a temporary file if it exists.
|
52
|
+
// Reading from disk ensures that we can track stats across builds.
|
53
|
+
const statsFile = fs.readFileSync(this.#file, 'utf-8')
|
54
|
+
this.#stats = JSON.parse(statsFile)
|
55
|
+
} catch {}
|
56
|
+
return this.#stats
|
57
|
+
}
|
58
|
+
|
59
|
+
#write = (stats) => {
|
60
|
+
this.#stats = Object.assign(this.#stats, stats)
|
61
|
+
fs.writeFileSync(this.#file, JSON.stringify(this.#stats))
|
62
|
+
}
|
63
|
+
|
64
|
+
sendMetric = () => {
|
65
|
+
if (!this.#monitorRemotely) {
|
66
|
+
return
|
67
|
+
}
|
68
|
+
|
69
|
+
if (!process.env.FT_SASS_BIZ_OPS_API_KEY) {
|
70
|
+
logError(
|
71
|
+
'We couldn\'t share your Sass build time, we\'re missing the environment variable "FT_SASS_BIZ_OPS_API_KEY". Please contact #origami-support with any questions.'
|
72
|
+
)
|
73
|
+
return
|
74
|
+
}
|
75
|
+
if (!process.env.FT_SASS_BIZ_OPS_SYSTEM_CODE) {
|
76
|
+
logError(
|
77
|
+
'We couldn\'t share your Sass build time, we\'re missing the environment variable "FT_SASS_BIZ_OPS_SYSTEM_CODE". Please contact #origami-support with any questions.'
|
78
|
+
)
|
79
|
+
return
|
80
|
+
}
|
81
|
+
|
82
|
+
const date = new Date()
|
83
|
+
const postData = JSON.stringify({
|
84
|
+
type: 'System',
|
85
|
+
metric: 'sass-build-time',
|
86
|
+
value: (this.#endTime - this.#startTime) / 1000,
|
87
|
+
date: date.toISOString(),
|
88
|
+
code: process.env.FT_SASS_BIZ_OPS_SYSTEM_CODE,
|
89
|
+
metadata: {
|
90
|
+
'node-env': process.env.NODE_ENV
|
91
|
+
}
|
92
|
+
})
|
93
|
+
|
94
|
+
const options = {
|
95
|
+
hostname: 'api.ft.com',
|
96
|
+
port: 443,
|
97
|
+
path: '/biz-ops-metrics/metric/add',
|
98
|
+
method: 'POST',
|
99
|
+
headers: {
|
100
|
+
'x-api-key': process.env.FT_SASS_BIZ_OPS_API_KEY,
|
101
|
+
'client-id': 'page-kit',
|
102
|
+
'Content-Type': 'application/json',
|
103
|
+
'Content-Length': postData.length
|
104
|
+
}
|
105
|
+
}
|
106
|
+
|
107
|
+
const request = https
|
108
|
+
.request(options, (response) => {
|
109
|
+
if (response.statusCode !== 200) {
|
110
|
+
logError(
|
111
|
+
`We couldn\'t send your Sass build time metrics to biz-ops. Status code: ${response.statusCode}.`
|
112
|
+
)
|
113
|
+
}
|
114
|
+
})
|
115
|
+
.on('error', (error) => {
|
116
|
+
logError(`We couldn\'t send your Sass build time metrics to biz-ops. Error: ${error}.`)
|
117
|
+
})
|
118
|
+
request.write(postData)
|
119
|
+
request.end()
|
120
|
+
}
|
121
|
+
|
122
|
+
reportAccordingToNoticeStrategy = () => {
|
123
|
+
let shouldReport
|
124
|
+
|
125
|
+
switch (this.#noticeStrategy) {
|
126
|
+
case 'never':
|
127
|
+
shouldReport = false
|
128
|
+
break
|
129
|
+
|
130
|
+
case 'always':
|
131
|
+
shouldReport = true
|
132
|
+
break
|
133
|
+
|
134
|
+
case 'throttle':
|
135
|
+
// Throttle notices to show a limited number per hour, or if the total sass build time
|
136
|
+
// has increased by a significant percentage. This favours more frequent reports to begin with.
|
137
|
+
const noticeTimeThrottle = Date.now() >= this.#stats.noticeDate + this.#noticeThrottleSeconds * 1000
|
138
|
+
const percentageTotalTimeThrottle =
|
139
|
+
this.#stats.totalTime > 0 &&
|
140
|
+
(this.#stats.totalTime / this.#stats.totalTimeAtLastNotice - 1) * 100 >=
|
141
|
+
this.#noticeThrottlePercentage // % increase
|
142
|
+
shouldReport = !this.#stats.noticeDate || noticeTimeThrottle || percentageTotalTimeThrottle
|
143
|
+
break
|
144
|
+
|
145
|
+
default:
|
146
|
+
break
|
147
|
+
}
|
148
|
+
|
149
|
+
if (shouldReport) {
|
150
|
+
this.#report()
|
151
|
+
}
|
152
|
+
}
|
153
|
+
|
154
|
+
#report = () => {
|
155
|
+
const seconds = this.#stats.totalTime / 1000
|
156
|
+
const minutes = seconds / 60
|
157
|
+
const hours = seconds / 3600
|
158
|
+
const time =
|
159
|
+
hours > 1
|
160
|
+
? `${hours.toFixed(1)} hours`
|
161
|
+
: minutes > 1
|
162
|
+
? `${minutes.toFixed(0)} minutes`
|
163
|
+
: `${seconds.toFixed(0)} seconds`
|
164
|
+
const emoji =
|
165
|
+
hours > 2 ? ['🔥', '😭', '😱'] : hours >= 1 ? ['🔥', '😱'] : minutes > 10 ? ['⏱️', '😬'] : ['⏱️']
|
166
|
+
|
167
|
+
let cta =
|
168
|
+
`Share your pain in Slack #sass-to-css, and help fix that! 🎉\n` +
|
169
|
+
`https://origami.ft.com/blog/2024/01/24/sass-build-times/\n\n`
|
170
|
+
|
171
|
+
if (!this.#monitorRemotely) {
|
172
|
+
cta =
|
173
|
+
`Help us improve build times by setting the "FT_SASS_STATS_MONITOR" environment variable.\n` +
|
174
|
+
`https://github.com/Financial-Times/biz-ops-metrics-api/blob/main/docs/API_DEFINITION.md#sass-build-monitoring \n\n`
|
175
|
+
}
|
176
|
+
|
177
|
+
// eslint-disable-next-line no-console
|
178
|
+
console.log(
|
179
|
+
`\n\ndotcom-build-sass:\nYou have spent at least ${emoji.join(' ')} ${time} ${emoji
|
180
|
+
.reverse()
|
181
|
+
.join(' ')} waiting on FT Sass to compile.\n${cta}`
|
182
|
+
)
|
183
|
+
|
184
|
+
this.#write({ noticeDate: Date.now(), totalTimeAtLastNotice: this.#stats.totalTime })
|
185
|
+
}
|
186
|
+
}
|
187
|
+
|
188
|
+
// We're proxying a few functions for monitoring purposes,
|
189
|
+
// we want to catch any monitoring errors silently.
|
190
|
+
const forgivingProxy = (target, task) => {
|
191
|
+
return new Proxy(target, {
|
192
|
+
apply(...args) {
|
193
|
+
try {
|
194
|
+
return task(...args)
|
195
|
+
} catch (error) {
|
196
|
+
Reflect.apply(...args)
|
197
|
+
logError(`Failed to monitor Sass build. Error: ${error}`)
|
198
|
+
}
|
199
|
+
}
|
200
|
+
})
|
201
|
+
}
|
202
|
+
|
203
|
+
const stats = new SassStats()
|
204
|
+
const monitoredSassLoaderProxy = forgivingProxy(sassLoader, (target, sassLoaderThis, argumentsList) => {
|
205
|
+
// Start the timer, sass-loader has been called with Sass content.
|
206
|
+
// https://github.com/webpack-contrib/sass-loader/blob/03773152760434a2dd845008c504a09c0eb3fd91/src/index.js#L19
|
207
|
+
stats.start()
|
208
|
+
// Assign our proxy to sass-loaders async function.
|
209
|
+
// https://github.com/webpack-contrib/sass-loader/blob/03773152760434a2dd845008c504a09c0eb3fd91/src/index.js#L29
|
210
|
+
const sassLoaderAsyncProxy = forgivingProxy(sassLoaderThis.async, (target, thisArg, argumentsList) => {
|
211
|
+
// Run sass-loader's async function as normal.
|
212
|
+
// Proxy the callback it returns.
|
213
|
+
// https://github.com/webpack-contrib/sass-loader/blob/03773152760434a2dd845008c504a09c0eb3fd91/src/index.js#L113
|
214
|
+
const sassLoaderCallback = Reflect.apply(target, thisArg, argumentsList)
|
215
|
+
return forgivingProxy(sassLoaderCallback, (target, thisArg, argumentsList) => {
|
216
|
+
// sass-loader's callback has been... called.
|
217
|
+
// Either we have sass, or the build failed.
|
218
|
+
stats.end()
|
219
|
+
stats.reportAccordingToNoticeStrategy()
|
220
|
+
stats.sendMetric()
|
221
|
+
return Reflect.apply(target, thisArg, argumentsList)
|
222
|
+
})
|
223
|
+
})
|
224
|
+
sassLoaderThis.async = sassLoaderAsyncProxy
|
225
|
+
// Run sass-loader as normal.
|
226
|
+
return Reflect.apply(target, sassLoaderThis, argumentsList)
|
227
|
+
})
|
228
|
+
|
229
|
+
export default monitoredSassLoaderProxy
|