@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.
Files changed (44) hide show
  1. package/README.md +270 -12
  2. package/dist/client/grpcClient.d.ts +16 -0
  3. package/dist/client/grpcClient.js +180 -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 +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-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,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 npx playwright test
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 | 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 |
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/stanterprise-playwright-reporter).
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
+ }