@stanterprise/playwright-reporter 1.0.0 → 1.0.1

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.
Files changed (44) hide show
  1. package/README.md +261 -11
  2. package/dist/client/grpcClient.d.ts +16 -0
  3. package/dist/client/grpcClient.js +172 -0
  4. package/dist/client/messageQueue.d.ts +27 -0
  5. package/dist/client/messageQueue.js +71 -0
  6. package/dist/handlers/index.d.ts +9 -0
  7. package/dist/handlers/index.js +19 -0
  8. package/dist/handlers/onBeginHandler.d.ts +6 -0
  9. package/dist/handlers/onBeginHandler.js +98 -0
  10. package/dist/handlers/onEndHandler.d.ts +5 -0
  11. package/dist/handlers/onEndHandler.js +53 -0
  12. package/dist/handlers/onErrorHandler.d.ts +5 -0
  13. package/dist/handlers/onErrorHandler.js +16 -0
  14. package/dist/handlers/onStepBeginHandler.d.ts +5 -0
  15. package/dist/handlers/onStepBeginHandler.js +39 -0
  16. package/dist/handlers/onStepEndHandler.d.ts +5 -0
  17. package/dist/handlers/onStepEndHandler.js +62 -0
  18. package/dist/handlers/onTestBeginHandler.d.ts +5 -0
  19. package/dist/handlers/onTestBeginHandler.js +33 -0
  20. package/dist/handlers/onTestEndHandler.d.ts +5 -0
  21. package/dist/handlers/onTestEndHandler.js +58 -0
  22. package/dist/handlers/onTestFailHandler.d.ts +5 -0
  23. package/dist/handlers/onTestFailHandler.js +25 -0
  24. package/dist/reporter.d.ts +4 -15
  25. package/dist/reporter.js +97 -386
  26. package/dist/types.d.ts +56 -0
  27. package/dist/utils/attachmentProcessor.d.ts +6 -2
  28. package/dist/utils/attachmentProcessor.js +33 -4
  29. package/dist/utils/debugLogger.d.ts +13 -0
  30. package/dist/utils/debugLogger.js +87 -0
  31. package/dist/utils/envRetriever.d.ts +9 -0
  32. package/dist/utils/envRetriever.js +23 -0
  33. package/dist/utils/idGenerators.d.ts +20 -0
  34. package/dist/utils/idGenerators.js +40 -0
  35. package/dist/utils/index.d.ts +4 -0
  36. package/dist/utils/index.js +4 -0
  37. package/dist/utils/metadataHelpers.d.ts +42 -0
  38. package/dist/utils/metadataHelpers.js +91 -0
  39. package/dist/utils/optionsMapper.d.ts +7 -0
  40. package/dist/utils/optionsMapper.js +51 -0
  41. package/dist/utils/statusMapper.js +7 -6
  42. package/dist/utils/timeHelpers.d.ts +5 -5
  43. package/dist/utils/timeHelpers.js +24 -8
  44. package/package.json +6 -7
package/README.md CHANGED
@@ -9,12 +9,13 @@ Custom Playwright test reporter that sends test results to Stanterprise via gRPC
9
9
  - 📎 **Attachment Support**: Handles screenshots, videos, and other test attachments
10
10
  - 🔌 **gRPC Integration**: Communicates with Stanterprise backend via gRPC protocol
11
11
  - ⚙️ **Configurable**: Supports environment variables and configuration options
12
+ - 🐛 **Debug Mode**: Captures outgoing gRPC messages to a JSONL file for troubleshooting
12
13
  - 🛡️ **Error Resilient**: Graceful error handling that doesn't interrupt test execution
13
14
 
14
15
  ## Installation
15
16
 
16
17
  ```bash
17
- npm install stanterprise-playwright-reporter --save-dev
18
+ npm install @stanterprise/playwright-reporter --save-dev
18
19
  ```
19
20
 
20
21
  ## Configuration
@@ -27,7 +28,7 @@ Add the reporter to your `playwright.config.ts`:
27
28
  import { defineConfig } from "@playwright/test";
28
29
 
