qase-javascript-commons 2.4.9 → 2.4.11

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 CHANGED
@@ -1,3 +1,15 @@
1
+ # qase-javascript-commons@2.4.11
2
+
3
+ ## What's new
4
+
5
+ Added retry mechanism for uploading attachments.
6
+
7
+ # qase-javascript-commons@2.4.10
8
+
9
+ ## What's new
10
+
11
+ Fixed an issue with report link for enterprise customers.
12
+
1
13
  # qase-javascript-commons@2.4.9
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;
@@ -13,7 +13,7 @@ const dateUtils_1 = require("./dateUtils");
13
13
  const form_data_1 = __importDefault(require("form-data"));
14
14
  const DEFAULT_API_HOST = 'qase.io';
15
15
  const API_BASE_URL = 'https://api-';
16
- const APP_BASE_URL = 'https://app-';
16
+ const APP_BASE_URL = 'https://';
17
17
  const API_VERSION = '/v1';
18
18
  var ApiErrorCode;
19
19
  (function (ApiErrorCode) {
@@ -145,11 +145,15 @@ class ClientV1 {
145
145
  return [];
146
146
  }
147
147
  const uploadedHashes = [];
148
- for (const attachment of attachments) {
148
+ for (let i = 0; i < attachments.length; i++) {
149
+ const attachment = attachments[i];
150
+ if (!attachment) {
151
+ continue;
152
+ }
149
153
  try {
150
154
  this.logger.logDebug(`Uploading attachment: ${attachment.file_path ?? attachment.file_name}`);
151
155
  const data = this.prepareAttachmentData(attachment);
152
- const response = await this.attachmentClient.uploadAttachment(this.config.project, [data]);
156
+ const response = await this.uploadAttachmentWithRetry(this.config.project, [data], attachment.file_path ?? attachment.file_name);
153
157
  const hash = response.data.result?.[0]?.hash;
154
158
  if (hash) {
155
159
  uploadedHashes.push(hash);
@@ -158,9 +162,88 @@ class ClientV1 {
158
162
  catch (error) {
159
163
  this.logger.logError('Cannot upload attachment:', error);
160
164
  }
165
+ // Add delay between requests to avoid rate limiting
166
+ // Skip delay after the last attachment
167
+ if (i < attachments.length - 1) {
168
+ await this.delay(100); // 100ms delay between requests
169
+ }
161
170
  }
162
171
  return uploadedHashes;
163
172
  }
173
+ /**
174
+ * Upload attachment with retry logic for 429 errors
175
+ * @param project Project code
176
+ * @param data Attachment data
177
+ * @param attachmentName Attachment name for logging
178
+ * @param maxRetries Maximum number of retry attempts
179
+ * @param initialDelay Initial delay in milliseconds
180
+ * @returns Promise with upload response
181
+ */
182
+ async uploadAttachmentWithRetry(project, data, attachmentName, maxRetries = 5, initialDelay = 1000) {
183
+ let lastError;
184
+ let delay = initialDelay;
185
+ for (let attempt = 0; attempt <= maxRetries; attempt++) {
186
+ try {
187
+ return await this.attachmentClient.uploadAttachment(project, data);
188
+ }
189
+ catch (error) {
190
+ lastError = error;
191
+ // Check if it's a 429 error (Too Many Requests)
192
+ if ((0, is_axios_error_1.isAxiosError)(error)) {
193
+ if (error.response?.status === 429) {
194
+ if (attempt < maxRetries) {
195
+ const retryAfter = this.getRetryAfter(error);
196
+ const waitTime = retryAfter ?? delay;
197
+ this.logger.logDebug(`Rate limit exceeded (429) for attachment "${attachmentName}". ` +
198
+ `Retrying in ${waitTime}ms (attempt ${attempt + 1}/${maxRetries})`);
199
+ await this.delay(waitTime);
200
+ // Exponential backoff: double the delay for next attempt
201
+ delay = Math.min(delay * 2, 30000); // Cap at 30 seconds
202
+ }
203
+ else {
204
+ this.logger.logError(`Failed to upload attachment "${attachmentName}" after ${maxRetries} retries due to rate limiting`);
205
+ }
206
+ }
207
+ else {
208
+ // For non-429 errors, throw immediately
209
+ throw error;
210
+ }
211
+ }
212
+ else {
213
+ // For non-Axios errors, throw immediately
214
+ throw error;
215
+ }
216
+ }
217
+ }
218
+ // If we exhausted all retries, throw the last error
219
+ throw lastError;
220
+ }
221
+ /**
222
+ * Extract Retry-After header value from response or return null
223
+ * @param error Axios error
224
+ * @returns Retry-After value in milliseconds or null
225
+ */
226
+ getRetryAfter(error) {
227
+ const headers = error.response?.headers;
228
+ if (!headers) {
229
+ return null;
230
+ }
231
+ const retryAfterHeader = headers['retry-after'];
232
+ if (retryAfterHeader && typeof retryAfterHeader === 'string') {
233
+ const retryAfterSeconds = parseInt(retryAfterHeader, 10);
234
+ if (!isNaN(retryAfterSeconds)) {
235
+ return retryAfterSeconds * 1000; // Convert to milliseconds
236
+ }
237
+ }
238
+ return null;
239
+ }
240
+ /**
241
+ * Delay execution for specified milliseconds
242
+ * @param ms Milliseconds to delay
243
+ */
244
+ delay(ms) {
245
+ return new Promise(resolve => setTimeout(resolve, ms));
246
+ }
164
247
  prepareAttachmentData(attachment) {
165
248
  if (attachment.file_path) {
166
249
  return {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "qase-javascript-commons",
3
- "version": "2.4.9",
3
+ "version": "2.4.11",
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.12.0",
27
+ "ajv": "^8.17.1",
28
28
  "async-mutex": "~0.5.0",
29
29
  "chalk": "^4.1.2",
30
- "env-schema": "^5.2.0",
31
- "form-data": "^4.0.4",
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.33",
35
+ "mime-types": "^2.1.35",
36
36
  "qase-api-client": "~1.1.0",
37
- "qase-api-v2-client": "~1.0.1",
37
+ "qase-api-v2-client": "~1.0.3",
38
38
  "strip-ansi": "^6.0.1",
39
- "uuid": "^9.0.0"
39
+ "uuid": "^9.0.1"
40
40
  },
41
41
  "devDependencies": {
42
- "@jest/globals": "^29.5.0",
43
- "@types/jest": "^29.5.2",
44
- "@types/lodash.get": "^4.4.7",
45
- "@types/lodash.merge": "^4.6.7",
46
- "@types/lodash.mergewith": "^4.6.7",
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/node": "^20.12.5",
49
- "@types/uuid": "^9.0.1",
50
- "axios": "^1.12.0",
51
- "jest": "^29.5.0",
52
- "ts-jest": "^29.1.0"
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
  }
@@ -2,7 +2,9 @@
2
2
  "extends": "./tsconfig.json",
3
3
 
4
4
  "compilerOptions": {
5
- "noEmit": false
5
+ "noEmit": false,
6
+ "skipLibCheck": true,
7
+ "types": []
6
8
  },
7
9
 
8
10
  "include": ["./src/**/*.ts"]