@financial-times/dotcom-build-sass 9.2.0 → 9.3.0

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 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. |
@@ -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,2 @@
1
+ declare const monitoredSassLoaderProxy: any;
2
+ export default monitoredSassLoaderProxy;
@@ -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(path_1.default.dirname(__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": "eaea1831fa92562dc7cee11bbbe2fd591174f362cc7b0edd53a507ac9a4b0995",
455
+ "version": "7fb00d27970b40b6bb55e0082ba7f14114cc4e5d23fc9912cea1da42e24ba5bc",
456
456
  "signature": "38296bee587ffb2a33b5f3e8c31f22f263d27c65a68666824e3b2f717e212a84",
457
457
  "affectsGlobalScope": false
458
458
  },
459
+ "../src/monitored-sass-loader.ts": {
460
+ "version": "284b63b2fe068813dab4f81fa6016322237f4fe86ec49632102784ebd8bb291e",
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@financial-times/dotcom-build-sass",
3
- "version": "9.2.0",
3
+ "version": "9.3.0",
4
4
  "description": "",
5
5
  "main": "dist/node/index.js",
6
6
  "types": "dist/node/index.d.ts",
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(path.dirname(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