squally-reporter 0.4.0 → 0.5.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 +54 -83
- package/dist/index.d.ts +95 -6
- package/dist/index.js +828 -206
- package/package.json +3 -2
package/README.md
CHANGED
|
@@ -39,11 +39,13 @@ export default defineConfig({
|
|
|
39
39
|
});
|
|
40
40
|
```
|
|
41
41
|
|
|
42
|
-
Alternatively, pass config explicitly instead of using env vars
|
|
42
|
+
Alternatively, pass config explicitly instead of using env vars - **only if
|
|
43
|
+
`apiKey` itself comes from an env var or secret store at runtime, never as a
|
|
44
|
+
literal string in a committed file:**
|
|
43
45
|
|
|
44
46
|
```ts
|
|
45
47
|
reporter: [
|
|
46
|
-
["squally-reporter", { apiUrl: "https://squally-app.vercel.app", apiKey:
|
|
48
|
+
["squally-reporter", { apiUrl: "https://squally-app.vercel.app", apiKey: process.env.SQUALLY_API_KEY }],
|
|
47
49
|
],
|
|
48
50
|
```
|
|
49
51
|
|
|
@@ -92,92 +94,61 @@ this reporter - a failed or skipped upload never fails your test run or
|
|
|
92
94
|
changes its exit code, it only logs a `console.warn` explaining what
|
|
93
95
|
happened.
|
|
94
96
|
|
|
95
|
-
##
|
|
97
|
+
## How results reach Squally
|
|
96
98
|
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
99
|
+
Results stream to Squally as your suite runs, not all at once at the end:
|
|
100
|
+
each test's result is sent in a small batch (at most every 25 results or 2
|
|
101
|
+
seconds, whichever comes first), so the "running" view in the dashboard stays
|
|
102
|
+
close to real time even for a long suite. There is no longer a single-request
|
|
103
|
+
size limit that could discard a whole run - a very large suite simply takes
|
|
104
|
+
more, smaller requests. As a courtesy the reporter still warns if a run's
|
|
105
|
+
result count climbs well past 5,000, since that usually means retries are
|
|
106
|
+
configured higher than intended rather than a suite that's actually that
|
|
107
|
+
large.
|
|
102
108
|
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
If you run into this, the practical way around it today is to let separate
|
|
108
|
-
parts of the suite report separately - for example as separate Playwright
|
|
109
|
-
projects or separate CI jobs, each producing its own run. Splitting one report
|
|
110
|
-
across several requests is not supported yet.
|
|
111
|
-
|
|
112
|
-
There is also a hard **~4.5 MB limit on the request itself**, coming from the
|
|
113
|
-
hosting platform rather than from Squally. Very large error messages, stack
|
|
114
|
-
traces or console output can hit it before the 5,000 mark; the reporter already
|
|
115
|
-
truncates each field to what the server accepts, which keeps normal runs well
|
|
116
|
-
clear of it.
|
|
109
|
+
If Squally is unreachable partway through, results already sent stay in the
|
|
110
|
+
dashboard; only what could not be delivered is lost, and your test run's exit
|
|
111
|
+
code is never affected either way.
|
|
117
112
|
|
|
118
113
|
## Sharding
|
|
119
114
|
|
|
120
|
-
**
|
|
121
|
-
separate process with its own reporter instance,
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
the
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
# Required - see the note below.
|
|
158
|
-
- uses: actions/checkout@v4
|
|
159
|
-
- uses: actions/download-artifact@v4
|
|
160
|
-
with:
|
|
161
|
-
pattern: blob-*
|
|
162
|
-
merge-multiple: true
|
|
163
|
-
path: all-blobs
|
|
164
|
-
- run: npx playwright merge-reports --reporter=squally-reporter ./all-blobs
|
|
165
|
-
env:
|
|
166
|
-
SQUALLY_API_URL: ${{ secrets.SQUALLY_API_URL }}
|
|
167
|
-
SQUALLY_API_KEY: ${{ secrets.SQUALLY_API_KEY }}
|
|
168
|
-
```
|
|
169
|
-
|
|
170
|
-
**The merge job needs the repository checked out.** Branch, commit, commit
|
|
171
|
-
message and author are read on whichever machine the reporter runs on - during
|
|
172
|
-
a merge that is the merge job, not the shards. Without a checkout (and without
|
|
173
|
-
the CI provider's own commit variables) they all come out empty, and a run
|
|
174
|
-
without a branch is never counted as a run on your stable branch, which is what
|
|
175
|
-
Squally's broken/healthy verdicts are based on. `actions/checkout` in the merge
|
|
176
|
-
job is enough.
|
|
177
|
-
|
|
178
|
-
The run's start time is taken from the earliest test, not from when the
|
|
179
|
-
reporter started, so a merge that happens minutes after the tests still dates
|
|
180
|
-
the run correctly.
|
|
115
|
+
**Handled automatically - no extra setup needed.** Each shard is still a
|
|
116
|
+
separate process with its own reporter instance, but Squally now correlates
|
|
117
|
+
them itself: all shards of one CI run are grouped together and shown as one
|
|
118
|
+
run once every shard has finished, with the flip side of that guarantee being
|
|
119
|
+
that the run only completes once every shard *has* reported.
|
|
120
|
+
|
|
121
|
+
Correlation is based on an id read from your CI provider's own environment,
|
|
122
|
+
so it costs you nothing on GitHub Actions, GitLab CI or CircleCI. If your
|
|
123
|
+
provider is not one of those (or you shard locally across several separate
|
|
124
|
+
`--shard` invocations), set `SQUALLY_CI_RUN_ID` to the same value for every
|
|
125
|
+
shard of one run - anything unique per CI run and shared by its shards works,
|
|
126
|
+
e.g. a build number your CI system already exposes.
|
|
127
|
+
|
|
128
|
+
**`playwright merge-reports` is not part of this and is not recommended** for
|
|
129
|
+
getting a single run in Squally - the automatic correlation above is the
|
|
130
|
+
supported path, and produces correct per-shard timing/branch/commit
|
|
131
|
+
information that a merge job cannot (see the historical note below). Nothing
|
|
132
|
+
stops you from using `merge-reports` for Playwright's own HTML report if you
|
|
133
|
+
want that separately; just don't point this reporter at the merged result
|
|
134
|
+
expecting Squally to treat it specially.
|
|
135
|
+
|
|
136
|
+
<details>
|
|
137
|
+
<summary>Why not just use `merge-reports`, historically</summary>
|
|
138
|
+
|
|
139
|
+
An earlier version of this reporter recommended `playwright merge-reports` as
|
|
140
|
+
the way to get one Squally run out of a sharded job. That path has a real
|
|
141
|
+
problem: branch, commit, commit message and author are all read on whichever
|
|
142
|
+
machine the reporter actually runs on - during a merge that is the merge job,
|
|
143
|
+
not the shards - and without a checkout there (and without the CI provider's
|
|
144
|
+
own commit variables) they all come out empty. A run with no branch is never
|
|
145
|
+
counted as a run on your stable branch, which is what Squally's broken/healthy
|
|
146
|
+
verdicts are based on - so a "merged" run could exist in the dashboard while
|
|
147
|
+
silently contributing nothing to flakiness detection. The automatic
|
|
148
|
+
correlation above reads that information from each shard's own environment
|
|
149
|
+
instead, so this problem does not arise.
|
|
150
|
+
|
|
151
|
+
</details>
|
|
181
152
|
|
|
182
153
|
## Tested revision (optional)
|
|
183
154
|
|
package/dist/index.d.ts
CHANGED
|
@@ -1,22 +1,111 @@
|
|
|
1
|
-
import type { FullConfig, FullResult, Reporter, Suite, TestCase, TestResult } from "@playwright/test/reporter";
|
|
1
|
+
import type { FullConfig, FullResult, Reporter, Suite, TestCase, TestError, TestResult } from "@playwright/test/reporter";
|
|
2
2
|
interface SquallyReporterOptions {
|
|
3
3
|
apiUrl?: string;
|
|
4
4
|
apiKey?: string;
|
|
5
5
|
}
|
|
6
|
+
interface AppendErrorEntry {
|
|
7
|
+
message: string;
|
|
8
|
+
stack?: string;
|
|
9
|
+
snippet?: string;
|
|
10
|
+
location?: {
|
|
11
|
+
file: string;
|
|
12
|
+
line: number;
|
|
13
|
+
column: number;
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
interface AppendResultEntry {
|
|
17
|
+
testName: string;
|
|
18
|
+
filePath: string | null;
|
|
19
|
+
status: "passed" | "failed" | "skipped";
|
|
20
|
+
retryCount: number;
|
|
21
|
+
repeatIndex: number;
|
|
22
|
+
durationMs: number | null;
|
|
23
|
+
errors?: AppendErrorEntry[];
|
|
24
|
+
consoleOutput: string | null;
|
|
25
|
+
tags?: string[];
|
|
26
|
+
browserName: string | null;
|
|
27
|
+
errorContext: string | null;
|
|
28
|
+
}
|
|
29
|
+
type ArtifactType = "screenshot" | "trace";
|
|
30
|
+
declare function truncate(value: string, max: number): string;
|
|
31
|
+
declare function truncateOrNull(value: string | null, max: number): string | null;
|
|
32
|
+
declare function mapStatus(status: TestResult["status"]): AppendResultEntry["status"];
|
|
33
|
+
declare function detectCiRunId(env: NodeJS.ProcessEnv, generateFallback: () => string): string;
|
|
34
|
+
interface ProjectSuiteLike {
|
|
35
|
+
project(): {
|
|
36
|
+
name: string;
|
|
37
|
+
} | undefined;
|
|
38
|
+
}
|
|
39
|
+
declare function resolveProjectName(projectSuites: readonly ProjectSuiteLike[]): string | null;
|
|
40
|
+
declare function normalizeRevision(raw: string): string;
|
|
41
|
+
interface ResolvedRevision {
|
|
42
|
+
revision: string;
|
|
43
|
+
source: "ci_native" | "passed_param";
|
|
44
|
+
}
|
|
45
|
+
declare function resolveTestedRevision(): ResolvedRevision | null;
|
|
46
|
+
declare function toPosixPath(p: string): string;
|
|
47
|
+
declare function stripAnsi(value: string): string;
|
|
48
|
+
declare function maskSecrets(value: string): string;
|
|
49
|
+
declare function prepareConsoleOutput(value: string | null): string | null;
|
|
50
|
+
declare function mapErrors(errors: readonly TestError[]): AppendErrorEntry[] | undefined;
|
|
51
|
+
declare function attemptQualifiesForArtifacts(mappedStatus: AppendResultEntry["status"], retryCount: number): boolean;
|
|
52
|
+
declare function artifactKey(testName: string, filePath: string, retryCount: number, type: ArtifactType): string;
|
|
53
|
+
declare function parseRetryAfterMs(headerValue: string | null, nowMs: number): number | null;
|
|
54
|
+
declare function computeBackoffMs(retryNumber: number): number;
|
|
6
55
|
export default class SquallyReporter implements Reporter {
|
|
7
56
|
private apiUrl;
|
|
8
57
|
private apiKey;
|
|
9
58
|
private enabled;
|
|
10
59
|
private startedAt;
|
|
11
|
-
private
|
|
60
|
+
private dispatcher;
|
|
61
|
+
private ciRunId;
|
|
62
|
+
private shardIndex;
|
|
63
|
+
private shardTotal;
|
|
64
|
+
private projectName;
|
|
65
|
+
private runTags;
|
|
66
|
+
private runConfig;
|
|
67
|
+
private buffer;
|
|
12
68
|
private artifacts;
|
|
13
|
-
private
|
|
69
|
+
private sendingDisabled;
|
|
70
|
+
private flushTimerHandle;
|
|
71
|
+
private inFlightFlushes;
|
|
72
|
+
private runIdPromise;
|
|
73
|
+
private runId;
|
|
14
74
|
constructor(options?: SquallyReporterOptions);
|
|
15
|
-
onBegin(config: FullConfig,
|
|
75
|
+
onBegin(config: FullConfig, suite: Suite): void;
|
|
76
|
+
private resolveRunConfig;
|
|
16
77
|
onTestEnd(test: TestCase, result: TestResult): void;
|
|
17
|
-
onEnd(result: FullResult)
|
|
18
|
-
private
|
|
78
|
+
onEnd: (result: FullResult) => Promise<void>;
|
|
79
|
+
private drainAndComplete;
|
|
80
|
+
private armFlushTimer;
|
|
81
|
+
private stopFlushTimer;
|
|
82
|
+
private giveUp;
|
|
83
|
+
private triggerFlush;
|
|
84
|
+
private flush;
|
|
85
|
+
private sendBatch;
|
|
86
|
+
private beginRun;
|
|
87
|
+
private completeRun;
|
|
88
|
+
private fetchRules;
|
|
89
|
+
private requestJson;
|
|
19
90
|
private uploadArtifacts;
|
|
20
91
|
private presignAndUpload;
|
|
21
92
|
}
|
|
93
|
+
export declare const __internal: {
|
|
94
|
+
truncate: typeof truncate;
|
|
95
|
+
truncateOrNull: typeof truncateOrNull;
|
|
96
|
+
mapStatus: typeof mapStatus;
|
|
97
|
+
stripAnsi: typeof stripAnsi;
|
|
98
|
+
normalizeRevision: typeof normalizeRevision;
|
|
99
|
+
resolveTestedRevision: typeof resolveTestedRevision;
|
|
100
|
+
toPosixPath: typeof toPosixPath;
|
|
101
|
+
detectCiRunId: typeof detectCiRunId;
|
|
102
|
+
resolveProjectName: typeof resolveProjectName;
|
|
103
|
+
maskSecrets: typeof maskSecrets;
|
|
104
|
+
prepareConsoleOutput: typeof prepareConsoleOutput;
|
|
105
|
+
mapErrors: typeof mapErrors;
|
|
106
|
+
parseRetryAfterMs: typeof parseRetryAfterMs;
|
|
107
|
+
computeBackoffMs: typeof computeBackoffMs;
|
|
108
|
+
attemptQualifiesForArtifacts: typeof attemptQualifiesForArtifacts;
|
|
109
|
+
artifactKey: typeof artifactKey;
|
|
110
|
+
};
|
|
22
111
|
export {};
|