@flakiness/sdk 0.147.0 → 0.149.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.
@@ -0,0 +1,149 @@
1
+ // src/staticServer.ts
2
+ import debug from "debug";
3
+ import * as fs from "fs";
4
+ import * as http from "http";
5
+ import * as path from "path";
6
+ var log = debug("fk:static_server");
7
+ var StaticServer = class {
8
+ _server;
9
+ _absoluteFolderPath;
10
+ _pathPrefix;
11
+ _cors;
12
+ _mimeTypes = {
13
+ ".html": "text/html",
14
+ ".js": "text/javascript",
15
+ ".css": "text/css",
16
+ ".json": "application/json",
17
+ ".png": "image/png",
18
+ ".jpg": "image/jpeg",
19
+ ".gif": "image/gif",
20
+ ".svg": "image/svg+xml",
21
+ ".ico": "image/x-icon",
22
+ ".txt": "text/plain"
23
+ };
24
+ constructor(pathPrefix, folderPath, cors) {
25
+ this._pathPrefix = "/" + pathPrefix.replace(/^\//, "").replace(/\/$/, "");
26
+ this._absoluteFolderPath = path.resolve(folderPath);
27
+ this._cors = cors;
28
+ this._server = http.createServer((req, res) => this._handleRequest(req, res));
29
+ }
30
+ port() {
31
+ const address = this._server.address();
32
+ if (!address)
33
+ return void 0;
34
+ return address.port;
35
+ }
36
+ address() {
37
+ const address = this._server.address();
38
+ if (!address)
39
+ return void 0;
40
+ const displayHost = address.address.includes(":") ? `[${address.address}]` : address.address;
41
+ return `http://${displayHost}:${address.port}${this._pathPrefix}`;
42
+ }
43
+ async _startServer(port, host) {
44
+ let okListener;
45
+ let errListener;
46
+ const result = new Promise((resolve2, reject) => {
47
+ okListener = resolve2;
48
+ errListener = reject;
49
+ }).finally(() => {
50
+ this._server.removeListener("listening", okListener);
51
+ this._server.removeListener("error", errListener);
52
+ });
53
+ this._server.once("listening", okListener);
54
+ this._server.once("error", errListener);
55
+ this._server.listen(port, host);
56
+ await result;
57
+ log('Serving "%s" on "%s"', this._absoluteFolderPath, this.address());
58
+ }
59
+ async start(port, host = "127.0.0.1") {
60
+ if (port === 0) {
61
+ await this._startServer(port, host);
62
+ return this.address();
63
+ }
64
+ for (let i = 0; i < 20; ++i) {
65
+ const err = await this._startServer(port, host).then(() => void 0).catch((e) => e);
66
+ if (!err)
67
+ return this.address();
68
+ if (err.code !== "EADDRINUSE")
69
+ throw err;
70
+ log("Port %d is busy (EADDRINUSE). Trying next port...", port);
71
+ port = port + 1;
72
+ if (port > 65535)
73
+ port = 4e3;
74
+ }
75
+ log("All sequential ports busy. Falling back to random port.");
76
+ await this._startServer(0, host);
77
+ return this.address();
78
+ }
79
+ stop() {
80
+ return new Promise((resolve2, reject) => {
81
+ this._server.close((err) => {
82
+ if (err) {
83
+ log("Error stopping server: %o", err);
84
+ reject(err);
85
+ } else {
86
+ log("Server stopped.");
87
+ resolve2();
88
+ }
89
+ });
90
+ });
91
+ }
92
+ _errorResponse(req, res, code, text) {
93
+ res.writeHead(code, { "Content-Type": "text/plain" });
94
+ res.end(text);
95
+ log(`[${code}] ${req.method} ${req.url}`);
96
+ }
97
+ _handleRequest(req, res) {
98
+ const { url, method } = req;
99
+ if (this._cors) {
100
+ res.setHeader("Access-Control-Allow-Headers", "*");
101
+ res.setHeader("Access-Control-Allow-Origin", this._cors);
102
+ res.setHeader("Access-Control-Allow-Methods", "*");
103
+ if (req.method === "OPTIONS") {
104
+ res.writeHead(204);
105
+ res.end();
106
+ return;
107
+ }
108
+ }
109
+ if (method !== "GET") {
110
+ this._errorResponse(req, res, 405, "Method Not Allowed");
111
+ return;
112
+ }
113
+ req.on("aborted", () => log(`ABORTED ${req.method} ${req.url}`));
114
+ res.on("close", () => {
115
+ if (!res.headersSent) log(`CLOSED BEFORE SEND ${req.method} ${req.url}`);
116
+ });
117
+ if (!url || !url.startsWith(this._pathPrefix)) {
118
+ this._errorResponse(req, res, 404, "Not Found");
119
+ return;
120
+ }
121
+ const relativePath = url.slice(this._pathPrefix.length);
122
+ const safeSuffix = path.normalize(decodeURIComponent(relativePath)).replace(/^(\.\.[\/\\])+/, "");
123
+ const filePath = path.join(this._absoluteFolderPath, safeSuffix);
124
+ if (!filePath.startsWith(this._absoluteFolderPath)) {
125
+ this._errorResponse(req, res, 403, "Forbidden");
126
+ return;
127
+ }
128
+ fs.stat(filePath, (err, stats) => {
129
+ if (err || !stats.isFile()) {
130
+ this._errorResponse(req, res, 404, "File Not Found");
131
+ return;
132
+ }
133
+ const ext = path.extname(filePath).toLowerCase();
134
+ const contentType = this._mimeTypes[ext] || "application/octet-stream";
135
+ res.writeHead(200, { "Content-Type": contentType });
136
+ log(`[200] ${req.method} ${req.url} -> ${filePath}`);
137
+ const readStream = fs.createReadStream(filePath);
138
+ readStream.pipe(res);
139
+ readStream.on("error", (err2) => {
140
+ log("Stream error: %o", err2);
141
+ res.end();
142
+ });
143
+ });
144
+ }
145
+ };
146
+ export {
147
+ StaticServer
148
+ };
149
+ //# sourceMappingURL=staticServer.js.map
@@ -0,0 +1,9 @@
1
+ // src/stripAnsi.ts
2
+ var ansiRegex = new RegExp("[\\u001B\\u009B][[\\]()#;?]*(?:(?:(?:[a-zA-Z\\d]*(?:;[-a-zA-Z\\d\\/#&.:=?%@~_]*)*)?\\u0007)|(?:(?:\\d{1,4}(?:;\\d{0,4})*)?[\\dA-PR-TZcf-ntqry=><~]))", "g");
3
+ function stripAnsi(str) {
4
+ return str.replace(ansiRegex, "");
5
+ }
6
+ export {
7
+ stripAnsi
8
+ };
9
+ //# sourceMappingURL=stripAnsi.js.map
@@ -44,9 +44,24 @@ function toFKUtilization(sample, previous) {
44
44
  };
45
45
  }
