@stanterprise/playwright-reporter 1.0.0 → 1.1.0
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 +270 -12
- package/dist/client/grpcClient.d.ts +16 -0
- package/dist/client/grpcClient.js +180 -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 +9 -10
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,213 @@ 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_INGESTION_AUTH_KEY`: Optional shared Observer ingestion key. When set, the reporter sends `authorization: Bearer <key>` as gRPC metadata on every ingestion call. Leave unset for keyless local/AIO Observer.
|
|
69
|
+
- `STANTERPRISE_GRPC_TLS`: Enable TLS for gRPC (default: `true`). Use TLS when sending the key over a network.
|
|
70
|
+
- `STANTERPRISE_META_*`: Custom metadata fields for test runs (prefix will be stripped)
|
|
71
|
+
- `STANTERPRISE_DEBUG`: Enable debug mode — captures outgoing gRPC messages to a JSONL file (default: `false`; case-insensitive: `"true"`, `"TRUE"`, `"True"` all work)
|
|
72
|
+
- `STANTERPRISE_DEBUG_FILE`: File path for debug JSONL output (default: `stanterprise-debug.jsonl`)
|
|
73
|
+
|
|
74
|
+
Keep the ingestion key in CI's secret store, not in reporter configuration,
|
|
75
|
+
test metadata, or source control. The key is sent only in gRPC metadata and is
|
|
76
|
+
not written to debug payloads. A missing or incorrect key causes Observer to
|
|
77
|
+
reject ingestion; authentication failures are not retried and are reported
|
|
78
|
+
without printing the key. Local/AIO deployments do not use this variable.
|
|
79
|
+
|
|
80
|
+
#### Custom Metadata
|
|
81
|
+
|
|
82
|
+
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.
|
|
83
|
+
|
|
84
|
+
For example:
|
|
85
|
+
|
|
86
|
+
- `STANTERPRISE_META_BUILD_ID=12345` becomes `BUILD_ID: 12345` in metadata
|
|
87
|
+
- `STANTERPRISE_META_BRANCH=main` becomes `BRANCH: main` in metadata
|
|
88
|
+
- `STANTERPRISE_META_COMMIT_SHA=abc123` becomes `COMMIT_SHA: abc123` in metadata
|
|
62
89
|
|
|
63
90
|
Example:
|
|
64
91
|
|
|
65
92
|
```bash
|
|
66
|
-
STANTERPRISE_GRPC_ADDRESS=myserver.com:50051
|
|
93
|
+
STANTERPRISE_GRPC_ADDRESS=myserver.com:50051 \
|
|
94
|
+
STANTERPRISE_META_BUILD_ID=12345 \
|
|
95
|
+
STANTERPRISE_META_BRANCH=main \
|
|
96
|
+
npx playwright test
|
|
67
97
|
```
|
|
68
98
|
|
|
69
99
|
## Configuration Options
|
|
70
100
|
|
|
71
|
-
| Option
|
|
72
|
-
|
|
|
73
|
-
| `grpcAddress`
|
|
74
|
-
| `
|
|
75
|
-
| `
|
|
76
|
-
| `
|
|
101
|
+
| Option | Type | Default | Description |
|
|
102
|
+
| -------------------- | ------- | -------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
103
|
+
| `grpcAddress` | string | `localhost:50051` | Full gRPC server address. Takes precedence over `grpcHost` and `grpcPort`. |
|
|
104
|
+
| `grpcHost` | string | | gRPC server host. Used when `grpcAddress` is not provided. Combined with `grpcPort` to form the address. |
|
|
105
|
+
| `grpcPort` | number | `50051` | gRPC server port. Used when `grpcAddress` is not provided. Combined with `grpcHost` to form the address. |
|
|
106
|
+
| `grpcEnabled` | boolean | `true` | Enable/disable gRPC reporting |
|
|
107
|
+
| `grpcTimeout` | number | `1000` | Timeout for gRPC calls in milliseconds |
|
|
108
|
+
| `grpcMaxMessageSize` | number | `104857600` | Max message size in bytes (100MB default) |
|
|
109
|
+
| `maxAttachmentSize` | number | `10485760` | Max attachment content size in bytes (10MB default) |
|
|
110
|
+
| `verbose` | boolean | `false` | Enable verbose logging |
|
|
111
|
+
| `grpcMaxRetries` | number | `3` | Maximum number of retry attempts for failed gRPC calls |
|
|
112
|
+
| `grpcRetryDelay` | number | `100` | Initial delay for retries in ms (uses exponential backoff) |
|
|
113
|
+
| `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. |
|
|
114
|
+
| `debugFile` | string | `stanterprise-debug.jsonl` | File path for debug JSONL output. Parent directories are created automatically if they do not exist. |
|
|
115
|
+
|
|
116
|
+
### Retry Configuration
|
|
117
|
+
|
|
118
|
+
The reporter automatically retries failed gRPC calls for transient errors using exponential backoff:
|
|
119
|
+
|
|
120
|
+
- **Retryable errors**: `UNAVAILABLE`, `DEADLINE_EXCEEDED`, `INTERNAL`, `UNKNOWN`
|
|
121
|
+
- **Non-retryable errors**: `INVALID_ARGUMENT`, `NOT_FOUND`, `PERMISSION_DENIED`, `UNAUTHENTICATED`, `ALREADY_EXISTS`
|
|
122
|
+
|
|
123
|
+
**Exponential backoff formula**: `grpcRetryDelay * (2 ^ attemptNumber)`
|
|
124
|
+
|
|
125
|
+
Example with default settings (`grpcRetryDelay: 100`, `grpcMaxRetries: 3`):
|
|
126
|
+
|
|
127
|
+
- Initial attempt: Immediate
|
|
128
|
+
- Retry 1: Wait 100ms (100 \* 2^0)
|
|
129
|
+
- Retry 2: Wait 200ms (100 \* 2^1)
|
|
130
|
+
- Retry 3: Wait 400ms (100 \* 2^2)
|
|
131
|
+
|
|
132
|
+
```typescript
|
|
133
|
+
export default defineConfig({
|
|
134
|
+
reporter: [
|
|
135
|
+
[
|
|
136
|
+
"stanterprise-playwright-reporter",
|
|
137
|
+
{
|
|
138
|
+
grpcMaxRetries: 5, // Try up to 5 times
|
|
139
|
+
grpcRetryDelay: 200, // Start with 200ms delay
|
|
140
|
+
verbose: true, // Log retry attempts
|
|
141
|
+
},
|
|
142
|
+
],
|
|
143
|
+
],
|
|
144
|
+
});
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
To disable retries:
|
|
148
|
+
|
|
149
|
+
```typescript
|
|
150
|
+
{
|
|
151
|
+
grpcMaxRetries: 0; // No retries, fail fast
|
|
152
|
+
}
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
## Debug Mode
|
|
156
|
+
|
|
157
|
+
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.
|
|
158
|
+
|
|
159
|
+
> **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.
|
|
160
|
+
|
|
161
|
+
### Enabling Debug Mode
|
|
162
|
+
|
|
163
|
+
Via configuration:
|
|
164
|
+
|
|
165
|
+
```typescript
|
|
166
|
+
export default defineConfig({
|
|
167
|
+
reporter: [
|
|
168
|
+
[
|
|
169
|
+
"@stanterprise/playwright-reporter",
|
|
170
|
+
{
|
|
171
|
+
debug: true,
|
|
172
|
+
debugFile: "debug-output.jsonl", // optional, defaults to stanterprise-debug.jsonl
|
|
173
|
+
},
|
|
174
|
+
],
|
|
175
|
+
],
|
|
176
|
+
});
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
Via environment variables:
|
|
180
|
+
|
|
181
|
+
```bash
|
|
182
|
+
STANTERPRISE_DEBUG=true STANTERPRISE_DEBUG_FILE=debug-output.jsonl npx playwright test
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
### Debug File Format
|
|
186
|
+
|
|
187
|
+
Each line is a JSON object with three fields:
|
|
188
|
+
|
|
189
|
+
```json
|
|
190
|
+
{"timestamp":"2026-03-23T04:10:00.000Z","path":"/testsystem.v1.observer.TestEventCollector/ReportRunStart","message":{"run_id":"abc-123","name":"My Run"}}
|
|
191
|
+
{"timestamp":"2026-03-23T04:10:01.000Z","path":"/testsystem.v1.observer.TestEventCollector/ReportTestBegin","message":{"test_case":{"id":"t1","name":"login test"}}}
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
- `timestamp` — ISO 8601 timestamp when the message was sent
|
|
195
|
+
- `path` — gRPC method path
|
|
196
|
+
- `message` — full serialized request payload
|
|
197
|
+
|
|
198
|
+
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.
|
|
199
|
+
|
|
200
|
+
## Sharding Support
|
|
201
|
+
|
|
202
|
+
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:
|
|
203
|
+
|
|
204
|
+
- `shard.current`: The current shard number (1-based)
|
|
205
|
+
- `shard.total`: The total number of shards
|
|
206
|
+
|
|
207
|
+
### Configuring Sharding
|
|
208
|
+
|
|
209
|
+
In your `playwright.config.ts`:
|
|
210
|
+
|
|
211
|
+
```typescript
|
|
212
|
+
import { defineConfig } from "@playwright/test";
|
|
213
|
+
|
|
214
|
+
export default defineConfig({
|
|
215
|
+
shard: { total: 5, current: 1 },
|
|
216
|
+
reporter: [["stanterprise-playwright-reporter"]],
|
|
217
|
+
});
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
Or via CLI:
|
|
221
|
+
|
|
222
|
+
```bash
|
|
223
|
+
npx playwright test --shard=1/5
|
|
224
|
+
npx playwright test --shard=2/5
|
|
225
|
+
# ... etc
|
|
226
|
+
```
|
|
227
|
+
|
|
228
|
+
### Aggregating Results Across Shards
|
|
229
|
+
|
|
230
|
+
**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.
|
|
231
|
+
|
|
232
|
+
```bash
|
|
233
|
+
# Set the same run ID for all shards
|
|
234
|
+
export STANTERPRISE_RUN_ID="my-test-run-123"
|
|
235
|
+
|
|
236
|
+
# Run all shards with the same run ID
|
|
237
|
+
npx playwright test --shard=1/5
|
|
238
|
+
npx playwright test --shard=2/5
|
|
239
|
+
npx playwright test --shard=3/5
|
|
240
|
+
npx playwright test --shard=4/5
|
|
241
|
+
npx playwright test --shard=5/5
|
|
242
|
+
```
|
|
243
|
+
|
|
244
|
+
In CI/CD environments, you can use a unique identifier from your CI system:
|
|
245
|
+
|
|
246
|
+
```bash
|
|
247
|
+
# GitHub Actions example
|
|
248
|
+
STANTERPRISE_RUN_ID="${GITHUB_RUN_ID}" npx playwright test --shard=1/5
|
|
249
|
+
|
|
250
|
+
# GitLab CI example
|
|
251
|
+
STANTERPRISE_RUN_ID="${CI_PIPELINE_ID}" npx playwright test --shard=1/5
|
|
252
|
+
|
|
253
|
+
# Jenkins example
|
|
254
|
+
STANTERPRISE_RUN_ID="${BUILD_ID}" npx playwright test --shard=1/5
|
|
255
|
+
```
|
|
256
|
+
|
|
257
|
+
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.
|
|
258
|
+
|
|
259
|
+
The shard information is automatically included in the metadata sent to the server, allowing you to:
|
|
260
|
+
|
|
261
|
+
- Track which tests ran on which shard
|
|
262
|
+
- Aggregate results from multiple shards
|
|
263
|
+
- Debug shard-specific issues
|
|
264
|
+
|
|
265
|
+
When verbose logging is enabled, you'll see shard information in the console output:
|
|
266
|
+
|
|
267
|
+
```
|
|
268
|
+
Stanterprise Reporter: Test run started with ID: 123e4567-e89b-12d3-a456-426614174000
|
|
269
|
+
Number of tests: 42
|
|
270
|
+
Run started at: 2026-01-25T10:30:00.000Z
|
|
271
|
+
Shard: 1 of 5
|
|
272
|
+
```
|
|
77
273
|
|
|
78
274
|
## What Gets Reported
|
|
79
275
|
|
|
@@ -91,6 +287,7 @@ STANTERPRISE_GRPC_ADDRESS=myserver.com:50051 npx playwright test
|
|
|
91
287
|
### Attachments
|
|
92
288
|
|
|
93
289
|
The reporter automatically processes and sends:
|
|
290
|
+
|
|
94
291
|
- Screenshots
|
|
95
292
|
- Videos
|
|
96
293
|
- Trace files
|
|
@@ -133,6 +330,8 @@ const reporter = new StanterpriseReporter({
|
|
|
133
330
|
grpcAddress: "localhost:50051",
|
|
134
331
|
grpcEnabled: true,
|
|
135
332
|
grpcTimeout: 1000,
|
|
333
|
+
grpcMaxMessageSize: 104857600, // 100MB
|
|
334
|
+
maxAttachmentSize: 10485760, // 10MB
|
|
136
335
|
verbose: false,
|
|
137
336
|
});
|
|
138
337
|
```
|
|
@@ -184,9 +383,62 @@ The reporter is organized into several modules:
|
|
|
184
383
|
- **utils/statusMapper.ts**: Maps Playwright statuses to protobuf enums
|
|
185
384
|
- **utils/attachmentProcessor.ts**: Processes test attachments
|
|
186
385
|
- **utils/timeHelpers.ts**: Handles timestamp and duration conversions
|
|
386
|
+
- **utils/debugLogger.ts**: Writes outgoing gRPC messages to a JSONL file when debug mode is enabled
|
|
187
387
|
|
|
188
388
|
## Troubleshooting
|
|
189
389
|
|
|
390
|
+
### gRPC Error 13 INTERNAL: failed to publish event
|
|
391
|
+
|
|
392
|
+
This error typically occurs due to one of two issues:
|
|
393
|
+
|
|
394
|
+
**1. Metadata Serialization (Most Common)**
|
|
395
|
+
|
|
396
|
+
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.**
|
|
397
|
+
|
|
398
|
+
**2. Large Attachments**
|
|
399
|
+
|
|
400
|
+
This error can also occur when test event payloads exceed the gRPC message size limit. Common causes:
|
|
401
|
+
|
|
402
|
+
1. **Large attachments**: Tests with large screenshots, videos, or trace files
|
|
403
|
+
2. **Default size limits**: gRPC has a default 4MB message size limit
|
|
404
|
+
|
|
405
|
+
**Solutions:**
|
|
406
|
+
|
|
407
|
+
1. **Increase message size limits** (recommended):
|
|
408
|
+
|
|
409
|
+
```typescript
|
|
410
|
+
reporter: [
|
|
411
|
+
[
|
|
412
|
+
"stanterprise-playwright-reporter",
|
|
413
|
+
{
|
|
414
|
+
grpcMaxMessageSize: 104857600, // 100MB (default)
|
|
415
|
+
maxAttachmentSize: 10485760, // 10MB (default)
|
|
416
|
+
},
|
|
417
|
+
],
|
|
418
|
+
],
|
|
419
|
+
```
|
|
420
|
+
|
|
421
|
+
2. **Reduce attachment sizes**:
|
|
422
|
+
- Configure Playwright to save attachments to disk instead of embedding them
|
|
423
|
+
- The reporter automatically uses file paths when available instead of content
|
|
424
|
+
|
|
425
|
+
```typescript
|
|
426
|
+
use: {
|
|
427
|
+
screenshot: "only-on-failure",
|
|
428
|
+
video: "retain-on-failure",
|
|
429
|
+
trace: "retain-on-failure",
|
|
430
|
+
},
|
|
431
|
+
```
|
|
432
|
+
|
|
433
|
+
3. **Monitor payload sizes**: Enable verbose logging to see actual payload sizes:
|
|
434
|
+
```typescript
|
|
435
|
+
reporter: [
|
|
436
|
+
["stanterprise-playwright-reporter", { verbose: true }],
|
|
437
|
+
],
|
|
438
|
+
```
|
|
439
|
+
|
|
440
|
+
The reporter will log warnings when attachments exceed the size limit and automatically skip their content while preserving metadata.
|
|
441
|
+
|
|
190
442
|
### Connection Issues
|
|
191
443
|
|
|
192
444
|
If you see gRPC connection errors:
|
|
@@ -216,6 +468,7 @@ Contributions are welcome! Please feel free to submit a Pull Request.
|
|
|
216
468
|
The repository uses GitHub Actions to automatically publish to NPM when a version tag is pushed:
|
|
217
469
|
|
|
218
470
|
1. Update the version following [semantic versioning](https://semver.org/):
|
|
471
|
+
|
|
219
472
|
```bash
|
|
220
473
|
npm version patch # for bug fixes (1.0.0 → 1.0.1)
|
|
221
474
|
npm version minor # for new features (1.0.0 → 1.1.0)
|
|
@@ -223,6 +476,7 @@ The repository uses GitHub Actions to automatically publish to NPM when a versio
|
|
|
223
476
|
```
|
|
224
477
|
|
|
225
478
|
2. Push the version tag to GitHub:
|
|
479
|
+
|
|
226
480
|
```bash
|
|
227
481
|
git push --follow-tags
|
|
228
482
|
```
|
|
@@ -239,16 +493,19 @@ The repository uses GitHub Actions to automatically publish to NPM when a versio
|
|
|
239
493
|
Alternatively, you can publish manually:
|
|
240
494
|
|
|
241
495
|
1. Ensure all tests pass:
|
|
496
|
+
|
|
242
497
|
```bash
|
|
243
498
|
npm test
|
|
244
499
|
```
|
|
245
500
|
|
|
246
501
|
2. Update the version:
|
|
502
|
+
|
|
247
503
|
```bash
|
|
248
504
|
npm version patch|minor|major
|
|
249
505
|
```
|
|
250
506
|
|
|
251
507
|
3. Publish to NPM (prepublishOnly script will build automatically):
|
|
508
|
+
|
|
252
509
|
```bash
|
|
253
510
|
npm publish
|
|
254
511
|
```
|
|
@@ -261,10 +518,12 @@ Alternatively, you can publish manually:
|
|
|
261
518
|
### What Gets Published
|
|
262
519
|
|
|
263
520
|
The package includes:
|
|
521
|
+
|
|
264
522
|
- `dist/` - Compiled JavaScript and TypeScript declarations
|
|
265
523
|
- `README.md` - Documentation
|
|
266
524
|
|
|
267
525
|
The following are excluded via `.npmignore`:
|
|
526
|
+
|
|
268
527
|
- Source files (`src/`, `tests/`, `examples/`)
|
|
269
528
|
- Development configuration files
|
|
270
529
|
- Build artifacts and logs
|
|
@@ -275,5 +534,4 @@ ISC - See [LICENSE](LICENSE) file for details
|
|
|
275
534
|
|
|
276
535
|
## Support
|
|
277
536
|
|
|
278
|
-
For issues and questions, please visit the [GitHub repository](https://github.com/stanterprise/
|
|
279
|
-
|
|
537
|
+
For issues and questions, please visit the [GitHub repository](https://github.com/stanterprise/playwright-reporter).
|
|
@@ -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,180 @@
|
|
|
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 ingestionAuthKey = process.env.STANTERPRISE_INGESTION_AUTH_KEY;
|
|
90
|
+
if (ingestionAuthKey) {
|
|
91
|
+
metadata.set("authorization", `Bearer ${ingestionAuthKey}`);
|
|
92
|
+
}
|
|
93
|
+
const callOptions = {
|
|
94
|
+
deadline: new Date(Date.now() + deadlineMs),
|
|
95
|
+
};
|
|
96
|
+
return new Promise((resolve, reject) => {
|
|
97
|
+
try {
|
|
98
|
+
grpcClient.makeUnaryRequest(path, reqSerialize, resDeserialize, message, metadata, callOptions, (err, response) => {
|
|
99
|
+
if (err)
|
|
100
|
+
return reject(err);
|
|
101
|
+
resolve(response);
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
catch (e) {
|
|
105
|
+
reject(e);
|
|
106
|
+
}
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* Pauses execution for specified milliseconds with unref'd timer to avoid blocking shutdown
|
|
111
|
+
*/
|
|
112
|
+
function pauseExecution(durationMs) {
|
|
113
|
+
return new Promise((resolve) => {
|
|
114
|
+
const timer = setTimeout(resolve, durationMs);
|
|
115
|
+
// Unref the timer so it doesn't keep the process alive
|
|
116
|
+
timer.unref();
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* Attempts gRPC call with exponential backoff retry logic using iterative approach
|
|
121
|
+
*/
|
|
122
|
+
async function attemptWithRetries(options, grpcClient, path, message, deadlineMs, maxRetries) {
|
|
123
|
+
let attemptNumber = 0;
|
|
124
|
+
let lastError = null;
|
|
125
|
+
// Try initial attempt + retries
|
|
126
|
+
for (let retriesRemaining = maxRetries; retriesRemaining >= 0; retriesRemaining--) {
|
|
127
|
+
try {
|
|
128
|
+
return await executeSingleAttempt(grpcClient, path, message, deadlineMs);
|
|
129
|
+
}
|
|
130
|
+
catch (err) {
|
|
131
|
+
lastError = err;
|
|
132
|
+
const grpcErr = err;
|
|
133
|
+
const statusCode = grpcErr?.code ?? grpc.status.UNKNOWN;
|
|
134
|
+
// Check if we should retry this error
|
|
135
|
+
const isRetryable = shouldAttemptRetryForCode(statusCode);
|
|
136
|
+
const hasRetriesLeft = retriesRemaining > 0;
|
|
137
|
+
if (!isRetryable || !hasRetriesLeft) {
|
|
138
|
+
if (statusCode === grpc.status.UNAUTHENTICATED) {
|
|
139
|
+
throw new Error("Observer rejected ingestion authentication (UNAUTHENTICATED); " +
|
|
140
|
+
"verify STANTERPRISE_INGESTION_AUTH_KEY");
|
|
141
|
+
}
|
|
142
|
+
throw err;
|
|
143
|
+
}
|
|
144
|
+
// Log retry attempt if verbose
|
|
145
|
+
if (options.verbose) {
|
|
146
|
+
console.log(`gRPC call failed (code: ${statusCode}), retrying... ` +
|
|
147
|
+
`(attempt ${attemptNumber + 1}, ${retriesRemaining} retries left)`);
|
|
148
|
+
}
|
|
149
|
+
// Calculate and wait for backoff duration
|
|
150
|
+
const baseDelay = options.grpcRetryDelay ?? 100;
|
|
151
|
+
const waitTime = calculateWaitDuration(baseDelay, attemptNumber);
|
|
152
|
+
await pauseExecution(waitTime);
|
|
153
|
+
attemptNumber++;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
// This should never be reached due to the throw in the loop, but TypeScript needs it
|
|
157
|
+
throw lastError || new Error("Retry loop exhausted without result");
|
|
158
|
+
}
|
|
159
|
+
/**
|
|
160
|
+
* Helper: generic unary call using raw method path with retry support.
|
|
161
|
+
*
|
|
162
|
+
* When a `queue` is provided the call is enqueued and executed only after all
|
|
163
|
+
* previously enqueued calls have completed, guaranteeing sequential delivery to
|
|
164
|
+
* the server. Without a queue the call is executed immediately (legacy / test
|
|
165
|
+
* behaviour).
|
|
166
|
+
*/
|
|
167
|
+
async function reportUnary(options, grpcClient, grpcPath, message, deadlineMs = 1000, queue) {
|
|
168
|
+
if (!options.grpcEnabled || !grpcClient) {
|
|
169
|
+
return Buffer.alloc(0);
|
|
170
|
+
}
|
|
171
|
+
const execute = async () => {
|
|
172
|
+
await (0, debugLogger_1.writeDebugEntry)(options, grpcPath, message);
|
|
173
|
+
const maxRetries = options.grpcMaxRetries ?? 3;
|
|
174
|
+
return attemptWithRetries(options, grpcClient, grpcPath, message, deadlineMs, maxRetries);
|
|
175
|
+
};
|
|
176
|
+
if (queue) {
|
|
177
|
+
return queue.enqueue(execute);
|
|
178
|
+
}
|
|
179
|
+
return execute();
|
|
180
|
+
}
|
|
@@ -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
|
+
}
|