29
30
  export default defineConfig({
30
- reporter: [["stanterprise-playwright-reporter"]],
31
+ reporter: [["@stanterprise/playwright-reporter"]],
31
32
  // ... other config
32
33
  });
33
34
  ```
@@ -40,12 +41,16 @@ import { defineConfig } from "@playwright/test";
40
41
  export default defineConfig({
41
42
  reporter: [
42
43
  [
43
- "stanterprise-playwright-reporter",
44
+ "@stanterprise/playwright-reporter",
44
45
  {
45
46
  grpcAddress: "localhost:50051", // gRPC server address
46
47
  grpcEnabled: true, // Enable/disable gRPC reporting
47
48
  grpcTimeout: 1000, // Timeout for gRPC calls in milliseconds
49
+ grpcMaxMessageSize: 104857600, // Max message size (100MB default)
50
+ maxAttachmentSize: 10485760, // Max attachment size (10MB default)
48
51
  verbose: false, // Enable verbose logging
52
+ debug: false, // Enable debug mode (writes gRPC messages to JSONL file)
53
+ debugFile: "stanterprise-debug.jsonl", // Path for debug JSONL output
49
54
  },
50
55
  ],
51
56
  ],
@@ -58,22 +63,205 @@ export default defineConfig({
58
63
  You can also configure the reporter using environment variables:
59
64
 
60
65
  - `STANTERPRISE_GRPC_ADDRESS`: gRPC server address (default: `localhost:50051`)
66
+ - `STANTERPRISE_GRPC_HOST`: gRPC server host
61
67
  - `STANTERPRISE_GRPC_ENABLED`: Enable/disable gRPC reporting (default: `true`)
68
+ - `STANTERPRISE_META_*`: Custom metadata fields for test runs (prefix will be stripped)
69
+ - `STANTERPRISE_DEBUG`: Enable debug mode — captures outgoing gRPC messages to a JSONL file (default: `false`; case-insensitive: `"true"`, `"TRUE"`, `"True"` all work)
70
+ - `STANTERPRISE_DEBUG_FILE`: File path for debug JSONL output (default: `stanterprise-debug.jsonl`)
71
+
72
+ #### Custom Metadata
73
+
74
+ Any environment variable with the `STANTERPRISE_META_` prefix will be automatically included in the test run metadata. The prefix is stripped from the key name.
75
+
76
+ For example:
77
+
78
+ - `STANTERPRISE_META_BUILD_ID=12345` becomes `BUILD_ID: 12345` in metadata
79
+ - `STANTERPRISE_META_BRANCH=main` becomes `BRANCH: main` in metadata
80
+ - `STANTERPRISE_META_COMMIT_SHA=abc123` becomes `COMMIT_SHA: abc123` in metadata
62
81
 
63
82
  Example:
64
83
 
65
84
  ```bash
