@stanterprise/playwright-reporter 0.0.18 → 0.0.19

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
@@ -92,6 +92,46 @@ npx playwright test
92
92
  | `grpcMaxMessageSize` | number | `104857600` | Max message size in bytes (100MB default) |
93
93
  | `maxAttachmentSize` | number | `10485760` | Max attachment content size in bytes (10MB default) |
94
94
  | `verbose` | boolean | `false` | Enable verbose logging |
95
+ | `grpcMaxRetries` | number | `3` | Maximum number of retry attempts for failed gRPC calls |
96
+ | `grpcRetryDelay` | number | `100` | Initial delay for retries in ms (uses exponential backoff) |
97
+
98
+ ### Retry Configuration
99
+
100
+ The reporter automatically retries failed gRPC calls for transient errors using exponential backoff:
101
+
102
+ - **Retryable errors**: `UNAVAILABLE`, `DEADLINE_EXCEEDED`, `INTERNAL`, `UNKNOWN`
103
+ - **Non-retryable errors**: `INVALID_ARGUMENT`, `NOT_FOUND`, `PERMISSION_DENIED`, `UNAUTHENTICATED`, `ALREADY_EXISTS`
104
+
105
+ **Exponential backoff formula**: `grpcRetryDelay * (2 ^ attemptNumber)`
106
+
107
+ Example with default settings (`grpcRetryDelay: 100`, `grpcMaxRetries: 3`):
108
+ - Initial attempt: Immediate
109
+ - Retry 1: Wait 100ms (100 * 2^0)
110
+ - Retry 2: Wait 200ms (100 * 2^1)
111
+ - Retry 3: Wait 400ms (100 * 2^2)
112
+
113
+ ```typescript
114
+ export default defineConfig({
115
+ reporter: [
116
+ [
117
+ "stanterprise-playwright-reporter",
118
+ {
119
+ grpcMaxRetries: 5, // Try up to 5 times
120
+ grpcRetryDelay: 200, // Start with 200ms delay
121
+ verbose: true, // Log retry attempts
122
+ },
123
+ ],
124
+ ],
125
+ });
126
+ ```
127
+
128
+ To disable retries:
129
+
130
+ ```typescript
131
+ {
132
+ grpcMaxRetries: 0 // No retries, fail fast
133
+ }
134
+ ```
95
135
 
96
136
  ## Sharding Support
97
137
 
@@ -1,6 +1,9 @@
1
1
  import { StanterpriseReporterOptions } from "../types";
2
2
  import * as grpc from "@grpc/grpc-js";
3
3
  export default function getClient(options: StanterpriseReporterOptions): grpc.Client | null;
