@stanterprise/playwright-reporter 0.0.17 → 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,120 @@ 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
+ ```
135
+
136
+ ## Sharding Support
137
+
138
+ The reporter automatically detects and reports when tests are running in sharded mode. When sharding is configured, the reporter includes the following metadata in test run events:
139
+
140
+ - `shard.current`: The current shard number (1-based)
141
+ - `shard.total`: The total number of shards
142
+
143
+ ### Configuring Sharding
144
+
145
+ In your `playwright.config.ts`:
146
+
147
+ ```typescript
148
+ import { defineConfig } from "@playwright/test";
149
+
150
+ export default defineConfig({
151
+ shard: { total: 5, current: 1 },
152
+ reporter: [["stanterprise-playwright-reporter"]],
153
+ });
154
+ ```
155
+
156
+ Or via CLI:
157
+
158
+ ```bash
159
+ npx playwright test --shard=1/5
160
+ npx playwright test --shard=2/5
161
+ # ... etc
162
+ ```
163
+
164
+ ### Aggregating Results Across Shards
165
+
166
+ **Important:** To have a complete set of tests accumulated in the observability server, you must specify the same `STANTERPRISE_RUN_ID` environment variable for all shards. This ensures that results from all shards are associated with the same test run.
167
+
168
+ ```bash
169
+ # Set the same run ID for all shards
170
+ export STANTERPRISE_RUN_ID="my-test-run-123"
171
+
172
+ # Run all shards with the same run ID
173
+ npx playwright test --shard=1/5
174
+ npx playwright test --shard=2/5
175
+ npx playwright test --shard=3/5
176
+ npx playwright test --shard=4/5
177
+ npx playwright test --shard=5/5
178
+ ```
179
+
180
+ In CI/CD environments, you can use a unique identifier from your CI system:
181
+
182
+ ```bash
183
+ # GitHub Actions example
184
+ STANTERPRISE_RUN_ID="${GITHUB_RUN_ID}" npx playwright test --shard=1/5
185
+
186
+ # GitLab CI example
187
+ STANTERPRISE_RUN_ID="${CI_PIPELINE_ID}" npx playwright test --shard=1/5
188
+
189
+ # Jenkins example
190
+ STANTERPRISE_RUN_ID="${BUILD_ID}" npx playwright test --shard=1/5
191
+ ```
192
+
193
+ Without a shared `STANTERPRISE_RUN_ID`, each shard will generate its own unique run ID, and the results will be reported as separate test runs rather than a single aggregated run.
194
+
195
+ The shard information is automatically included in the metadata sent to the server, allowing you to:
196
+
197
+ - Track which tests ran on which shard
198
+ - Aggregate results from multiple shards
199
+ - Debug shard-specific issues
200
+
201
+ When verbose logging is enabled, you'll see shard information in the console output:
202
+
203
+ ```
204
+ Stanterprise Reporter: Test run started with ID: 123e4567-e89b-12d3-a456-426614174000
205
+ Number of tests: 42
206
+ Run started at: 2026-01-25T10:30:00.000Z
207
+ Shard: 1 of 5
208
+ ```
95
209
 
96
210
  ## What Gets Reported
97
211
 
@@ -240,7 +354,6 @@ This error can also occur when test event payloads exceed the gRPC message size
240
354
  ```
241
355
 
242
356
  2. **Reduce attachment sizes**:
243
-
244
357
  - Configure Playwright to save attachments to disk instead of embedding them
245
358
  - The reporter automatically uses file paths when available instead of content
246
359
 
@@ -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
+ }
@@ -19,6 +19,11 @@ function handleOnBeginEvent(config, suite, name, runId, client, options) {
19
19
  variables.forEach((value, key) => {
20
20
  metadata[key] = value;
21
21
  });
22
+ // Add shard information if present
23
+ if (config.shard) {
24
+ metadata["shard.current"] = String(config.shard.current);
25
+ metadata["shard.total"] = String(config.shard.total);
26
+ }
22
27
  const request = new events_1.ReportRunStartEventRequest({
23
28
  run_id: runId,
24
29
  name: name,
package/dist/reporter.js CHANGED
@@ -45,6 +45,9 @@ class StanterpriseReporter {
45
45
  console.log(`Stanterprise Reporter: Test run started with ID: ${this.runId}`);
46
46
  console.log(`Number of tests: ${suite.allTests().length}`);
47
47
  console.log(`Run started at: ${this.runStartTime.toISOString()}`);
48
+ if (config.shard) {
49
+ console.log(`Shard: ${config.shard.current} of ${config.shard.total}`);
50
+ }
48
51
  }
49
52
  if (this.options.grpcEnabled) {
50
53
  (0, handlers_1.handleOnBeginEvent)(config, suite, process.env.STANTERPRISE_TEST_RUN_NAME || "Playwright Test Run", this.runId, this.grpcClient, this.options);
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.17",
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": {