66
- STANTERPRISE_GRPC_ADDRESS=myserver.com:50051 npx playwright test
85
+ STANTERPRISE_GRPC_ADDRESS=myserver.com:50051 \
86
+ STANTERPRISE_META_BUILD_ID=12345 \
87
+ STANTERPRISE_META_BRANCH=main \
88
+ npx playwright test
67
89
  ```
68
90
 
69
91
  ## Configuration Options
70
92
 
71
- | Option | Type | Default | Description |
72
- | -------------- | ------- | ---------------- | ----------------------------------------- |
73
- | `grpcAddress` | string | `localhost:50051`| gRPC server address |
74
- | `grpcEnabled` | boolean | `true` | Enable/disable gRPC reporting |
75
- | `grpcTimeout` | number | `1000` | Timeout for gRPC calls in milliseconds |
76
- | `verbose` | boolean | `false` | Enable verbose logging |
93
+ | Option | Type | Default | Description |
94
+ | -------------------- | ------- | -------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
95
+ | `grpcAddress` | string | `localhost:50051` | Full gRPC server address. Takes precedence over `grpcHost` and `grpcPort`. |
96
+ | `grpcHost` | string | | gRPC server host. Used when `grpcAddress` is not provided. Combined with `grpcPort` to form the address. |
97
+ | `grpcPort` | number | `50051` | gRPC server port. Used when `grpcAddress` is not provided. Combined with `grpcHost` to form the address. |
98
+ | `grpcEnabled` | boolean | `true` | Enable/disable gRPC reporting |
99
+ | `grpcTimeout` | number | `1000` | Timeout for gRPC calls in milliseconds |
100
+ | `grpcMaxMessageSize` | number | `104857600` | Max message size in bytes (100MB default) |
101
+ | `maxAttachmentSize` | number | `10485760` | Max attachment content size in bytes (10MB default) |
102
+ | `verbose` | boolean | `false` | Enable verbose logging |
103
+ | `grpcMaxRetries` | number | `3` | Maximum number of retry attempts for failed gRPC calls |
104
+ | `grpcRetryDelay` | number | `100` | Initial delay for retries in ms (uses exponential backoff) |
105
+ | `debug` | boolean | `false` | Enable debug mode — writes all outgoing gRPC messages to a JSONL file. **Warning:** output may include sensitive request payloads and grow very large. Use only in controlled environments. |
106
+ | `debugFile` | string | `stanterprise-debug.jsonl` | File path for debug JSONL output. Parent directories are created automatically if they do not exist. |
107
+
108
+ ### Retry Configuration
109
+
110
+ The reporter automatically retries failed gRPC calls for transient errors using exponential backoff:
111
+
112
+ - **Retryable errors**: `UNAVAILABLE`, `DEADLINE_EXCEEDED`, `INTERNAL`, `UNKNOWN`
113
+ - **Non-retryable errors**: `INVALID_ARGUMENT`, `NOT_FOUND`, `PERMISSION_DENIED`, `UNAUTHENTICATED`, `ALREADY_EXISTS`
114
+
115
+ **Exponential backoff formula**: `grpcRetryDelay * (2 ^ attemptNumber)`
116
+
117
+ Example with default settings (`grpcRetryDelay: 100`, `grpcMaxRetries: 3`):
118
+
119
+ - Initial attempt: Immediate
120
+ - Retry 1: Wait 100ms (100 \* 2^0)
121
+ - Retry 2: Wait 200ms (100 \* 2^1)
122
+ - Retry 3: Wait 400ms (100 \* 2^2)
123
+
124
+ ```typescript
125
+ export default defineConfig({
126
+ reporter: [
127
+ [
128
+ "stanterprise-playwright-reporter",
129
+ {
130
+ grpcMaxRetries: 5, // Try up to 5 times
131
+ grpcRetryDelay: 200, // Start with 200ms delay
132
+ verbose: true, // Log retry attempts
133
+ },
134
+ ],
135
+ ],
136
+ });
137
+ ```
138
+
139
+ To disable retries:
140
+
141
+ ```typescript
142
+ {
143
+ grpcMaxRetries: 0; // No retries, fail fast
144
+ }
145
+ ```
146
+
147
+ ## Debug Mode
148
+
149
+ The reporter includes an opt-in debug mode that captures every outgoing gRPC message to a JSONL file. This is useful for troubleshooting, auditing what is sent to the backend, or diagnosing unexpected behavior.
150
+
151
+ > **Warning:** Debug output may include full request payloads — including attachment content and metadata — which can contain sensitive values. Debug files may also grow very large. Enable this only in controlled environments and handle the output file securely.
152
+
153
+ ### Enabling Debug Mode
154
+
155
+ Via configuration:
156
+
157
+ ```typescript
158
+ export default defineConfig({
159
+ reporter: [
160
+ [
161
+ "@stanterprise/playwright-reporter",
162
+ {
163
+ debug: true,
164
+ debugFile: "debug-output.jsonl", // optional, defaults to stanterprise-debug.jsonl
165
+ },
166
+ ],
167
+ ],
168
+ });
169
+ ```
170
+
171
+ Via environment variables:
172
+
173
+ ```bash
174
+ STANTERPRISE_DEBUG=true STANTERPRISE_DEBUG_FILE=debug-output.jsonl npx playwright test
175
+ ```
176
+
177
+ ### Debug File Format
178
+
179
+ Each line is a JSON object with three fields:
180
+
181
+ ```json
182
+ {"timestamp":"2026-03-23T04:10:00.000Z","path":"/testsystem.v1.observer.TestEventCollector/ReportRunStart","message":{"run_id":"abc-123","name":"My Run"}}
183
+ {"timestamp":"2026-03-23T04:10:01.000Z","path":"/testsystem.v1.observer.TestEventCollector/ReportTestBegin","message":{"test_case":{"id":"t1","name":"login test"}}}
184
+ ```
185
+
186
+ - `timestamp` — ISO 8601 timestamp when the message was sent
187
+ - `path` — gRPC method path
188
+ - `message` — full serialized request payload
189
+
190
+ The debug file is created/truncated at reporter initialization, so each test run starts with a clean file. Entries are only written for messages that are actually dispatched (i.e., when gRPC is enabled). Writes are performed asynchronously to minimize overhead.
191
+
192
+ ## Sharding Support
193
+
194
+ 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:
195
+
196
+ - `shard.current`: The current shard number (1-based)
197
+ - `shard.total`: The total number of shards
198
+
199
+ ### Configuring Sharding
200
+
201
+ In your `playwright.config.ts`:
202
+
203
+ ```typescript
204
+ import { defineConfig } from "@playwright/test";
205
+
206
+ export default defineConfig({
207
+ shard: { total: 5, current: 1 },
208
+ reporter: [["stanterprise-playwright-reporter"]],
209
+ });
210
+ ```
211
+
212
+ Or via CLI:
213
+
214
+ ```bash
215
+ npx playwright test --shard=1/5
216
+ npx playwright test --shard=2/5
217
+ # ... etc
218
+ ```
219
+
220
+ ### Aggregating Results Across Shards
221
+
222
+ **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.
223
+
224
+ ```bash
225
+ # Set the same run ID for all shards
226
+ export STANTERPRISE_RUN_ID="my-test-run-123"
227
+
228
+ # Run all shards with the same run ID
229
+ npx playwright test --shard=1/5
230
+ npx playwright test --shard=2/5
231
+ npx playwright test --shard=3/5
232
+ npx playwright test --shard=4/5
233
+ npx playwright test --shard=5/5
234
+ ```
235
+
236
+ In CI/CD environments, you can use a unique identifier from your CI system:
237
+
238
+ ```bash
239
+ # GitHub Actions example
240
+ STANTERPRISE_RUN_ID="${GITHUB_RUN_ID}" npx playwright test --shard=1/5
241
+
242
+ # GitLab CI example
243
+ STANTERPRISE_RUN_ID="${CI_PIPELINE_ID}" npx playwright test --shard=1/5
244
+
245
+ # Jenkins example
246
+ STANTERPRISE_RUN_ID="${BUILD_ID}" npx playwright test --shard=1/5
247
+ ```
248
+
249
+ 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.
250
+
251
+ The shard information is automatically included in the metadata sent to the server, allowing you to:
252
+
253
+ - Track which tests ran on which shard
254
+ - Aggregate results from multiple shards
255
+ - Debug shard-specific issues
256
+
257
+ When verbose logging is enabled, you'll see shard information in the console output:
258
+
259
+ ```
260
+ Stanterprise Reporter: Test run started with ID: 123e4567-e89b-12d3-a456-426614174000
261
+ Number of tests: 42
262
+ Run started at: 2026-01-25T10:30:00.000Z
263
+ Shard: 1 of 5
264
+ ```
77
265
 
78
266
  ## What Gets Reported
79
267
 
@@ -91,6 +279,7 @@ STANTERPRISE_GRPC_ADDRESS=myserver.com:50051 npx playwright test
91
279
  ### Attachments
92
280
 
93
281
  The reporter automatically processes and sends:
282
+
94
283
  - Screenshots
95
284
  - Videos
96
285
  - Trace files
@@ -133,6 +322,8 @@ const reporter = new StanterpriseReporter({
133
322
  grpcAddress: "localhost:50051",
134
323
  grpcEnabled: true,
135
324
  grpcTimeout: 1000,
325
+ grpcMaxMessageSize: 104857600, // 100MB
326
+ maxAttachmentSize: 10485760, // 10MB
136
327
  verbose: false,
137
328
  });
138
329
  ```
