qase-javascript-commons 2.4.10 → 2.4.12
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/changelog.md +12 -0
- package/dist/client/clientV1.d.ts +21 -0
- package/dist/client/clientV1.js +99 -2
- package/package.json +18 -17
- package/tsconfig.build.json +3 -1
package/changelog.md
CHANGED
|
@@ -1,3 +1,15 @@
|
|
|
1
|
+
# qase-javascript-commons@2.4.12
|
|
2
|
+
|
|
3
|
+
## What's new
|
|
4
|
+
|
|
5
|
+
Improved the retry mechanism for uploading attachments.
|
|
6
|
+
|
|
7
|
+
# qase-javascript-commons@2.4.11
|
|
8
|
+
|
|
9
|
+
## What's new
|
|
10
|
+
|
|
11
|
+
Added retry mechanism for uploading attachments.
|
|
12
|
+
|
|
1
13
|
# qase-javascript-commons@2.4.10
|
|
2
14
|
|
|
3
15
|
## What's new
|
|
@@ -21,6 +21,27 @@ export declare class ClientV1 implements IClient {
|
|
|
21
21
|
private getErrorMessage;
|
|
22
22
|
uploadAttachment(attachment: Attachment): Promise<string>;
|
|
23
23
|
protected uploadAttachments(attachments: Attachment[]): Promise<string[]>;
|
|
24
|
+
/**
|
|
25
|
+
* Upload attachment with retry logic for 429 errors
|
|
26
|
+
* @param project Project code
|
|
27
|
+
* @param data Attachment data
|
|
28
|
+
* @param attachmentName Attachment name for logging
|
|
29
|
+
* @param maxRetries Maximum number of retry attempts
|
|
30
|
+
* @param initialDelay Initial delay in milliseconds
|
|
31
|
+
* @returns Promise with upload response
|
|
32
|
+
*/
|
|
33
|
+
private uploadAttachmentWithRetry;
|
|
34
|
+
/**
|
|
35
|
+
* Extract Retry-After header value from response or return null
|
|
36
|
+
* @param error Axios error
|
|
37
|
+
* @returns Retry-After value in milliseconds or null
|
|
38
|
+
*/
|
|
39
|
+
private getRetryAfter;
|
|
40
|
+
/**
|
|
41
|
+
* Delay execution for specified milliseconds
|
|
42
|
+
* @param ms Milliseconds to delay
|
|
43
|
+
*/
|
|
44
|
+
private delay;
|
|
24
45
|
private prepareAttachmentData;
|
|
25
46
|
private getEnvironmentId;
|
|
26
47
|
private prepareRunObject;
|
package/dist/client/clientV1.js
CHANGED
|
@@ -145,11 +145,21 @@ class ClientV1 {
|
|
|
145
145
|
return [];
|
|
146
146
|
}
|
|
147
147
|
const uploadedHashes = [];
|
|
148
|
-
|
|
148
|
+
// Add initial random delay to spread out requests from different workers/shard
|
|
149
|
+
// This helps prevent all workers from hitting the API at the same time
|
|
150
|
+
if (attachments.length > 0) {
|
|
151
|
+
const initialJitter = Math.random() * 500; // 0-500ms random delay
|
|
152
|
+
await this.delay(initialJitter);
|
|
153
|
+
}
|
|
154
|
+
for (let i = 0; i < attachments.length; i++) {
|
|
155
|
+
const attachment = attachments[i];
|
|
156
|
+
if (!attachment) {
|
|
157
|
+
continue;
|
|
158
|
+
}
|
|
149
159
|
try {
|
|
150
160
|
this.logger.logDebug(`Uploading attachment: ${attachment.file_path ?? attachment.file_name}`);
|
|
151
161
|
const data = this.prepareAttachmentData(attachment);
|
|
152
|
-
const response = await this.
|
|
162
|
+
const response = await this.uploadAttachmentWithRetry(this.config.project, [data], attachment.file_path ?? attachment.file_name);
|
|
153
163
|
const hash = response.data.result?.[0]?.hash;
|
|
154
164
|
if (hash) {
|
|
155
165
|
uploadedHashes.push(hash);
|
|
@@ -158,9 +168,96 @@ class ClientV1 {
|
|
|
158
168
|
catch (error) {
|
|
159
169
|
this.logger.logError('Cannot upload attachment:', error);
|
|
160
170
|
}
|
|
171
|
+
// Add delay between requests to avoid rate limiting
|
|
172
|
+
// Skip delay after the last attachment
|
|
173
|
+
if (i < attachments.length - 1) {
|
|
174
|
+
// Increased delay with random jitter to prevent synchronization
|
|
175
|
+
const baseDelay = 1000; // 1000ms (1 second) base delay
|
|
176
|
+
const jitter = Math.random() * 300; // 0-300ms random jitter
|
|
177
|
+
await this.delay(baseDelay + jitter);
|
|
178
|
+
}
|
|
161
179
|
}
|
|
162
180
|
return uploadedHashes;
|
|
163
181
|
}
|
|
182
|
+
/**
|
|
183
|
+
* Upload attachment with retry logic for 429 errors
|
|
184
|
+
* @param project Project code
|
|
185
|
+
* @param data Attachment data
|
|
186
|
+
* @param attachmentName Attachment name for logging
|
|
187
|
+
* @param maxRetries Maximum number of retry attempts
|
|
188
|
+
* @param initialDelay Initial delay in milliseconds
|
|
189
|
+
* @returns Promise with upload response
|
|
190
|
+
*/
|
|
191
|
+
async uploadAttachmentWithRetry(project, data, attachmentName, maxRetries = 5, initialDelay = 1000) {
|
|
192
|
+
let lastError;
|
|
193
|
+
let delay = initialDelay;
|
|
194
|
+
for (let attempt = 0; attempt <= maxRetries; attempt++) {
|
|
195
|
+
try {
|
|
196
|
+
return await this.attachmentClient.uploadAttachment(project, data);
|
|
197
|
+
}
|
|
198
|
+
catch (error) {
|
|
199
|
+
lastError = error;
|
|
200
|
+
// Check if it's a 429 error (Too Many Requests)
|
|
201
|
+
if ((0, is_axios_error_1.isAxiosError)(error)) {
|
|
202
|
+
if (error.response?.status === 429) {
|
|
203
|
+
if (attempt < maxRetries) {
|
|
204
|
+
const retryAfter = this.getRetryAfter(error);
|
|
205
|
+
const baseWaitTime = retryAfter ?? delay;
|
|
206
|
+
// Add jitter (random delay) to prevent all workers from retrying simultaneously
|
|
207
|
+
// Jitter is 10-30% of the wait time to spread out retry attempts
|
|
208
|
+
const jitterPercent = 0.1 + Math.random() * 0.2; // 10-30%
|
|
209
|
+
const jitter = baseWaitTime * jitterPercent;
|
|
210
|
+
const waitTime = Math.floor(baseWaitTime + jitter);
|
|
211
|
+
this.logger.logDebug(`Rate limit exceeded (429) for attachment "${attachmentName}". ` +
|
|
212
|
+
`Retrying in ${waitTime}ms (attempt ${attempt + 1}/${maxRetries})`);
|
|
213
|
+
await this.delay(waitTime);
|
|
214
|
+
// Exponential backoff: double the delay for next attempt
|
|
215
|
+
delay = Math.min(delay * 2, 30000); // Cap at 30 seconds
|
|
216
|
+
}
|
|
217
|
+
else {
|
|
218
|
+
this.logger.logError(`Failed to upload attachment "${attachmentName}" after ${maxRetries} retries due to rate limiting`);
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
else {
|
|
222
|
+
// For non-429 errors, throw immediately
|
|
223
|
+
throw error;
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
else {
|
|
227
|
+
// For non-Axios errors, throw immediately
|
|
228
|
+
throw error;
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
// If we exhausted all retries, throw the last error
|
|
233
|
+
throw lastError;
|
|
234
|
+
}
|
|
235
|
+
/**
|
|
236
|
+
* Extract Retry-After header value from response or return null
|
|
237
|
+
* @param error Axios error
|
|
238
|
+
* @returns Retry-After value in milliseconds or null
|
|
239
|
+
*/
|
|
240
|
+
getRetryAfter(error) {
|
|
241
|
+
const headers = error.response?.headers;
|
|
242
|
+
if (!headers) {
|
|
243
|
+
return null;
|
|
244
|
+
}
|
|
245
|
+
const retryAfterHeader = headers['retry-after'];
|
|
246
|
+
if (retryAfterHeader && typeof retryAfterHeader === 'string') {
|
|
247
|
+
const retryAfterSeconds = parseInt(retryAfterHeader, 10);
|
|
248
|
+
if (!isNaN(retryAfterSeconds)) {
|
|
249
|
+
return retryAfterSeconds * 1000; // Convert to milliseconds
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
return null;
|
|
253
|
+
}
|
|
254
|
+
/**
|
|
255
|
+
* Delay execution for specified milliseconds
|
|
256
|
+
* @param ms Milliseconds to delay
|
|
257
|
+
*/
|
|
258
|
+
delay(ms) {
|
|
259
|
+
return new Promise(resolve => setTimeout(resolve, ms));
|
|
260
|
+
}
|
|
164
261
|
prepareAttachmentData(attachment) {
|
|
165
262
|
if (attachment.file_path) {
|
|
166
263
|
return {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "qase-javascript-commons",
|
|
3
|
-
"version": "2.4.
|
|
3
|
+
"version": "2.4.12",
|
|
4
4
|
"description": "Qase JS Reporters",
|
|
5
5
|
"main": "./dist/index.js",
|
|
6
6
|
"types": "./dist/index.d.ts",
|
|
@@ -24,31 +24,32 @@
|
|
|
24
24
|
"author": "Qase Team <support@qase.io>",
|
|
25
25
|
"license": "Apache-2.0",
|
|
26
26
|
"dependencies": {
|
|
27
|
-
"ajv": "^8.
|
|
27
|
+
"ajv": "^8.17.1",
|
|
28
28
|
"async-mutex": "~0.5.0",
|
|
29
29
|
"chalk": "^4.1.2",
|
|
30
|
-
"env-schema": "^5.2.
|
|
31
|
-
"form-data": "^4.0.
|
|
30
|
+
"env-schema": "^5.2.1",
|
|
31
|
+
"form-data": "^4.0.5",
|
|
32
32
|
"lodash.get": "^4.4.2",
|
|
33
33
|
"lodash.merge": "^4.6.2",
|
|
34
34
|
"lodash.mergewith": "^4.6.2",
|
|
35
|
-
"mime-types": "^2.1.
|
|
35
|
+
"mime-types": "^2.1.35",
|
|
36
36
|
"qase-api-client": "~1.1.0",
|
|
37
|
-
"qase-api-v2-client": "~1.0.
|
|
37
|
+
"qase-api-v2-client": "~1.0.3",
|
|
38
38
|
"strip-ansi": "^6.0.1",
|
|
39
|
-
"uuid": "^9.0.
|
|
39
|
+
"uuid": "^9.0.1"
|
|
40
40
|
},
|
|
41
41
|
"devDependencies": {
|
|
42
|
-
"@jest/globals": "^29.
|
|
43
|
-
"@types/jest": "^29.5.
|
|
44
|
-
"@types/lodash.get": "^4.4.
|
|
45
|
-
"@types/lodash.merge": "^4.6.
|
|
46
|
-
"@types/lodash.mergewith": "^4.6.
|
|
42
|
+
"@jest/globals": "^29.7.0",
|
|
43
|
+
"@types/jest": "^29.5.14",
|
|
44
|
+
"@types/lodash.get": "^4.4.9",
|
|
45
|
+
"@types/lodash.merge": "^4.6.9",
|
|
46
|
+
"@types/lodash.mergewith": "^4.6.9",
|
|
47
47
|
"@types/mime-types": "^2.1.4",
|
|
48
|
-
"@types/
|
|
49
|
-
"@types/
|
|
50
|
-
"
|
|
51
|
-
"
|
|
52
|
-
"
|
|
48
|
+
"@types/minimatch": "^6.0.0",
|
|
49
|
+
"@types/node": "^20.19.25",
|
|
50
|
+
"@types/uuid": "^9.0.8",
|
|
51
|
+
"axios": "^1.13.2",
|
|
52
|
+
"jest": "^29.7.0",
|
|
53
|
+
"ts-jest": "^29.4.5"
|
|
53
54
|
}
|
|
54
55
|
}
|