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 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
- ## Limits per run
97
+ ## How results reach Squally
96
98
 
97
- Squally accepts at most **5,000 test results in a single run**. Beyond that the
98
- whole report is rejected, so the reporter checks before sending and skips the
99
- upload with a warning rather than letting the server discard it after the fact.
100
- Nothing else about your test run is affected - this only means that one run
101
- does not appear in the dashboard.
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
- **Retries count separately.** Each attempt is its own result, so a suite of
104
- 2,000 tests having a bad day reaches the ceiling sooner than the test count
105
- suggests.
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
- **Playwright's `--shard` is not merged automatically.** Each shard is a
121
- separate process with its own reporter instance, so each one reports its own
122
- run: a job split four ways shows up in Squally as four runs from the same
123
- commit, each holding only its own slice of the suite. The reporter says so in
124
- the log when it sees `--shard`.
125
-
126
- What that does and does not affect:
127
-
128
- - **Per-test flakiness stays correct.** A test lives in exactly one shard, so
129
- it still contributes one data point per CI run, and its history, flip rate
130
- and retry rate are unaffected.
131
- - **Run-level numbers are off.** The run count, "runs per day", and each run's
132
- own pass/fail (it only covers its slice) all reflect shards rather than CI
133
- runs.
134
-
135
- ### Reporting sharded runs as one run
136
-
137
- Use Playwright's own blob reports: have each shard write a blob, then merge
138
- them in one final job and let this reporter run on the merged result. That
139
- produces exactly one run with every test in it.
140
-
141
- ```yaml
142
- test:
143
- strategy:
144
- matrix:
145
- shard: [1, 2, 3, 4]
146
- steps:
147
- - uses: actions/checkout@v4
148
- - run: npx playwright test --shard=${{ matrix.shard }}/4 --reporter=blob
149
- - uses: actions/upload-artifact@v4
150
- with:
151
- name: blob-${{ matrix.shard }}
152
- path: blob-report
153
-
154
- merge:
155
- needs: test
156
- steps:
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 results;
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 earliestTestStart;
69
+ private sendingDisabled;
70
+ private flushTimerHandle;
71
+ private inFlightFlushes;
72
+ private runIdPromise;
73
+ private runId;
14
74
  constructor(options?: SquallyReporterOptions);
15
- onBegin(config: FullConfig, _suite: Suite): void;
75
+ onBegin(config: FullConfig, suite: Suite): void;
76
+ private resolveRunConfig;
16
77
  onTestEnd(test: TestCase, result: TestResult): void;
17
- onEnd(result: FullResult): Promise<void>;
18
- private runStartedAt;
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 {};