@@ -184,9 +375,62 @@ The reporter is organized into several modules:
184
375
  - **utils/statusMapper.ts**: Maps Playwright statuses to protobuf enums
185
376
  - **utils/attachmentProcessor.ts**: Processes test attachments
186
377
  - **utils/timeHelpers.ts**: Handles timestamp and duration conversions
378
+ - **utils/debugLogger.ts**: Writes outgoing gRPC messages to a JSONL file when debug mode is enabled
187
379
 
188
380
  ## Troubleshooting
189
381
 
382
+ ### gRPC Error 13 INTERNAL: failed to publish event
383
+
384
+ This error typically occurs due to one of two issues:
385
+
386
+ **1. Metadata Serialization (Most Common)**
387
+
388
+ If you see this error with the stack trace mentioning "metadata is not iterable" or serialization issues, it was a bug in v0.0.13 and earlier versions. **Solution: Upgrade to v0.0.14 or later.**
389
+
390
+ **2. Large Attachments**
391
+
392
+ This error can also occur when test event payloads exceed the gRPC message size limit. Common causes:
393
+
394
+ 1. **Large attachments**: Tests with large screenshots, videos, or trace files
395
+ 2. **Default size limits**: gRPC has a default 4MB message size limit
396
+
397
+ **Solutions:**
398
+
399
+ 1. **Increase message size limits** (recommended):
400
+
401
+ ```typescript
402
+ reporter: [
403
+ [
404
+ "stanterprise-playwright-reporter",
405
+ {
406
+ grpcMaxMessageSize: 104857600, // 100MB (default)
407
+ maxAttachmentSize: 10485760, // 10MB (default)
408
+ },
409
+ ],
410
+ ],
411
+ ```
412
+
413
+ 2. **Reduce attachment sizes**:
414
+ - Configure Playwright to save attachments to disk instead of embedding them
415
+ - The reporter automatically uses file paths when available instead of content
416
+
417
+ ```typescript
418
+ use: {
419
+ screenshot: "only-on-failure",
420
+ video: "retain-on-failure",
421
+ trace: "retain-on-failure",
422
+ },
423
+ ```
424
+
425
+ 3. **Monitor payload sizes**: Enable verbose logging to see actual payload sizes:
426
+ ```typescript
427
+ reporter: [
428
+ ["stanterprise-playwright-reporter", { verbose: true }],
429
+ ],
430
+ ```
431
+
432
+ The reporter will log warnings when attachments exceed the size limit and automatically skip their content while preserving metadata.
433
+
190
434
  ### Connection Issues
