@pixeyedev/core 0.4.4 → 0.4.5
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/client.d.ts +17 -1
- package/dist/client.js +14 -2
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/uploadError.d.ts +35 -0
- package/dist/uploadError.js +83 -0
- package/dist/utils/parseShardInt.d.ts +23 -0
- package/dist/utils/parseShardInt.js +41 -0
- package/package.json +1 -1
package/dist/client.d.ts
CHANGED
|
@@ -3,7 +3,23 @@ export declare class PixeyeError extends Error {
|
|
|
3
3
|
status: number;
|
|
4
4
|
path: string;
|
|
5
5
|
body: string;
|
|
6
|
-
|
|
6
|
+
/**
|
|
7
|
+
* Where the failure came from. `'api'` = the Pixeye control-plane (a 4xx
|
|
8
|
+
* here can be a customer misconfig — token/plan/permissions). `'r2'` = a
|
|
9
|
+
* data-plane PUT to R2 via a presigned URL (a 403/404 here is a Pixeye-side
|
|
10
|
+
* fault — expired URL, bucket/credential, clock skew — NEVER the customer's).
|
|
11
|
+
* Used by `isUploadMisconfig` so an R2 outage can't fail a customer's CI.
|
|
12
|
+
*/
|
|
13
|
+
origin: 'api' | 'r2';
|
|
14
|
+
constructor(status: number, path: string, body: string,
|
|
15
|
+
/**
|
|
16
|
+
* Where the failure came from. `'api'` = the Pixeye control-plane (a 4xx
|
|
17
|
+
* here can be a customer misconfig — token/plan/permissions). `'r2'` = a
|
|
18
|
+
* data-plane PUT to R2 via a presigned URL (a 403/404 here is a Pixeye-side
|
|
19
|
+
* fault — expired URL, bucket/credential, clock skew — NEVER the customer's).
|
|
20
|
+
* Used by `isUploadMisconfig` so an R2 outage can't fail a customer's CI.
|
|
21
|
+
*/
|
|
22
|
+
origin?: 'api' | 'r2');
|
|
7
23
|
}
|
|
8
24
|
export interface PixeyeClientOptions {
|
|
9
25
|
serverUrl: string;
|
package/dist/client.js
CHANGED
|
@@ -4,11 +4,21 @@ export class PixeyeError extends Error {
|
|
|
4
4
|
status;
|
|
5
5
|
path;
|
|
6
6
|
body;
|
|
7
|
-
|
|
7
|
+
origin;
|
|
8
|
+
constructor(status, path, body,
|
|
9
|
+
/**
|
|
10
|
+
* Where the failure came from. `'api'` = the Pixeye control-plane (a 4xx
|
|
11
|
+
* here can be a customer misconfig — token/plan/permissions). `'r2'` = a
|
|
12
|
+
* data-plane PUT to R2 via a presigned URL (a 403/404 here is a Pixeye-side
|
|
13
|
+
* fault — expired URL, bucket/credential, clock skew — NEVER the customer's).
|
|
14
|
+
* Used by `isUploadMisconfig` so an R2 outage can't fail a customer's CI.
|
|
15
|
+
*/
|
|
16
|
+
origin = 'api') {
|
|
8
17
|
super(`Pixeye API ${path} failed (${status}): ${body}`);
|
|
9
18
|
this.status = status;
|
|
10
19
|
this.path = path;
|
|
11
20
|
this.body = body;
|
|
21
|
+
this.origin = origin;
|
|
12
22
|
this.name = 'PixeyeError';
|
|
13
23
|
}
|
|
14
24
|
}
|
|
@@ -67,7 +77,9 @@ export class PixeyeClient {
|
|
|
67
77
|
headers: { 'Content-Type': contentType }
|
|
68
78
|
});
|
|
69
79
|
if (!res.ok) {
|
|
70
|
-
|
|
80
|
+
// 'r2' origin — a PUT failure here is Pixeye-side (expired presigned
|
|
81
|
+
// URL, bucket/creds, clock skew), never the customer's misconfig.
|
|
82
|
+
throw new PixeyeError(res.status, maskedUrl, await res.text(), 'r2');
|
|
71
83
|
}
|
|
72
84
|
}, isRetryableHttpError, {
|
|
73
85
|
maxAttempts: 3,
|
package/dist/index.d.ts
CHANGED
|
@@ -10,3 +10,4 @@ export { runParallel } from './parallel.js';
|
|
|
10
10
|
export type { RetryOptions } from './retry.js';
|
|
11
11
|
export { isRetryableHttpError, maskPresignedUrl, withRetry } from './retry.js';
|
|
12
12
|
export { DEFAULT_SERVER_URL, DEFAULT_UPLOAD_CONCURRENCY } from './constants.js';
|
|
13
|
+
export { isUploadMisconfig, renderUploadFailureBanner, resolveFailOnError, uploadErrorSummary } from './uploadError.js';
|
package/dist/index.js
CHANGED
|
@@ -7,3 +7,4 @@ export { runParallel } from './parallel.js';
|
|
|
7
7
|
export { isRetryableHttpError, maskPresignedUrl, withRetry } from './retry.js';
|
|
8
8
|
// Constants
|
|
9
9
|
export { DEFAULT_SERVER_URL, DEFAULT_UPLOAD_CONCURRENCY } from './constants.js';
|
|
10
|
+
export { isUploadMisconfig, renderUploadFailureBanner, resolveFailOnError, uploadErrorSummary } from './uploadError.js';
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Classify an upload failure and render a loud banner — shared by the Playwright
|
|
3
|
+
* reporter and the CLI so a lapsed plan / bad token surfaces identically (P0-2).
|
|
4
|
+
*
|
|
5
|
+
* The rule: distinguish the CUSTOMER's misconfiguration (they must act) from a
|
|
6
|
+
* transient / our-side fault (their CI should not break over our hiccup).
|
|
7
|
+
*/
|
|
8
|
+
/**
|
|
9
|
+
* True when the failure is the customer's configuration/billing, not ours.
|
|
10
|
+
* A `PixeyeError` from the R2 data-plane (`origin: 'r2'`) is EXCLUDED — a
|
|
11
|
+
* 403/404 on a presigned PUT (expired URL, bucket/creds, clock skew) is a
|
|
12
|
+
* Pixeye-side fault and must never fail the customer's CI.
|
|
13
|
+
*/
|
|
14
|
+
export declare const isUploadMisconfig: (err: unknown) => boolean;
|
|
15
|
+
/**
|
|
16
|
+
* One-line `HTTP <status> — <detail>` summary. Prefers the error's `body`
|
|
17
|
+
* (the meaningful server code, e.g. `SUBSCRIPTION_CANCELLED`) over the full
|
|
18
|
+
* `PixeyeError.message`, which already embeds the status (avoids `HTTP 402 —
|
|
19
|
+
* Pixeye API … failed (402): …`).
|
|
20
|
+
*/
|
|
21
|
+
export declare const uploadErrorSummary: (err: unknown) => string;
|
|
22
|
+
/**
|
|
23
|
+
* Resolve the fail-on-error setting identically for the reporter and CLI: fail
|
|
24
|
+
* by default; opt out via EITHER an explicit `false` option OR
|
|
25
|
+
* `PIXEYE_FAIL_ON_ERROR=false`. (The CLI's commander always supplies a boolean
|
|
26
|
+
* option; the reporter's is optional — this keeps both consistent.)
|
|
27
|
+
*/
|
|
28
|
+
export declare const resolveFailOnError: (option: boolean | undefined, env: string | undefined) => boolean;
|
|
29
|
+
/**
|
|
30
|
+
* A bright, unmissable banner for a rejected upload. `willFail` toggles the
|
|
31
|
+
* closing line between "failing this step" and "opted out — passing anyway".
|
|
32
|
+
*/
|
|
33
|
+
export declare const renderUploadFailureBanner: (err: unknown, opts: {
|
|
34
|
+
willFail: boolean;
|
|
35
|
+
}) => string;
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Classify an upload failure and render a loud banner — shared by the Playwright
|
|
3
|
+
* reporter and the CLI so a lapsed plan / bad token surfaces identically (P0-2).
|
|
4
|
+
*
|
|
5
|
+
* The rule: distinguish the CUSTOMER's misconfiguration (they must act) from a
|
|
6
|
+
* transient / our-side fault (their CI should not break over our hiccup).
|
|
7
|
+
*/
|
|
8
|
+
/**
|
|
9
|
+
* HTTP statuses that mean the CUSTOMER must act — a bad/absent token (401),
|
|
10
|
+
* a lapsed subscription or exceeded quota (402), insufficient permissions
|
|
11
|
+
* (403), or a wrong project/endpoint (404). These fail CI by default so a run
|
|
12
|
+
* can't pass green while zero screenshots reached Pixeye. EVERYTHING else
|
|
13
|
+
* (429 rate-limit, 5xx, network, timeouts, no status) is transient / our fault
|
|
14
|
+
* → warn, never break their CI.
|
|
15
|
+
*/
|
|
16
|
+
const MISCONFIG_STATUSES = [401, 402, 403, 404];
|
|
17
|
+
const statusOf = (err) => {
|
|
18
|
+
const status = err?.status;
|
|
19
|
+
return typeof status === 'number' ? status : undefined;
|
|
20
|
+
};
|
|
21
|
+
/**
|
|
22
|
+
* True when the failure is the customer's configuration/billing, not ours.
|
|
23
|
+
* A `PixeyeError` from the R2 data-plane (`origin: 'r2'`) is EXCLUDED — a
|
|
24
|
+
* 403/404 on a presigned PUT (expired URL, bucket/creds, clock skew) is a
|
|
25
|
+
* Pixeye-side fault and must never fail the customer's CI.
|
|
26
|
+
*/
|
|
27
|
+
export const isUploadMisconfig = (err) => {
|
|
28
|
+
if (err?.origin === 'r2') {
|
|
29
|
+
return false;
|
|
30
|
+
}
|
|
31
|
+
const status = statusOf(err);
|
|
32
|
+
return status !== undefined && MISCONFIG_STATUSES.includes(status);
|
|
33
|
+
};
|
|
34
|
+
/**
|
|
35
|
+
* One-line `HTTP <status> — <detail>` summary. Prefers the error's `body`
|
|
36
|
+
* (the meaningful server code, e.g. `SUBSCRIPTION_CANCELLED`) over the full
|
|
37
|
+
* `PixeyeError.message`, which already embeds the status (avoids `HTTP 402 —
|
|
38
|
+
* Pixeye API … failed (402): …`).
|
|
39
|
+
*/
|
|
40
|
+
export const uploadErrorSummary = (err) => {
|
|
41
|
+
const status = statusOf(err);
|
|
42
|
+
const body = err?.body;
|
|
43
|
+
const detail = typeof body === 'string' && body.length > 0
|
|
44
|
+
? body
|
|
45
|
+
: err instanceof Error
|
|
46
|
+
? err.message
|
|
47
|
+
: String(err);
|
|
48
|
+
return status !== undefined ? `HTTP ${status} — ${detail}` : detail;
|
|
49
|
+
};
|
|
50
|
+
/**
|
|
51
|
+
* Resolve the fail-on-error setting identically for the reporter and CLI: fail
|
|
52
|
+
* by default; opt out via EITHER an explicit `false` option OR
|
|
53
|
+
* `PIXEYE_FAIL_ON_ERROR=false`. (The CLI's commander always supplies a boolean
|
|
54
|
+
* option; the reporter's is optional — this keeps both consistent.)
|
|
55
|
+
*/
|
|
56
|
+
export const resolveFailOnError = (option, env) => option !== false && env !== 'false';
|
|
57
|
+
/**
|
|
58
|
+
* A bright, unmissable banner for a rejected upload. `willFail` toggles the
|
|
59
|
+
* closing line between "failing this step" and "opted out — passing anyway".
|
|
60
|
+
*/
|
|
61
|
+
export const renderUploadFailureBanner = (err, opts) => {
|
|
62
|
+
const line = '━'.repeat(64);
|
|
63
|
+
const footer = opts.willFail
|
|
64
|
+
? [
|
|
65
|
+
' Failing this step so it cannot pass silently. Fix the above, or pass',
|
|
66
|
+
' failOnError: false (reporter) / --no-fail-on-error (CLI) to opt out.'
|
|
67
|
+
]
|
|
68
|
+
: [' failOnError is OFF — this step passes despite the lost coverage.'];
|
|
69
|
+
return [
|
|
70
|
+
'',
|
|
71
|
+
`\x1b[31m\x1b[1m${line}`,
|
|
72
|
+
` ✗ PIXEYE UPLOAD REJECTED — ${uploadErrorSummary(err)}`,
|
|
73
|
+
'',
|
|
74
|
+
' Your screenshots did NOT reach Pixeye. Visual coverage for this run is',
|
|
75
|
+
' LOST. This is a configuration or billing problem on your account (API',
|
|
76
|
+
' token, subscription/quota, or project permissions) — not a transient',
|
|
77
|
+
' Pixeye outage.',
|
|
78
|
+
'',
|
|
79
|
+
...footer,
|
|
80
|
+
`${line}\x1b[0m`,
|
|
81
|
+
''
|
|
82
|
+
].join('\n');
|
|
83
|
+
};
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* F-095: parse shard-related env vars (PIXEYE_SHARD_INDEX,
|
|
3
|
+
* PIXEYE_SHARD_TOTAL, CYPRESS_SPLIT_INDEX, CURRENTS_PARALLEL_INDEX, …)
|
|
4
|
+
* with a NaN guard.
|
|
5
|
+
*
|
|
6
|
+
* Bare `parseInt('abc', 10)` returns NaN; `JSON.stringify({ index: NaN })`
|
|
7
|
+
* coerces to `null` and the API rejects with a confusing
|
|
8
|
+
* "Validation failed: index must be a number" error 30s deep into a CI
|
|
9
|
+
* pipeline. Failing fast at parse time with a clear message identifying
|
|
10
|
+
* the offending env var lets users fix `PIXEYE_SHARD_INDEX=` typos
|
|
11
|
+
* without running the test suite.
|
|
12
|
+
*
|
|
13
|
+
* The helper is intentionally permissive about the shape: callers pass
|
|
14
|
+
* the env var name so the error message is self-explanatory. Returns
|
|
15
|
+
* `undefined` when the value is empty/missing (so callers can default).
|
|
16
|
+
*/
|
|
17
|
+
export declare const parseShardInt: (value: string | undefined | null, name: string) => number;
|
|
18
|
+
/**
|
|
19
|
+
* Same as `parseShardInt` but returns `undefined` when the value is
|
|
20
|
+
* missing — useful when the env var is optional and the caller will
|
|
21
|
+
* fall back to a default. Still throws on a non-numeric explicit value.
|
|
22
|
+
*/
|
|
23
|
+
export declare const parseShardIntOptional: (value: string | undefined | null, name: string) => number | undefined;
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* F-095: parse shard-related env vars (PIXEYE_SHARD_INDEX,
|
|
3
|
+
* PIXEYE_SHARD_TOTAL, CYPRESS_SPLIT_INDEX, CURRENTS_PARALLEL_INDEX, …)
|
|
4
|
+
* with a NaN guard.
|
|
5
|
+
*
|
|
6
|
+
* Bare `parseInt('abc', 10)` returns NaN; `JSON.stringify({ index: NaN })`
|
|
7
|
+
* coerces to `null` and the API rejects with a confusing
|
|
8
|
+
* "Validation failed: index must be a number" error 30s deep into a CI
|
|
9
|
+
* pipeline. Failing fast at parse time with a clear message identifying
|
|
10
|
+
* the offending env var lets users fix `PIXEYE_SHARD_INDEX=` typos
|
|
11
|
+
* without running the test suite.
|
|
12
|
+
*
|
|
13
|
+
* The helper is intentionally permissive about the shape: callers pass
|
|
14
|
+
* the env var name so the error message is self-explanatory. Returns
|
|
15
|
+
* `undefined` when the value is empty/missing (so callers can default).
|
|
16
|
+
*/
|
|
17
|
+
export const parseShardInt = (value, name) => {
|
|
18
|
+
if (value === undefined || value === null || value === '') {
|
|
19
|
+
throw new Error(`${name} must be set`);
|
|
20
|
+
}
|
|
21
|
+
const parsed = parseInt(value, 10);
|
|
22
|
+
if (Number.isNaN(parsed)) {
|
|
23
|
+
throw new Error(`${name} must be an integer (got ${JSON.stringify(value)})`);
|
|
24
|
+
}
|
|
25
|
+
return parsed;
|
|
26
|
+
};
|
|
27
|
+
/**
|
|
28
|
+
* Same as `parseShardInt` but returns `undefined` when the value is
|
|
29
|
+
* missing — useful when the env var is optional and the caller will
|
|
30
|
+
* fall back to a default. Still throws on a non-numeric explicit value.
|
|
31
|
+
*/
|
|
32
|
+
export const parseShardIntOptional = (value, name) => {
|
|
33
|
+
if (value === undefined || value === null || value === '') {
|
|
34
|
+
return undefined;
|
|
35
|
+
}
|
|
36
|
+
const parsed = parseInt(value, 10);
|
|
37
|
+
if (Number.isNaN(parsed)) {
|
|
38
|
+
throw new Error(`${name} must be an integer (got ${JSON.stringify(value)})`);
|
|
39
|
+
}
|
|
40
|
+
return parsed;
|
|
41
|
+
};
|