4
+ /**
5
+ * Helper: generic unary call using raw method path with retry support
6
+ */
4
7
  export declare function reportUnary(options: StanterpriseReporterOptions, grpcClient: grpc.Client, path: string, message: {
5
8
  serialize?: (w?: any) => Uint8Array;
6
9
  serializeBinary?: () => Uint8Array;
@@ -49,11 +49,28 @@ function getClient(options) {
49
49
  }
50
50
  return null;
51
51
  }
52
- // Helper: generic unary call using raw method path
53
- async function reportUnary(options, grpcClient, path, message, deadlineMs = 1000) {
54
- if (!options.grpcEnabled || !grpcClient) {
55
- return Buffer.alloc(0);
56
- }
52
+ /**
53
+ * Determines if a gRPC error code represents a transient failure worth retrying
54
+ */
55
+ function shouldAttemptRetryForCode(errorCode) {
56
+ const transientCodes = new Set([
57
+ grpc.status.UNAVAILABLE,
58
+ grpc.status.DEADLINE_EXCEEDED,
59
+ grpc.status.INTERNAL,
60
+ grpc.status.UNKNOWN,
61
+ ]);
62
+ return transientCodes.has(errorCode);
63
+ }
64
+ /**
65
+ * Calculates wait time using power-of-two exponential backoff
66
+ */
67
+ function calculateWaitDuration(baseDelayMs, attemptsSoFar) {
68
+ return baseDelayMs * Math.pow(2, attemptsSoFar);
69
+ }
70
+ /**
71
+ * Performs a single gRPC unary request attempt
72
+ */
73
+ function executeSingleAttempt(grpcClient, path, message, deadlineMs) {
57
74
  const reqSerialize = (arg) => {
58
75
  const m = arg;
59
76
  const bytes = m?.serializeBinary
@@ -81,3 +98,59 @@ async function reportUnary(options, grpcClient, path, message, deadlineMs = 1000
81
98
  }
82
99
  });
83
100
  }
101
+ /**
102
+ * Pauses execution for specified milliseconds with unref'd timer to avoid blocking shutdown
103
+ */
104
+ function pauseExecution(durationMs) {
105
+ return new Promise((resolve) => {
106
+ const timer = setTimeout(resolve, durationMs);
107
+ // Unref the timer so it doesn't keep the process alive
108
+ timer.unref();
109
+ });
110
+ }
111
+ /**
112
+ * Attempts gRPC call with exponential backoff retry logic using iterative approach
113
+ */
114
+ async function attemptWithRetries(options, grpcClient, path, message, deadlineMs, maxRetries) {
115
+ let attemptNumber = 0;
116
+ let lastError = null;
117
+ // Try initial attempt + retries
118
+ for (let retriesRemaining = maxRetries; retriesRemaining >= 0; retriesRemaining--) {
119
+ try {
120
+ return await executeSingleAttempt(grpcClient, path, message, deadlineMs);
121
+ }
122
+ catch (err) {
123
+ lastError = err;
124
+ const grpcErr = err;
125
+ const statusCode = grpcErr?.code ?? grpc.status.UNKNOWN;
126
+ // Check if we should retry this error
127
+ const isRetryable = shouldAttemptRetryForCode(statusCode);
128
+ const hasRetriesLeft = retriesRemaining > 0;
129
+ if (!isRetryable || !hasRetriesLeft) {
130
+ throw err;
131
+ }
132
+ // Log retry attempt if verbose
133
+ if (options.verbose) {
134
+ console.log(`gRPC call failed (code: ${statusCode}), retrying... ` +
135
+ `(attempt ${attemptNumber + 1}, ${retriesRemaining} retries left)`);
136
+ }
137
+ // Calculate and wait for backoff duration
138
+ const baseDelay = options.grpcRetryDelay ?? 100;
139
+ const waitTime = calculateWaitDuration(baseDelay, attemptNumber);
140
+ await pauseExecution(waitTime);
141
+ attemptNumber++;
142
+ }
143
+ }
144
+ // This should never be reached due to the throw in the loop, but TypeScript needs it
145
+ throw lastError || new Error("Retry loop exhausted without result");
146
+ }
147
+ /**
148
+ * Helper: generic unary call using raw method path with retry support
149
+ */
150
+ async function reportUnary(options, grpcClient, path, message, deadlineMs = 1000) {
151
+ if (!options.grpcEnabled || !grpcClient) {
152
+ return Buffer.alloc(0);
153
+ }
154
+ const maxRetries = options.grpcMaxRetries ?? 3;
155
+ return attemptWithRetries(options, grpcClient, path, message, deadlineMs, maxRetries);
156
+ }
package/dist/types.d.ts CHANGED
@@ -36,6 +36,17 @@ export interface StanterpriseReporterOptions {
36
36
  * @default false
37
37
  */
38
38
  verbose?: boolean;
39
+ /**
40
+ * Maximum number of retry attempts for failed gRPC calls
41
+ * @default 3
42
+ */
43
+ grpcMaxRetries?: number;
44
+ /**
45
+ * Initial delay between retry attempts in milliseconds
46
+ * Actual delay uses exponential backoff: initialDelay * (2 ^ attemptNumber)
47
+ * @default 100
48
+ */
49
+ grpcRetryDelay?: number;
39
50
  }
40
51
  /**
41
52
  * Internal type for managing test execution state
@@ -16,6 +16,10 @@ function defineOptions(providedOptions) {
16
16
  providedOptions.grpcEnabled ??
17
17
  (process.env.STANTERPRISE_GRPC_ENABLED || "true").toLowerCase() !== "false";
18
18
  result.grpcTimeout = providedOptions.grpcTimeout || 1000;
19
+ result.grpcMaxMessageSize = providedOptions.grpcMaxMessageSize || 104857600;
20
+ result.maxAttachmentSize = providedOptions.maxAttachmentSize || 10485760;
19
21
  result.verbose = providedOptions.verbose || false;
22
+ result.grpcMaxRetries = providedOptions.grpcMaxRetries ?? 3;
23
+ result.grpcRetryDelay = providedOptions.grpcRetryDelay || 100;
20
24
  return result;
21
25
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@stanterprise/playwright-reporter",
3
- "version": "0.0.18",
3
+ "version": "0.0.19",
4
4
  "description": "Custom Playwright test reporter that sends test results to Stanterprise via gRPC",
5
5
  "homepage": "https://github.com/stanterprise/stanterprise-playwright-reporter#readme",
6
6
  "bugs": {