191
435
 
192
436
  If you see gRPC connection errors:
@@ -216,6 +460,7 @@ Contributions are welcome! Please feel free to submit a Pull Request.
216
460
  The repository uses GitHub Actions to automatically publish to NPM when a version tag is pushed:
217
461
 
218
462
  1. Update the version following [semantic versioning](https://semver.org/):
463
+
219
464
  ```bash
220
465
  npm version patch # for bug fixes (1.0.0 → 1.0.1)
221
466
  npm version minor # for new features (1.0.0 → 1.1.0)
@@ -223,6 +468,7 @@ The repository uses GitHub Actions to automatically publish to NPM when a versio
223
468
  ```
224
469
 
225
470
  2. Push the version tag to GitHub:
471
+
226
472
  ```bash
227
473
  git push --follow-tags
228
474
  ```
@@ -239,16 +485,19 @@ The repository uses GitHub Actions to automatically publish to NPM when a versio
239
485
  Alternatively, you can publish manually:
240
486
 
241
487
  1. Ensure all tests pass:
488
+
242
489
  ```bash
243
490
  npm test
244
491
  ```
245
492
 
246
493
  2. Update the version:
494
+
247
495
  ```bash
248
496
  npm version patch|minor|major
249
497
  ```
250
498
 
251
499
  3. Publish to NPM (prepublishOnly script will build automatically):
500
+
252
501
  ```bash
253
502
  npm publish
254
503
  ```
@@ -261,10 +510,12 @@ Alternatively, you can publish manually:
261
510
  ### What Gets Published
262
511
 
263
512
  The package includes:
513
+
264
514
  - `dist/` - Compiled JavaScript and TypeScript declarations
265
515
  - `README.md` - Documentation
266
516
 
267
517
  The following are excluded via `.npmignore`:
518
+
268
519
  - Source files (`src/`, `tests/`, `examples/`)
269
520
  - Development configuration files
270
521
  - Build artifacts and logs
@@ -276,4 +527,3 @@ ISC - See [LICENSE](LICENSE) file for details
276
527
  ## Support
277
528
 
278
529
  For issues and questions, please visit the [GitHub repository](https://github.com/stanterprise/stanterprise-playwright-reporter).
279
-
@@ -0,0 +1,16 @@
1
+ import { StanterpriseReporterOptions } from "../types";
2
+ import * as grpc from "@grpc/grpc-js";
3
+ import { MessageQueue } from "./messageQueue";
4
+ export default function getClient(options: StanterpriseReporterOptions): grpc.Client | null;
5
+ /**
6
+ * Helper: generic unary call using raw method path with retry support.
7
+ *
8
+ * When a `queue` is provided the call is enqueued and executed only after all
9
+ * previously enqueued calls have completed, guaranteeing sequential delivery to
10
+ * the server. Without a queue the call is executed immediately (legacy / test
11
+ * behaviour).
12
+ */
13
+ export declare function reportUnary(options: StanterpriseReporterOptions, grpcClient: grpc.Client, grpcPath: string, message: {
14
+ serialize?: (w?: any) => Uint8Array;
15
+ serializeBinary?: () => Uint8Array;
16
+ }, deadlineMs?: number, queue?: MessageQueue): Promise<Buffer>;
@@ -0,0 +1,172 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.default = getClient;
37
+ exports.reportUnary = reportUnary;
38
+ const grpc = __importStar(require("@grpc/grpc-js"));
39
+ const debugLogger_1 = require("../utils/debugLogger");
40
+ function getClient(options) {
41
+ try {
42
+ const maxMessageSize = options.grpcMaxMessageSize || 104857600; // 100MB default
43
+ const credentials = options.tls
44
+ ? grpc.credentials.createSsl()
45
+ : grpc.credentials.createInsecure();
46
+ return new grpc.Client(options.grpcAddress, credentials, {
47
+ "grpc.max_send_message_length": maxMessageSize,
48
+ "grpc.max_receive_message_length": maxMessageSize,
49
+ });
50
+ }
51
+ catch (e) {
52
+ console.error("Failed to create gRPC client", e);
53
+ }
54
+ return null;
55
+ }
56
+ /**
57
+ * Determines if a gRPC error code represents a transient failure worth retrying
58
+ */
59
+ function shouldAttemptRetryForCode(errorCode) {
60
+ const transientCodes = new Set([
61
+ grpc.status.UNAVAILABLE,
62
+ grpc.status.DEADLINE_EXCEEDED,
63
+ grpc.status.INTERNAL,
64
+ grpc.status.UNKNOWN,
65
+ ]);
66
+ return transientCodes.has(errorCode);
67
+ }
68
+ /**
69
+ * Calculates wait time using power-of-two exponential backoff
70
+ */
71
+ function calculateWaitDuration(baseDelayMs, attemptsSoFar) {
72
+ return baseDelayMs * Math.pow(2, attemptsSoFar);
73
+ }
74
+ /**
75
+ * Performs a single gRPC unary request attempt
76
+ */
77
+ function executeSingleAttempt(grpcClient, path, message, deadlineMs) {
78
+ const reqSerialize = (arg) => {
79
+ const m = arg;
80
+ const bytes = m?.serializeBinary
81
+ ? m.serializeBinary()
82
+ : m?.serialize
83
+ ? m.serialize()
84
+ : new Uint8Array(0);
85
+ return Buffer.from(bytes);
86
+ };
87
+ const resDeserialize = (bytes) => bytes;
88
+ const metadata = new grpc.Metadata();
89
+ const callOptions = {
90
+ deadline: new Date(Date.now() + deadlineMs),
91
+ };
92
+ return new Promise((resolve, reject) => {
93
+ try {
94
+ grpcClient.makeUnaryRequest(path, reqSerialize, resDeserialize, message, metadata, callOptions, (err, response) => {
95
+ if (err)
96
+ return reject(err);
97
+ resolve(response);
98
+ });
99
+ }
100
+ catch (e) {
101
+ reject(e);
102
+ }
103
+ });
104
+ }
105
+ /**
106
+ * Pauses execution for specified milliseconds with unref'd timer to avoid blocking shutdown
107
+ */
108
+ function pauseExecution(durationMs) {
109
+ return new Promise((resolve) => {
110
+ const timer = setTimeout(resolve, durationMs);
111
+ // Unref the timer so it doesn't keep the process alive
112
+ timer.unref();
113
+ });
114
+ }
115
+ /**
116
+ * Attempts gRPC call with exponential backoff retry logic using iterative approach
117
+ */
118
+ async function attemptWithRetries(options, grpcClient, path, message, deadlineMs, maxRetries) {
119
+ let attemptNumber = 0;
120
+ let lastError = null;
121
+ // Try initial attempt + retries
122
+ for (let retriesRemaining = maxRetries; retriesRemaining >= 0; retriesRemaining--) {
123
+ try {
124
+ return await executeSingleAttempt(grpcClient, path, message, deadlineMs);
125
+ }
126
+ catch (err) {
127
+ lastError = err;
128
+ const grpcErr = err;
129
+ const statusCode = grpcErr?.code ?? grpc.status.UNKNOWN;
130
+ // Check if we should retry this error
131
+ const isRetryable = shouldAttemptRetryForCode(statusCode);
132
+ const hasRetriesLeft = retriesRemaining > 0;
133
+ if (!isRetryable || !hasRetriesLeft) {
134
+ throw err;
135
+ }
136
+ // Log retry attempt if verbose
137
+ if (options.verbose) {
138
+ console.log(`gRPC call failed (code: ${statusCode}), retrying... ` +
139
+ `(attempt ${attemptNumber + 1}, ${retriesRemaining} retries left)`);
140
+ }
141
+ // Calculate and wait for backoff duration
142
+ const baseDelay = options.grpcRetryDelay ?? 100;
143
+ const waitTime = calculateWaitDuration(baseDelay, attemptNumber);
144
+ await pauseExecution(waitTime);
145
+ attemptNumber++;
146
+ }
147
+ }
148
+ // This should never be reached due to the throw in the loop, but TypeScript needs it
149
+ throw lastError || new Error("Retry loop exhausted without result");
150
+ }
151
+ /**
152
+ * Helper: generic unary call using raw method path with retry support.
153
+ *
154
+ * When a `queue` is provided the call is enqueued and executed only after all
155
+ * previously enqueued calls have completed, guaranteeing sequential delivery to
156
+ * the server. Without a queue the call is executed immediately (legacy / test
157
+ * behaviour).
158
+ */
159
+ async function reportUnary(options, grpcClient, grpcPath, message, deadlineMs = 1000, queue) {
160
+ if (!options.grpcEnabled || !grpcClient) {
161
+ return Buffer.alloc(0);
162
+ }
163
+ const execute = async () => {
164
+ await (0, debugLogger_1.writeDebugEntry)(options, grpcPath, message);
165
+ const maxRetries = options.grpcMaxRetries ?? 3;
166
+ return attemptWithRetries(options, grpcClient, grpcPath, message, deadlineMs, maxRetries);
167
+ };
168
+ if (queue) {
169
+ return queue.enqueue(execute);
170
+ }
171
+ return execute();
172
+ }
@@ -0,0 +1,27 @@
1
+ /**
2
+ * A sequential async message queue that processes enqueued tasks one at a time,
3
+ * guaranteeing that each task completes before the next one begins.
4
+ *
5
+ * This ensures that gRPC messages are delivered to the server in the exact order
6
+ * they were enqueued, regardless of how quickly each individual call completes.
7
+ * It also serializes debug file writes as a side effect, since those are performed
8
+ * inside each task.
9
+ */
10
+ export declare class MessageQueue {
11
+ private tasks;
12
+ private processing;
13
+ private drainResolvers;
14
+ /**
15
+ * Adds a task to the queue and returns a Promise that resolves (or rejects)
16
+ * when the task has been executed. If the queue is idle the task starts
17
+ * immediately; otherwise it waits for all preceding tasks to finish first.
18
+ */
19
+ enqueue<T>(fn: () => Promise<T>): Promise<T>;
20
+ /**
21
+ * Returns a Promise that resolves once the queue has been fully drained
22
+ * (i.e. all currently enqueued tasks have finished). Resolves immediately
23
+ * if the queue is already empty.
24
+ */
25
+ drain(): Promise<void>;
26
+ private processNext;
27
+ }
@@ -0,0 +1,71 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.MessageQueue = void 0;
4
+ /**
5
+ * A sequential async message queue that processes enqueued tasks one at a time,
6
+ * guaranteeing that each task completes before the next one begins.
7
+ *
8
+ * This ensures that gRPC messages are delivered to the server in the exact order
9
+ * they were enqueued, regardless of how quickly each individual call completes.
10
+ * It also serializes debug file writes as a side effect, since those are performed
11
+ * inside each task.
12
+ */
13
+ class MessageQueue {
14
+ constructor() {
15
+ this.tasks = [];
16
+ this.processing = false;
17
+ this.drainResolvers = [];
18
+ }
19
+ /**
20
+ * Adds a task to the queue and returns a Promise that resolves (or rejects)
21
+ * when the task has been executed. If the queue is idle the task starts
22
+ * immediately; otherwise it waits for all preceding tasks to finish first.
23
+ */
24
+ enqueue(fn) {
25
+ return new Promise((resolve, reject) => {
26
+ this.tasks.push(async () => {
27
+ try {
28
+ const result = await fn();
29
+ resolve(result);
30
+ }
31
+ catch (e) {
32
+ reject(e);
33
+ }
34
+ });
35
+ if (!this.processing) {
36
+ this.processNext();
37
+ }
38
+ });
39
+ }
40
+ /**
41
+ * Returns a Promise that resolves once the queue has been fully drained
42
+ * (i.e. all currently enqueued tasks have finished). Resolves immediately
43
+ * if the queue is already empty.
44
+ */
45
+ drain() {
46
+ if (!this.processing && this.tasks.length === 0) {
47
+ return Promise.resolve();
48
+ }
49
+ return new Promise((resolve) => {
50
+ this.drainResolvers.push(resolve);
51
+ });
52
+ }
53
+ async processNext() {
54
+ this.processing = true;
55
+ while (this.tasks.length > 0) {
56
+ const task = this.tasks.shift();
57
+ try {
58
+ await task();
59
+ }
60
+ catch {
61
+ // Each task's promise already carries the error to its caller.
62
+ // Errors must not stop the queue from processing subsequent tasks.
63
+ }
64
+ }
65
+ this.processing = false;
66
+ const resolvers = this.drainResolvers;
67
+ this.drainResolvers = [];
68
+ resolvers.forEach((r) => r());
69
+ }
70
+ }
71
+ exports.MessageQueue = MessageQueue;
@@ -0,0 +1,9 @@
1
+ import { handleOnBeginEvent } from "./onBeginHandler";
2
+ import { handleOnEndEvent } from "./onEndHandler";
3
+ import { handleOnStepBeginEvent } from "./onStepBeginHandler";
4
+ import { handleOnStepEndEvent } from "./onStepEndHandler";
5
+ import { handleOnTestBeginEvent } from "./onTestBeginHandler";
6
+ import { handleOnTestEndEvent } from "./onTestEndHandler";
7
+ import { handleOnTestFailEvent } from "./onTestFailHandler";
8
+ import { handleOnErrorEvent } from "./onErrorHandler";
9
+ export { handleOnBeginEvent, handleOnEndEvent, handleOnStepBeginEvent, handleOnStepEndEvent, handleOnTestBeginEvent, handleOnTestEndEvent, handleOnTestFailEvent, handleOnErrorEvent, };