@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.
- package/README.md +261 -11
- package/dist/client/grpcClient.d.ts +16 -0
- package/dist/client/grpcClient.js +172 -0
- package/dist/client/messageQueue.d.ts +27 -0
- package/dist/client/messageQueue.js +71 -0
- package/dist/handlers/index.d.ts +9 -0
- package/dist/handlers/index.js +19 -0
- package/dist/handlers/onBeginHandler.d.ts +6 -0
- package/dist/handlers/onBeginHandler.js +98 -0
- package/dist/handlers/onEndHandler.d.ts +5 -0
- package/dist/handlers/onEndHandler.js +53 -0
- package/dist/handlers/onErrorHandler.d.ts +5 -0
- package/dist/handlers/onErrorHandler.js +16 -0
- package/dist/handlers/onStepBeginHandler.d.ts +5 -0
- package/dist/handlers/onStepBeginHandler.js +39 -0
- package/dist/handlers/onStepEndHandler.d.ts +5 -0
- package/dist/handlers/onStepEndHandler.js +62 -0
- package/dist/handlers/onTestBeginHandler.d.ts +5 -0
- package/dist/handlers/onTestBeginHandler.js +33 -0
- package/dist/handlers/onTestEndHandler.d.ts +5 -0
- package/dist/handlers/onTestEndHandler.js +58 -0
- package/dist/handlers/onTestFailHandler.d.ts +5 -0
- package/dist/handlers/onTestFailHandler.js +25 -0
- package/dist/reporter.d.ts +4 -15
- package/dist/reporter.js +97 -386
- package/dist/types.d.ts +56 -0
- package/dist/utils/attachmentProcessor.d.ts +6 -2
- package/dist/utils/attachmentProcessor.js +33 -4
- package/dist/utils/debugLogger.d.ts +13 -0
- package/dist/utils/debugLogger.js +87 -0
- package/dist/utils/envRetriever.d.ts +9 -0
- package/dist/utils/envRetriever.js +23 -0
- package/dist/utils/idGenerators.d.ts +20 -0
- package/dist/utils/idGenerators.js +40 -0
- package/dist/utils/index.d.ts +4 -0
- package/dist/utils/index.js +4 -0
- package/dist/utils/metadataHelpers.d.ts +42 -0
- package/dist/utils/metadataHelpers.js +91 -0
- package/dist/utils/optionsMapper.d.ts +7 -0
- package/dist/utils/optionsMapper.js +51 -0
- package/dist/utils/statusMapper.js +7 -6
- package/dist/utils/timeHelpers.d.ts +5 -5
- package/dist/utils/timeHelpers.js +24 -8
- 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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
72
|
-
|
|
|
73
|
-
| `grpcAddress`
|
|
74
|
-
| `
|
|
75
|
-
| `
|
|
76
|
-
| `
|
|
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, };
|