squally-reporter 0.5.2 → 0.6.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/dist/index.d.ts +40 -0
- package/dist/index.js +92 -2
- package/package.json +42 -42
package/dist/index.d.ts
CHANGED
|
@@ -29,6 +29,18 @@ interface AppendResultEntry {
|
|
|
29
29
|
type ArtifactType = "screenshot" | "trace";
|
|
30
30
|
declare function truncate(value: string, max: number): string;
|
|
31
31
|
declare function truncateOrNull(value: string | null, max: number): string | null;
|
|
32
|
+
/**
|
|
33
|
+
* This shard's flush interval: the base cadence stretched by how many shards
|
|
34
|
+
* share the rate budget with it.
|
|
35
|
+
*
|
|
36
|
+
* Only the TIMER is scaled. FLUSH_THRESHOLD_RESULTS is not, and that is
|
|
37
|
+
* deliberate: it is a latency bound on a fast suite, not a rate control, and
|
|
38
|
+
* stretching it would delay results without bounding the burst any better -
|
|
39
|
+
* the burst is bounded by how fast tests finish, which is not ours to set.
|
|
40
|
+
* The spec's acknowledged worst case (a very fast unit-style suite crossing 25
|
|
41
|
+
* faster than the interval) is therefore unchanged by this.
|
|
42
|
+
*/
|
|
43
|
+
declare function flushIntervalFor(shardTotal: number | null): number;
|
|
32
44
|
declare function mapStatus(status: TestResult["status"]): AppendResultEntry["status"];
|
|
33
45
|
declare function detectCiRunId(env: NodeJS.ProcessEnv, generateFallback: () => string): string;
|
|
34
46
|
interface ProjectSuiteLike {
|
|
@@ -66,6 +78,33 @@ export default class SquallyReporter implements Reporter {
|
|
|
66
78
|
private runConfig;
|
|
67
79
|
private buffer;
|
|
68
80
|
private artifacts;
|
|
81
|
+
/**
|
|
82
|
+
* Every result this shard produced, counted where it is produced (0.6.0).
|
|
83
|
+
*
|
|
84
|
+
* SIX PATHS LOSE RESULTS, and this counter exists because five of them are
|
|
85
|
+
* invisible afterwards and the sixth cannot be counted at all where it
|
|
86
|
+
* happens:
|
|
87
|
+
*
|
|
88
|
+
* 1. a batch dropped after its retries are spent (429, 5xx, network,
|
|
89
|
+
* unparseable response) - sendBatch's default branch
|
|
90
|
+
* 2. a batch dropped as malformed (400), which is not retried
|
|
91
|
+
* 3. the whole unsent buffer discarded by giveUp() on 402/404/409
|
|
92
|
+
* 4. every result produced AFTER a give-up - onTestEnd returns before the
|
|
93
|
+
* buffer, so there is nothing to count at the point of loss
|
|
94
|
+
* 5. batches still in flight when the drain budget expires at onEnd; the
|
|
95
|
+
* process then completes the run and exits out from under them
|
|
96
|
+
* 6. onBegin never getting a runId, so nothing was ever sendable
|
|
97
|
+
*
|
|
98
|
+
* A drop counter would have to be correct in all six places forever, and
|
|
99
|
+
* path 4 has no place to put it. Counting production instead needs one
|
|
100
|
+
* increment in one function, and it makes loss visible even on a path this
|
|
101
|
+
* reporter does not yet know about - the server compares this against what
|
|
102
|
+
* it actually stored.
|
|
103
|
+
*
|
|
104
|
+
* Per shard, for this shard's runId: each shard reports its own production
|
|
105
|
+
* against its own row, so no group arithmetic is needed on either side.
|
|
106
|
+
*/
|
|
107
|
+
private resultsProduced;
|
|
69
108
|
private sendingDisabled;
|
|
70
109
|
private flushTimerHandle;
|
|
71
110
|
private inFlightFlushes;
|
|
@@ -116,6 +155,7 @@ export declare const __internal: {
|
|
|
116
155
|
mapErrors: typeof mapErrors;
|
|
117
156
|
parseRetryAfterMs: typeof parseRetryAfterMs;
|
|
118
157
|
computeBackoffMs: typeof computeBackoffMs;
|
|
158
|
+
flushIntervalFor: typeof flushIntervalFor;
|
|
119
159
|
attemptQualifiesForArtifacts: typeof attemptQualifiesForArtifacts;
|
|
120
160
|
artifactKey: typeof artifactKey;
|
|
121
161
|
};
|
package/dist/index.js
CHANGED
|
@@ -138,6 +138,47 @@ const RULES_REQUEST_TIMEOUT_MS = 5000;
|
|
|
138
138
|
// API key): 60 s / 2 s = 30 requests/minute, half of 60.
|
|
139
139
|
const FLUSH_THRESHOLD_RESULTS = 25;
|
|
140
140
|
const FLUSH_INTERVAL_MS = 2000;
|
|
141
|
+
// Scaled by the shard count (0.6.0), because the budget the 2 s above was
|
|
142
|
+
// derived from is NOT per reporter - it is per API key, and every shard of a
|
|
143
|
+
// run carries the same key. The firewall counts `header:authorization`, so
|
|
144
|
+
// eight shards each flushing every 2 s produce 8 x 30 = 240 requests/minute
|
|
145
|
+
// against a 60/minute rule. The "half of 60" margin the constant above claims
|
|
146
|
+
// was computed for a single reporter and silently stopped holding the day
|
|
147
|
+
// sharding arrived.
|
|
148
|
+
//
|
|
149
|
+
// interval x shards keeps the AGGREGATE rate at 30/minute for any shard count:
|
|
150
|
+
// S shards x (60 s / 2S s) = 30. The live view does not get slower for it
|
|
151
|
+
// either - it shows the RUN, and a run made of S shards receives an update
|
|
152
|
+
// from one shard or another every 2 s on average regardless of S.
|
|
153
|
+
//
|
|
154
|
+
// CAPPED, and the cap is about crash exposure rather than rate. Everything
|
|
155
|
+
// buffered is lost if the runner is killed (CI cancellation, OOM), so the
|
|
156
|
+
// interval is also the size of the window at risk. 30 s is the same figure
|
|
157
|
+
// RATE_LIMIT_BACKOFF_CAP_MS uses for "the longest we ever wait", and it keeps
|
|
158
|
+
// the aggregate inside the budget up to 30 shards (S x 60/30 <= 60), which
|
|
159
|
+
// covers the 4-to-16 range the app's own shard documentation describes with
|
|
160
|
+
// room to spare.
|
|
161
|
+
//
|
|
162
|
+
// Above 30 shards the aggregate creeps over the rule again. That is accepted:
|
|
163
|
+
// it is outside the documented range, the retry path already handles a 429,
|
|
164
|
+
// and since 0.6.0 any batch actually lost to it is visible in the
|
|
165
|
+
// resultsProduced diff rather than silent.
|
|
166
|
+
const FLUSH_INTERVAL_CAP_MS = 30000;
|
|
167
|
+
/**
|
|
168
|
+
* This shard's flush interval: the base cadence stretched by how many shards
|
|
169
|
+
* share the rate budget with it.
|
|
170
|
+
*
|
|
171
|
+
* Only the TIMER is scaled. FLUSH_THRESHOLD_RESULTS is not, and that is
|
|
172
|
+
* deliberate: it is a latency bound on a fast suite, not a rate control, and
|
|
173
|
+
* stretching it would delay results without bounding the burst any better -
|
|
174
|
+
* the burst is bounded by how fast tests finish, which is not ours to set.
|
|
175
|
+
* The spec's acknowledged worst case (a very fast unit-style suite crossing 25
|
|
176
|
+
* faster than the interval) is therefore unchanged by this.
|
|
177
|
+
*/
|
|
178
|
+
function flushIntervalFor(shardTotal) {
|
|
179
|
+
const shards = Math.max(1, shardTotal ?? 1);
|
|
180
|
+
return Math.min(FLUSH_INTERVAL_CAP_MS, FLUSH_INTERVAL_MS * shards);
|
|
181
|
+
}
|
|
141
182
|
// --- Retry policy (spec section 3.2) ----------------------------------------
|
|
142
183
|
//
|
|
143
184
|
// Two independent policies, both applied by requestJson() below via separate
|
|
@@ -772,6 +813,33 @@ class SquallyReporter {
|
|
|
772
813
|
this.projectName = null;
|
|
773
814
|
this.buffer = [];
|
|
774
815
|
this.artifacts = [];
|
|
816
|
+
/**
|
|
817
|
+
* Every result this shard produced, counted where it is produced (0.6.0).
|
|
818
|
+
*
|
|
819
|
+
* SIX PATHS LOSE RESULTS, and this counter exists because five of them are
|
|
820
|
+
* invisible afterwards and the sixth cannot be counted at all where it
|
|
821
|
+
* happens:
|
|
822
|
+
*
|
|
823
|
+
* 1. a batch dropped after its retries are spent (429, 5xx, network,
|
|
824
|
+
* unparseable response) - sendBatch's default branch
|
|
825
|
+
* 2. a batch dropped as malformed (400), which is not retried
|
|
826
|
+
* 3. the whole unsent buffer discarded by giveUp() on 402/404/409
|
|
827
|
+
* 4. every result produced AFTER a give-up - onTestEnd returns before the
|
|
828
|
+
* buffer, so there is nothing to count at the point of loss
|
|
829
|
+
* 5. batches still in flight when the drain budget expires at onEnd; the
|
|
830
|
+
* process then completes the run and exits out from under them
|
|
831
|
+
* 6. onBegin never getting a runId, so nothing was ever sendable
|
|
832
|
+
*
|
|
833
|
+
* A drop counter would have to be correct in all six places forever, and
|
|
834
|
+
* path 4 has no place to put it. Counting production instead needs one
|
|
835
|
+
* increment in one function, and it makes loss visible even on a path this
|
|
836
|
+
* reporter does not yet know about - the server compares this against what
|
|
837
|
+
* it actually stored.
|
|
838
|
+
*
|
|
839
|
+
* Per shard, for this shard's runId: each shard reports its own production
|
|
840
|
+
* against its own row, so no group arithmetic is needed on either side.
|
|
841
|
+
*/
|
|
842
|
+
this.resultsProduced = 0;
|
|
775
843
|
// True once nothing more will ever be sent for this run - a permanent
|
|
776
844
|
// give-up (402/404/409 on append, or onBegin never getting a runId at
|
|
777
845
|
// all). onTestEnd stops collecting further results/artifacts once this is
|
|
@@ -945,7 +1013,21 @@ class SquallyReporter {
|
|
|
945
1013
|
};
|
|
946
1014
|
}
|
|
947
1015
|
onTestEnd(test, result) {
|
|
948
|
-
|
|
1016
|
+
// Two guards, deliberately split by the counter between them.
|
|
1017
|
+
//
|
|
1018
|
+
// `enabled` is false when there is nothing to report to at all (no URL or
|
|
1019
|
+
// key, or `--list`): no run exists, no /complete will be sent, and there
|
|
1020
|
+
// is nothing for a count to be compared against.
|
|
1021
|
+
//
|
|
1022
|
+
// `sendingDisabled` is different - a run DOES exist server-side, it will
|
|
1023
|
+
// be completed, and every result arriving after the give-up is lost. That
|
|
1024
|
+
// is loss path 4, and counting BEFORE this return is the only place it can
|
|
1025
|
+
// be seen. Moving the increment below the return would make the largest
|
|
1026
|
+
// loss path of the six silently report zero.
|
|
1027
|
+
if (!this.enabled)
|
|
1028
|
+
return;
|
|
1029
|
+
this.resultsProduced += 1;
|
|
1030
|
+
if (this.sendingDisabled)
|
|
949
1031
|
return;
|
|
950
1032
|
// Belt and braces: onTestBegin has already done this for every test
|
|
951
1033
|
// Playwright executes, but a result arriving without its begin would
|
|
@@ -1114,7 +1196,11 @@ class SquallyReporter {
|
|
|
1114
1196
|
const handle = setTimeout(() => {
|
|
1115
1197
|
this.triggerFlush();
|
|
1116
1198
|
this.armFlushTimer();
|
|
1117
|
-
|
|
1199
|
+
// Read per arming rather than cached at onBegin: shardTotal is known
|
|
1200
|
+
// before the first arming (config.shard.total, set in onBegin), so this
|
|
1201
|
+
// is the same value every time - reading it here keeps the timer honest
|
|
1202
|
+
// if that ever stops being true.
|
|
1203
|
+
}, flushIntervalFor(this.shardTotal));
|
|
1118
1204
|
handle.unref();
|
|
1119
1205
|
this.flushTimerHandle = handle;
|
|
1120
1206
|
}
|
|
@@ -1254,6 +1340,9 @@ class SquallyReporter {
|
|
|
1254
1340
|
shardIndex: this.shardIndex,
|
|
1255
1341
|
status,
|
|
1256
1342
|
finishedAt: new Date().toISOString(),
|
|
1343
|
+
// Read at the last possible moment: every loss path has already happened
|
|
1344
|
+
// by the time /complete is sent, so this number is final.
|
|
1345
|
+
resultsProduced: this.resultsProduced,
|
|
1257
1346
|
};
|
|
1258
1347
|
const outcome = await this.requestJson("POST", `${this.apiUrl}/api/ingest/v2/runs/${runId}/complete`, payload);
|
|
1259
1348
|
if (outcome.kind !== "ok") {
|
|
@@ -1515,6 +1604,7 @@ exports.__internal = {
|
|
|
1515
1604
|
mapErrors,
|
|
1516
1605
|
parseRetryAfterMs,
|
|
1517
1606
|
computeBackoffMs,
|
|
1607
|
+
flushIntervalFor,
|
|
1518
1608
|
attemptQualifiesForArtifacts,
|
|
1519
1609
|
artifactKey,
|
|
1520
1610
|
};
|
package/package.json
CHANGED
|
@@ -1,42 +1,42 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "squally-reporter",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "Playwright reporter that sends test results to your Squally dashboard.",
|
|
5
|
-
"author": "Squally <squally.dev@gmail.com>",
|
|
6
|
-
"keywords": [
|
|
7
|
-
"playwright",
|
|
8
|
-
"playwright-reporter",
|
|
9
|
-
"flaky-tests",
|
|
10
|
-
"test-results",
|
|
11
|
-
"ci"
|
|
12
|
-
],
|
|
13
|
-
"main": "dist/index.js",
|
|
14
|
-
"types": "dist/index.d.ts",
|
|
15
|
-
"files": [
|
|
16
|
-
"dist"
|
|
17
|
-
],
|
|
18
|
-
"engines": {
|
|
19
|
-
"node": ">=22.19.0"
|
|
20
|
-
},
|
|
21
|
-
"scripts": {
|
|
22
|
-
"build": "tsc",
|
|
23
|
-
"prepublishOnly": "npm run build",
|
|
24
|
-
"prepare": "npm run build",
|
|
25
|
-
"test:unit": "node --test \"test/unit/**/*.test.js\"",
|
|
26
|
-
"test:exit-code": "node test/exit-code/run.mjs",
|
|
27
|
-
"test:empty-runs": "node test/empty-runs/run.mjs",
|
|
28
|
-
"test": "npm run build && npm run test:unit && npm run test:exit-code && npm run test:empty-runs"
|
|
29
|
-
},
|
|
30
|
-
"peerDependencies": {
|
|
31
|
-
"@playwright/test": ">=1.30.0"
|
|
32
|
-
},
|
|
33
|
-
"devDependencies": {
|
|
34
|
-
"@playwright/test": "^1.48.0",
|
|
35
|
-
"@types/node": "^20.14.0",
|
|
36
|
-
"typescript": "^5.6.0"
|
|
37
|
-
},
|
|
38
|
-
"license": "MIT",
|
|
39
|
-
"dependencies": {
|
|
40
|
-
"undici": "^8.10.0"
|
|
41
|
-
}
|
|
42
|
-
}
|
|
1
|
+
{
|
|
2
|
+
"name": "squally-reporter",
|
|
3
|
+
"version": "0.6.0",
|
|
4
|
+
"description": "Playwright reporter that sends test results to your Squally dashboard.",
|
|
5
|
+
"author": "Squally <squally.dev@gmail.com>",
|
|
6
|
+
"keywords": [
|
|
7
|
+
"playwright",
|
|
8
|
+
"playwright-reporter",
|
|
9
|
+
"flaky-tests",
|
|
10
|
+
"test-results",
|
|
11
|
+
"ci"
|
|
12
|
+
],
|
|
13
|
+
"main": "dist/index.js",
|
|
14
|
+
"types": "dist/index.d.ts",
|
|
15
|
+
"files": [
|
|
16
|
+
"dist"
|
|
17
|
+
],
|
|
18
|
+
"engines": {
|
|
19
|
+
"node": ">=22.19.0"
|
|
20
|
+
},
|
|
21
|
+
"scripts": {
|
|
22
|
+
"build": "tsc",
|
|
23
|
+
"prepublishOnly": "npm run build",
|
|
24
|
+
"prepare": "npm run build",
|
|
25
|
+
"test:unit": "node --test \"test/unit/**/*.test.js\"",
|
|
26
|
+
"test:exit-code": "node test/exit-code/run.mjs",
|
|
27
|
+
"test:empty-runs": "node test/empty-runs/run.mjs",
|
|
28
|
+
"test": "npm run build && npm run test:unit && npm run test:exit-code && npm run test:empty-runs"
|
|
29
|
+
},
|
|
30
|
+
"peerDependencies": {
|
|
31
|
+
"@playwright/test": ">=1.30.0"
|
|
32
|
+
},
|
|
33
|
+
"devDependencies": {
|
|
34
|
+
"@playwright/test": "^1.48.0",
|
|
35
|
+
"@types/node": "^20.14.0",
|
|
36
|
+
"typescript": "^5.6.0"
|
|
37
|
+
},
|
|
38
|
+
"license": "MIT",
|
|
39
|
+
"dependencies": {
|
|
40
|
+
"undici": "^8.10.0"
|
|
41
|
+
}
|
|
42
|
+
}
|