46
46
  var SystemUtilizationSampler = class {
47
+ /**
48
+ * The accumulated system utilization data.
49
+ *
50
+ * This object is populated as samples are collected and can be directly included in
51
+ * Flakiness reports. It contains:
52
+ * - `samples` - Array of utilization samples with CPU/memory percentages and durations
53
+ * - `startTimestamp` - Timestamp when sampling began
54
+ * - `totalMemoryBytes` - Total system memory in bytes
55
+ */
47
56
  result;
48
57
  _lastSample = getSystemUtilization();
49
58
  _timer;
59
+ /**
60
+ * Creates a new SystemUtilizationSampler and starts sampling immediately.
61
+ *
62
+ * The first sample is collected after 50ms, and subsequent samples are collected
63
+ * every 1000ms. Call `dispose()` to stop sampling and clean up resources.
64
+ */
50
65
  constructor() {
51
66
  this.result = {
52
67
  samples: [],
@@ -61,6 +76,12 @@ var SystemUtilizationSampler = class {
61
76
  this._lastSample = sample;
62
77
  this._timer = setTimeout(this._addSample.bind(this), 1e3);
63
78
  }
79
+ /**
80
+ * Stops sampling and cleans up resources.
81
+ *
82
+ * Call this method when you're done collecting utilization data to stop the sampling
83
+ * timer and prevent memory leaks. The `result` object remains accessible after disposal.
84
+ */
64
85
  dispose() {
65
86
  clearTimeout(this._timer);
66
87
  }
@@ -1,15 +1,25 @@
1
- // src/reportUploader.ts
2
- import { compressTextAsync, compressTextSync } from "@flakiness/shared/node/compression.js";
1
+ // src/uploadReport.ts
3
2
  import assert from "assert";
4
- import crypto from "crypto";
5
- import fs from "fs";
3
+ import fs2 from "fs";
6
4
  import { URL } from "url";
7
5
 
8
- // src/httpUtils.ts
6
+ // src/_internalUtils.ts
7
+ import crypto from "crypto";
8
+ import fs from "fs";
9
9
  import http from "http";
10
10
  import https from "https";
11
-
12
- // src/utils.ts
11
+ import util from "util";
12
+ import zlib from "zlib";
13
+ var asyncBrotliCompress = util.promisify(zlib.brotliCompress);
14
+ async function compressTextAsync(text) {
15
+ return asyncBrotliCompress(text, {
16
+ chunkSize: 32 * 1024,
17
+ params: {
18
+ [zlib.constants.BROTLI_PARAM_QUALITY]: 6,
19
+ [zlib.constants.BROTLI_PARAM_MODE]: zlib.constants.BROTLI_MODE_TEXT
20
+ }
21
+ });
22
+ }
13
23
  var FLAKINESS_DBG = !!process.env.FLAKINESS_DBG;
14
24
  function errorText(error) {
15
25
  return FLAKINESS_DBG ? error.stack : error.message;
@@ -30,10 +40,6 @@ async function retryWithBackoff(job, backoff = []) {
30
40
  }
31
41
  return await job();
32
42
  }
33
- var ansiRegex = new RegExp("[\\u001B\\u009B][[\\]()#;?]*(?:(?:(?:[a-zA-Z\\d]*(?:;[-a-zA-Z\\d\\/#&.:=?%@~_]*)*)?\\u0007)|(?:(?:\\d{1,4}(?:;\\d{0,4})*)?[\\dA-PR-TZcf-ntqry=><~]))", "g");
34
-
35
- // src/httpUtils.ts
36
- var FLAKINESS_DBG2 = !!process.env.FLAKINESS_DBG;
37
43
  var httpUtils;
38
44
  ((httpUtils2) => {
39
45
  function createRequest({ url, method = "get", headers = {} }) {
@@ -96,8 +102,11 @@ var httpUtils;
96
102
  }
97
103
  httpUtils2.postJSON = postJSON;
98
104
  })(httpUtils || (httpUtils = {}));
99
-
100
- // src/reportUploader.ts
105
+ function sha1Text(data) {
106
+ const hash = crypto.createHash("sha1");
107
+ hash.update(data);
108
+ return hash.digest("hex");
109
+ }
101
110
  function sha1File(filePath) {
102
111
  return new Promise((resolve, reject) => {
103
112
  const hash = crypto.createHash("sha1");
@@ -113,64 +122,60 @@ function sha1File(filePath) {
113
122
  });
114
123
  });
115
124
  }
125
+
126
+ // src/uploadReport.ts
116
127
  async function createFileAttachment(contentType, filePath) {
117
128
  return {
129
+ type: "file",
118
130
  contentType,
119
131
  id: await sha1File(filePath),
120
132
  path: filePath
121
133
  };
122
134
  }
123
135
  async function createDataAttachment(contentType, data) {
124
- const hash = crypto.createHash("sha1");
125
- hash.update(data);
126
- const id = hash.digest("hex");
127
136
  return {
137
+ type: "buffer",
128
138
  contentType,
129
- id,
139
+ id: sha1Text(data),
130
140
  body: data
131
141
  };
132
142
  }
133
- var ReportUploader = class _ReportUploader {
134
- static optionsFromEnv(overrides) {
135
- const flakinessAccessToken = overrides?.flakinessAccessToken ?? process.env["FLAKINESS_ACCESS_TOKEN"];
136
- if (!flakinessAccessToken)
137
- return void 0;
138
- const flakinessEndpoint = overrides?.flakinessEndpoint ?? process.env["FLAKINESS_ENDPOINT"] ?? "https://flakiness.io";
139
- return { flakinessAccessToken, flakinessEndpoint };
143
+ async function uploadReport(report, attachments, options) {
144
+ const flakinessAccessToken = options?.flakinessAccessToken ?? process.env["FLAKINESS_ACCESS_TOKEN"];
145
+ const flakinessEndpoint = options?.flakinessEndpoint ?? process.env["FLAKINESS_ENDPOINT"] ?? "https://flakiness.io";
146
+ const logger = options?.logger ?? console;
147
+ if (!flakinessAccessToken) {
148
+ const reason = "No FLAKINESS_ACCESS_TOKEN found";
149
+ if (process.env.CI)
150
+ logger.warn(`[flakiness.io] \u26A0 Skipping upload: ${reason}`);
151
+ return { status: "skipped", reason };
140
152
  }
141
- static async upload(options) {
142
- const uploaderOptions = _ReportUploader.optionsFromEnv(options);
143
- if (!uploaderOptions) {
144
- if (process.env.CI)
145
- options.log?.(`[flakiness.io] Uploading skipped since no FLAKINESS_ACCESS_TOKEN is specified`);
146
- return void 0;
147
- }
148
- const uploader = new _ReportUploader(uploaderOptions);
149
- const upload = uploader.createUpload(options.report, options.attachments);
153
+ try {
154
+ const upload = new ReportUpload(report, attachments, { flakinessAccessToken, flakinessEndpoint });
150
155
  const uploadResult = await upload.upload();
151
156
  if (!uploadResult.success) {
152
- options.log?.(`[flakiness.io] X Failed to upload to ${uploaderOptions.flakinessEndpoint}: ${uploadResult.message}`);
153
- return { errorMessage: uploadResult.message };
157
+ const errorMessage = uploadResult.message || "Unknown upload error";
158
+ logger.error(`[flakiness.io] \u2715 Failed to upload: ${errorMessage}`);
159
+ if (options?.throwOnFailure)
160
+ throw new Error(`Flakiness upload failed: ${errorMessage}`);
161
+ return { status: "failed", error: errorMessage };
154
162
  }
155
- options.log?.(`[flakiness.io] \u2713 Report uploaded ${uploadResult.message ?? ""}`);
156
- if (uploadResult.reportUrl)
157
- options.log?.(`[flakiness.io] ${uploadResult.reportUrl}`);
163
+ logger.log(`[flakiness.io] \u2713 Uploaded to ${uploadResult.reportUrl}`);
164
+ return { status: "success", reportUrl: uploadResult.reportUrl };
165
+ } catch (e) {
166
+ const errorMessage = e.message || String(e);
167
+ logger.error(`[flakiness.io] \u2715 Unexpected error during upload: ${errorMessage}`);
168
+ if (options?.throwOnFailure)
169
+ throw e;
170
+ return { status: "failed", error: errorMessage };
158
171
  }
159
- _options;
160
- constructor(options) {
161
- this._options = options;
162
- }
163
- createUpload(report, attachments) {
164
- const upload = new ReportUpload(this._options, report, attachments);
165
- return upload;
166
- }
167
- };
172
+ }
168
173
  var HTTP_BACKOFF = [100, 500, 1e3, 1e3, 1e3, 1e3];
169
174
  var ReportUpload = class {
170
175
  _report;
171
176
  _attachments;
172
177
  _options;
173
- constructor(options, report, attachments) {
178
+ constructor(report, attachments, options) {
174
179
  this._options = options;
175
180
  this._report = report;
176
181
  this._attachments = attachments;
@@ -196,7 +201,7 @@ var ReportUpload = class {
196
201
  error
197
202
  }));
198
203
  }
199
- async upload(options) {
204
+ async upload() {
200
205
  const response = await this._api("/api/upload/start", this._options.flakinessAccessToken);
201
206
  if (response?.error || !response.result)
202
207
  return { success: false, message: response.error };
@@ -208,19 +213,19 @@ var ReportUpload = class {
208
213
  return { success: false, message: attachmentsPresignedUrls.error };
209
214
  const attachments = new Map(attachmentsPresignedUrls.result.map((a) => [a.attachmentId, a.presignedUrl]));
210
215
  await Promise.all([
211
- this._uploadReport(JSON.stringify(this._report), response.result.presignedReportUrl, options?.syncCompression ?? false),
216
+ this._uploadReport(JSON.stringify(this._report), response.result.presignedReportUrl),
212
217
  ...this._attachments.map((attachment) => {
213
218
  const uploadURL = attachments.get(attachment.id);
214
219
  if (!uploadURL)
215
220
  throw new Error("Internal error: missing upload URL for attachment!");
216
- return this._uploadAttachment(attachment, uploadURL, options?.syncCompression ?? false);
221
+ return this._uploadAttachment(attachment, uploadURL);
217
222
  })
218
223
  ]);
219
224
  await this._api("/api/upload/finish", response.result.uploadToken);
220
225
  return { success: true, reportUrl: webUrl };
221
226
  }
222
- async _uploadReport(data, uploadUrl, syncCompression) {
223
- const compressed = syncCompression ? compressTextSync(data) : await compressTextAsync(data);
227
+ async _uploadReport(data, uploadUrl) {
228
+ const compressed = await compressTextAsync(data);
224
229
  const headers = {
225
230
  "Content-Type": "application/json",
226
231
  "Content-Length": Buffer.byteLength(compressed) + "",
@@ -237,30 +242,29 @@ var ReportUpload = class {
237
242
  await responseDataPromise;
238
243
  }, HTTP_BACKOFF);
239
244
  }
240
- async _uploadAttachment(attachment, uploadUrl, syncCompression) {
245
+ async _uploadAttachment(attachment, uploadUrl) {
241
246
  const mimeType = attachment.contentType.toLocaleLowerCase().trim();
242
247
  const compressable = mimeType.startsWith("text/") || mimeType.endsWith("+json") || mimeType.endsWith("+text") || mimeType.endsWith("+xml");
243
- if (!compressable && attachment.path) {
244
- const attachmentPath = attachment.path;
248
+ if (!compressable && attachment.type === "file") {
245
249
  await retryWithBackoff(async () => {
246
250
  const { request, responseDataPromise } = httpUtils.createRequest({
247
251
  url: uploadUrl,
248
252
  headers: {
249
253
  "Content-Type": attachment.contentType,
250
- "Content-Length": (await fs.promises.stat(attachmentPath)).size + ""
254
+ "Content-Length": (await fs2.promises.stat(attachment.path)).size + ""
251
255
  },
252
256
  method: "put"
253
257
  });
254
- fs.createReadStream(attachmentPath).pipe(request);
258
+ fs2.createReadStream(attachment.path).pipe(request);
255
259
  await responseDataPromise;
256
260
  }, HTTP_BACKOFF);
257
261
  return;
258
262
  }
259
- let buffer = attachment.body ? attachment.body : attachment.path ? await fs.promises.readFile(attachment.path) : void 0;
263
+ let buffer = attachment.type === "buffer" ? attachment.body : await fs2.promises.readFile(attachment.path);
260
264
  assert(buffer);
261
265
  const encoding = compressable ? "br" : void 0;
262
266
  if (compressable)
263
- buffer = syncCompression ? compressTextSync(buffer) : await compressTextAsync(buffer);
267
+ buffer = await compressTextAsync(buffer);
264
268
  const headers = {
265
269
  "Content-Type": attachment.contentType,
266
270
  "Content-Length": Buffer.byteLength(buffer) + "",
@@ -279,8 +283,8 @@ var ReportUpload = class {
279
283
  }
280
284
  };
281
285
  export {
282
- ReportUploader,
283
286
  createDataAttachment,
284
- createFileAttachment
287
+ createFileAttachment,
288
+ uploadReport
285
289
  };
286
- //# sourceMappingURL=reportUploader.js.map
290
+ //# sourceMappingURL=uploadReport.js.map
@@ -0,0 +1,19 @@
1
+ // src/visitTests.ts
2
+ function visitTests(report, testVisitor) {
3
+ function visitSuite(suite, parents) {
4
+ parents.push(suite);
5
+ for (const test of suite.tests ?? [])
6
+ testVisitor(test, parents);
7
+ for (const childSuite of suite.suites ?? [])
8
+ visitSuite(childSuite, parents);
9
+ parents.pop();
10
+ }
11
+ for (const test of report.tests ?? [])
12
+ testVisitor(test, []);
13
+ for (const suite of report.suites)
14
+ visitSuite(suite, []);
15
+ }
16
+ export {
17
+ visitTests
18
+ };
19
+ //# sourceMappingURL=visitTests.js.map
@@ -0,0 +1,31 @@
1
+ // src/writeReport.ts
2
+ import fs from "fs";
3
+ import path from "path";
4
+ async function writeReport(report, attachments, outputFolder) {
5
+ const reportPath = path.join(outputFolder, "report.json");
6
+ const attachmentsFolder = path.join(outputFolder, "attachments");
7
+ await fs.promises.rm(outputFolder, { recursive: true, force: true });
8
+ await fs.promises.mkdir(outputFolder, { recursive: true });
9
+ await fs.promises.writeFile(reportPath, JSON.stringify(report), "utf-8");
10
+ if (attachments.length)
11
+ await fs.promises.mkdir(attachmentsFolder);
12
+ const movedAttachments = [];
13
+ for (const attachment of attachments) {
14
+ const attachmentPath = path.join(attachmentsFolder, attachment.id);
15
+ if (attachment.type === "file")
16
+ await fs.promises.cp(attachment.path, attachmentPath);
17
+ else if (attachment.type === "buffer")
18
+ await fs.promises.writeFile(attachmentPath, attachment.body);
19
+ movedAttachments.push({
20
+ type: "file",
21
+ contentType: attachment.contentType,
22
+ id: attachment.id,
23
+ path: attachmentPath
24
+ });
25
+ }
26
+ return movedAttachments;
27
+ }
28
+ export {
29
+ writeReport
30
+ };
31
+ //# sourceMappingURL=writeReport.js.map
package/package.json CHANGED
@@ -1,20 +1,17 @@
1
1
  {
2
2
  "name": "@flakiness/sdk",
3
- "version": "0.147.0",
3
+ "version": "0.149.0",
4
4
  "private": false,
5
5
  "exports": {
6
- "./localReportApi": {
7
- "types": "./types/src/localReportApi.d.ts"
8
- },
9
6
  ".": {
10
7
  "types": "./types/src/index.d.ts",
11
8
  "import": "./lib/index.js",
12
9
  "require": "./lib/index.js"
13
10
  },
14
11
  "./browser": {
15
- "types": "./types/src/browser/index.d.ts",
16
- "import": "./lib/browser/index.js",
17
- "require": "./lib/browser/index.js"
12
+ "types": "./types/src/browser.d.ts",
13
+ "import": "./lib/browser.js",
14
+ "require": "./lib/browser.js"
18
15
  }
19
16
  },
20
17
  "type": "module",
@@ -25,22 +22,14 @@
25
22
  "author": "Degu Labs, Inc",
26
23
  "license": "Fair Source 100",
27
24
  "devDependencies": {
28
- "@playwright/test": "^1.57.0",
29
- "@types/babel__code-frame": "^7.0.6",
30
- "@types/compression": "^1.8.1",
31
- "@types/express": "^4.17.20"
25
+ "@types/babel__code-frame": "^7.0.6"
32
26
  },
33
27
  "dependencies": {
34
28
  "@babel/code-frame": "^7.26.2",
35
29
  "@flakiness/flakiness-report": "^0.16.0",
36
- "@flakiness/shared": "0.147.0",
37
- "body-parser": "^1.20.3",
38
30
  "chalk": "^5.6.2",
39
- "compression": "^1.8.1",
40
31
  "debug": "^4.3.7",
41
- "express": "^4.21.2",
42
- "express-async-errors": "^3.1.1",
43
32
  "open": "^10.2.0",
44
- "zod": "^3.25.23"
33
+ "stable-hash": "^0.0.6"
45
34
  }
46
35
  }