@browserstack/mcp-server 1.2.15-beta.1 → 1.2.15-beta.3
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/lib/percy-api/auth.d.ts +41 -0
- package/dist/lib/percy-api/auth.js +96 -0
- package/dist/lib/percy-api/build-items.d.ts +31 -0
- package/dist/lib/percy-api/build-items.js +61 -0
- package/dist/lib/percy-api/cache.d.ts +28 -0
- package/dist/lib/percy-api/cache.js +48 -0
- package/dist/lib/percy-api/client.d.ts +69 -0
- package/dist/lib/percy-api/client.js +275 -0
- package/dist/lib/percy-api/errors.d.ts +15 -0
- package/dist/lib/percy-api/errors.js +52 -0
- package/dist/lib/percy-api/formatter.d.ts +16 -0
- package/dist/lib/percy-api/formatter.js +344 -0
- package/dist/lib/percy-api/percy-auth.d.ts +43 -0
- package/dist/lib/percy-api/percy-auth.js +142 -0
- package/dist/lib/percy-api/percy-error-handler.d.ts +24 -0
- package/dist/lib/percy-api/percy-error-handler.js +302 -0
- package/dist/lib/percy-api/percy-session.d.ts +42 -0
- package/dist/lib/percy-api/percy-session.js +87 -0
- package/dist/lib/percy-api/polling.d.ts +26 -0
- package/dist/lib/percy-api/polling.js +42 -0
- package/dist/lib/percy-api/types.d.ts +56 -0
- package/dist/lib/percy-api/types.js +76 -0
- package/dist/server-factory.js +4 -0
- package/dist/tools/percy-mcp/advanced/branchline-operations.d.ts +16 -0
- package/dist/tools/percy-mcp/advanced/branchline-operations.js +81 -0
- package/dist/tools/percy-mcp/advanced/manage-variants.d.ts +16 -0
- package/dist/tools/percy-mcp/advanced/manage-variants.js +155 -0
- package/dist/tools/percy-mcp/advanced/manage-visual-monitoring.d.ts +16 -0
- package/dist/tools/percy-mcp/advanced/manage-visual-monitoring.js +171 -0
- package/dist/tools/percy-mcp/auth/auth-status.d.ts +3 -0
- package/dist/tools/percy-mcp/auth/auth-status.js +131 -0
- package/dist/tools/percy-mcp/core/approve-build.d.ts +14 -0
- package/dist/tools/percy-mcp/core/approve-build.js +97 -0
- package/dist/tools/percy-mcp/core/get-build-items.d.ts +13 -0
- package/dist/tools/percy-mcp/core/get-build-items.js +65 -0
- package/dist/tools/percy-mcp/core/get-build.d.ts +10 -0
- package/dist/tools/percy-mcp/core/get-build.js +16 -0
- package/dist/tools/percy-mcp/core/get-comparison.d.ts +11 -0
- package/dist/tools/percy-mcp/core/get-comparison.js +59 -0
- package/dist/tools/percy-mcp/core/get-snapshot.d.ts +10 -0
- package/dist/tools/percy-mcp/core/get-snapshot.js +40 -0
- package/dist/tools/percy-mcp/core/list-builds.d.ts +14 -0
- package/dist/tools/percy-mcp/core/list-builds.js +45 -0
- package/dist/tools/percy-mcp/core/list-projects.d.ts +12 -0
- package/dist/tools/percy-mcp/core/list-projects.js +51 -0
- package/dist/tools/percy-mcp/creation/create-app-snapshot.d.ts +12 -0
- package/dist/tools/percy-mcp/creation/create-app-snapshot.js +29 -0
- package/dist/tools/percy-mcp/creation/create-build.d.ts +19 -0
- package/dist/tools/percy-mcp/creation/create-build.js +68 -0
- package/dist/tools/percy-mcp/creation/create-comparison.d.ts +18 -0
- package/dist/tools/percy-mcp/creation/create-comparison.js +90 -0
- package/dist/tools/percy-mcp/creation/create-snapshot.d.ts +17 -0
- package/dist/tools/percy-mcp/creation/create-snapshot.js +99 -0
- package/dist/tools/percy-mcp/creation/finalize-build.d.ts +12 -0
- package/dist/tools/percy-mcp/creation/finalize-build.js +33 -0
- package/dist/tools/percy-mcp/creation/finalize-comparison.d.ts +10 -0
- package/dist/tools/percy-mcp/creation/finalize-comparison.js +16 -0
- package/dist/tools/percy-mcp/creation/finalize-snapshot.d.ts +12 -0
- package/dist/tools/percy-mcp/creation/finalize-snapshot.js +33 -0
- package/dist/tools/percy-mcp/creation/upload-resource.d.ts +15 -0
- package/dist/tools/percy-mcp/creation/upload-resource.js +43 -0
- package/dist/tools/percy-mcp/creation/upload-tile.d.ts +11 -0
- package/dist/tools/percy-mcp/creation/upload-tile.js +53 -0
- package/dist/tools/percy-mcp/diagnostics/analyze-logs-realtime.d.ts +13 -0
- package/dist/tools/percy-mcp/diagnostics/analyze-logs-realtime.js +65 -0
- package/dist/tools/percy-mcp/diagnostics/get-build-logs.d.ts +17 -0
- package/dist/tools/percy-mcp/diagnostics/get-build-logs.js +74 -0
- package/dist/tools/percy-mcp/diagnostics/get-network-logs.d.ts +5 -0
- package/dist/tools/percy-mcp/diagnostics/get-network-logs.js +21 -0
- package/dist/tools/percy-mcp/diagnostics/get-suggestions.d.ts +7 -0
- package/dist/tools/percy-mcp/diagnostics/get-suggestions.js +24 -0
- package/dist/tools/percy-mcp/index.d.ts +36 -0
- package/dist/tools/percy-mcp/index.js +1137 -0
- package/dist/tools/percy-mcp/intelligence/get-ai-analysis.d.ts +15 -0
- package/dist/tools/percy-mcp/intelligence/get-ai-analysis.js +166 -0
- package/dist/tools/percy-mcp/intelligence/get-ai-quota.d.ts +9 -0
- package/dist/tools/percy-mcp/intelligence/get-ai-quota.js +73 -0
- package/dist/tools/percy-mcp/intelligence/get-build-summary.d.ts +11 -0
- package/dist/tools/percy-mcp/intelligence/get-build-summary.js +78 -0
- package/dist/tools/percy-mcp/intelligence/get-rca.d.ts +6 -0
- package/dist/tools/percy-mcp/intelligence/get-rca.js +153 -0
- package/dist/tools/percy-mcp/intelligence/suggest-prompt.d.ts +15 -0
- package/dist/tools/percy-mcp/intelligence/suggest-prompt.js +86 -0
- package/dist/tools/percy-mcp/intelligence/trigger-ai-recompute.d.ts +16 -0
- package/dist/tools/percy-mcp/intelligence/trigger-ai-recompute.js +64 -0
- package/dist/tools/percy-mcp/management/create-project.d.ts +14 -0
- package/dist/tools/percy-mcp/management/create-project.js +52 -0
- package/dist/tools/percy-mcp/management/get-usage-stats.d.ts +12 -0
- package/dist/tools/percy-mcp/management/get-usage-stats.js +61 -0
- package/dist/tools/percy-mcp/management/manage-browser-targets.d.ts +12 -0
- package/dist/tools/percy-mcp/management/manage-browser-targets.js +136 -0
- package/dist/tools/percy-mcp/management/manage-comments.d.ts +14 -0
- package/dist/tools/percy-mcp/management/manage-comments.js +147 -0
- package/dist/tools/percy-mcp/management/manage-ignored-regions.d.ts +18 -0
- package/dist/tools/percy-mcp/management/manage-ignored-regions.js +182 -0
- package/dist/tools/percy-mcp/management/manage-project-settings.d.ts +16 -0
- package/dist/tools/percy-mcp/management/manage-project-settings.js +97 -0
- package/dist/tools/percy-mcp/management/manage-tokens.d.ts +14 -0
- package/dist/tools/percy-mcp/management/manage-tokens.js +90 -0
- package/dist/tools/percy-mcp/management/manage-webhooks.d.ts +15 -0
- package/dist/tools/percy-mcp/management/manage-webhooks.js +180 -0
- package/dist/tools/percy-mcp/v2/auth-status.d.ts +3 -0
- package/dist/tools/percy-mcp/v2/auth-status.js +80 -0
- package/dist/tools/percy-mcp/v2/clone-build.d.ts +24 -0
- package/dist/tools/percy-mcp/v2/clone-build.js +539 -0
- package/dist/tools/percy-mcp/v2/create-app-build.d.ts +28 -0
- package/dist/tools/percy-mcp/v2/create-app-build.js +442 -0
- package/dist/tools/percy-mcp/v2/create-build.d.ts +16 -0
- package/dist/tools/percy-mcp/v2/create-build.js +601 -0
- package/dist/tools/percy-mcp/v2/create-project.d.ts +8 -0
- package/dist/tools/percy-mcp/v2/create-project.js +33 -0
- package/dist/tools/percy-mcp/v2/discover-urls.d.ts +7 -0
- package/dist/tools/percy-mcp/v2/discover-urls.js +38 -0
- package/dist/tools/percy-mcp/v2/figma-baseline.d.ts +7 -0
- package/dist/tools/percy-mcp/v2/figma-baseline.js +18 -0
- package/dist/tools/percy-mcp/v2/figma-build.d.ts +7 -0
- package/dist/tools/percy-mcp/v2/figma-build.js +39 -0
- package/dist/tools/percy-mcp/v2/figma-link.d.ts +6 -0
- package/dist/tools/percy-mcp/v2/figma-link.js +27 -0
- package/dist/tools/percy-mcp/v2/get-ai-summary.d.ts +5 -0
- package/dist/tools/percy-mcp/v2/get-ai-summary.js +109 -0
- package/dist/tools/percy-mcp/v2/get-build-detail.d.ts +22 -0
- package/dist/tools/percy-mcp/v2/get-build-detail.js +566 -0
- package/dist/tools/percy-mcp/v2/get-builds.d.ts +8 -0
- package/dist/tools/percy-mcp/v2/get-builds.js +63 -0
- package/dist/tools/percy-mcp/v2/get-comparison.d.ts +5 -0
- package/dist/tools/percy-mcp/v2/get-comparison.js +95 -0
- package/dist/tools/percy-mcp/v2/get-devices.d.ts +5 -0
- package/dist/tools/percy-mcp/v2/get-devices.js +33 -0
- package/dist/tools/percy-mcp/v2/get-insights.d.ts +7 -0
- package/dist/tools/percy-mcp/v2/get-insights.js +52 -0
- package/dist/tools/percy-mcp/v2/get-projects.d.ts +6 -0
- package/dist/tools/percy-mcp/v2/get-projects.js +41 -0
- package/dist/tools/percy-mcp/v2/get-snapshot.d.ts +5 -0
- package/dist/tools/percy-mcp/v2/get-snapshot.js +93 -0
- package/dist/tools/percy-mcp/v2/get-test-case-history.d.ts +5 -0
- package/dist/tools/percy-mcp/v2/get-test-case-history.js +20 -0
- package/dist/tools/percy-mcp/v2/get-test-cases.d.ts +6 -0
- package/dist/tools/percy-mcp/v2/get-test-cases.js +36 -0
- package/dist/tools/percy-mcp/v2/index.d.ts +35 -0
- package/dist/tools/percy-mcp/v2/index.js +544 -0
- package/dist/tools/percy-mcp/v2/list-integrations.d.ts +5 -0
- package/dist/tools/percy-mcp/v2/list-integrations.js +41 -0
- package/dist/tools/percy-mcp/v2/manage-domains.d.ts +8 -0
- package/dist/tools/percy-mcp/v2/manage-domains.js +33 -0
- package/dist/tools/percy-mcp/v2/manage-insights-email.d.ts +8 -0
- package/dist/tools/percy-mcp/v2/manage-insights-email.js +49 -0
- package/dist/tools/percy-mcp/v2/manage-usage-alerts.d.ts +10 -0
- package/dist/tools/percy-mcp/v2/manage-usage-alerts.js +43 -0
- package/dist/tools/percy-mcp/v2/migrate-integrations.d.ts +6 -0
- package/dist/tools/percy-mcp/v2/migrate-integrations.js +20 -0
- package/dist/tools/percy-mcp/v2/preview-comparison.d.ts +5 -0
- package/dist/tools/percy-mcp/v2/preview-comparison.js +17 -0
- package/dist/tools/percy-mcp/v2/search-build-items.d.ts +12 -0
- package/dist/tools/percy-mcp/v2/search-build-items.js +59 -0
- package/dist/tools/percy-mcp/workflows/auto-triage.d.ts +7 -0
- package/dist/tools/percy-mcp/workflows/auto-triage.js +83 -0
- package/dist/tools/percy-mcp/workflows/clone-build.d.ts +22 -0
- package/dist/tools/percy-mcp/workflows/clone-build.js +414 -0
- package/dist/tools/percy-mcp/workflows/create-percy-build.d.ts +32 -0
- package/dist/tools/percy-mcp/workflows/create-percy-build.js +434 -0
- package/dist/tools/percy-mcp/workflows/debug-failed-build.d.ts +5 -0
- package/dist/tools/percy-mcp/workflows/debug-failed-build.js +122 -0
- package/dist/tools/percy-mcp/workflows/diff-explain.d.ts +6 -0
- package/dist/tools/percy-mcp/workflows/diff-explain.js +148 -0
- package/dist/tools/percy-mcp/workflows/pr-visual-report.d.ts +8 -0
- package/dist/tools/percy-mcp/workflows/pr-visual-report.js +185 -0
- package/dist/tools/percy-mcp/workflows/run-tests.d.ts +17 -0
- package/dist/tools/percy-mcp/workflows/run-tests.js +107 -0
- package/dist/tools/percy-mcp/workflows/snapshot-urls.d.ts +18 -0
- package/dist/tools/percy-mcp/workflows/snapshot-urls.js +197 -0
- package/package.json +3 -2
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Percy API authentication module.
|
|
3
|
+
* Resolves Percy tokens via environment variables or BrowserStack credential fallback.
|
|
4
|
+
*
|
|
5
|
+
* SECURITY: Token values are NEVER logged or included in error messages.
|
|
6
|
+
* Masked format (****<last4>) is used when referencing tokens in diagnostics.
|
|
7
|
+
*/
|
|
8
|
+
import { BrowserStackConfig } from "../types.js";
|
|
9
|
+
type TokenScope = "project" | "org" | "auto";
|
|
10
|
+
interface ResolveTokenOptions {
|
|
11
|
+
projectName?: string;
|
|
12
|
+
scope?: TokenScope;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Masks a token for safe display in error messages.
|
|
16
|
+
* Shows only the last 4 characters.
|
|
17
|
+
*/
|
|
18
|
+
export declare function maskToken(token: string): string;
|
|
19
|
+
/**
|
|
20
|
+
* Resolves a Percy token using the following priority:
|
|
21
|
+
*
|
|
22
|
+
* 1. `process.env.PERCY_TOKEN` (for project or auto scope)
|
|
23
|
+
* 2. `process.env.PERCY_ORG_TOKEN` (for org scope)
|
|
24
|
+
* 3. Fallback: fetch via BrowserStack API using `fetchPercyToken()`
|
|
25
|
+
* 4. If nothing works, throws an enriched error with guidance
|
|
26
|
+
*/
|
|
27
|
+
export declare function resolvePercyToken(config: BrowserStackConfig, options?: ResolveTokenOptions): Promise<string>;
|
|
28
|
+
/**
|
|
29
|
+
* Returns headers for Percy API requests.
|
|
30
|
+
* Includes Authorization, Content-Type, and User-Agent.
|
|
31
|
+
*/
|
|
32
|
+
export declare function getPercyHeaders(config: BrowserStackConfig, options?: {
|
|
33
|
+
scope?: TokenScope;
|
|
34
|
+
projectName?: string;
|
|
35
|
+
}): Promise<Record<string, string>>;
|
|
36
|
+
/**
|
|
37
|
+
* Returns the Percy API base URL.
|
|
38
|
+
* Defaults to `https://percy.io/api/v1`, overridable via `PERCY_API_URL` env var.
|
|
39
|
+
*/
|
|
40
|
+
export declare function getPercyApiBaseUrl(): string;
|
|
41
|
+
export {};
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Percy API authentication module.
|
|
3
|
+
* Resolves Percy tokens via environment variables or BrowserStack credential fallback.
|
|
4
|
+
*
|
|
5
|
+
* SECURITY: Token values are NEVER logged or included in error messages.
|
|
6
|
+
* Masked format (****<last4>) is used when referencing tokens in diagnostics.
|
|
7
|
+
*/
|
|
8
|
+
import { fetchPercyToken } from "../../tools/sdk-utils/percy-web/fetchPercyToken.js";
|
|
9
|
+
/**
|
|
10
|
+
* Masks a token for safe display in error messages.
|
|
11
|
+
* Shows only the last 4 characters.
|
|
12
|
+
*/
|
|
13
|
+
export function maskToken(token) {
|
|
14
|
+
if (token.length <= 4) {
|
|
15
|
+
return "****";
|
|
16
|
+
}
|
|
17
|
+
return `****${token.slice(-4)}`;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Resolves a Percy token using the following priority:
|
|
21
|
+
*
|
|
22
|
+
* 1. `process.env.PERCY_TOKEN` (for project or auto scope)
|
|
23
|
+
* 2. `process.env.PERCY_ORG_TOKEN` (for org scope)
|
|
24
|
+
* 3. Fallback: fetch via BrowserStack API using `fetchPercyToken()`
|
|
25
|
+
* 4. If nothing works, throws an enriched error with guidance
|
|
26
|
+
*/
|
|
27
|
+
export async function resolvePercyToken(config, options = {}) {
|
|
28
|
+
const { projectName, scope = "auto" } = options;
|
|
29
|
+
// For project or auto scope, check PERCY_TOKEN first
|
|
30
|
+
if (scope === "project" || scope === "auto") {
|
|
31
|
+
const envToken = process.env.PERCY_TOKEN;
|
|
32
|
+
if (envToken) {
|
|
33
|
+
return envToken;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
// For org scope, check PERCY_ORG_TOKEN
|
|
37
|
+
if (scope === "org") {
|
|
38
|
+
const orgToken = process.env.PERCY_ORG_TOKEN;
|
|
39
|
+
if (orgToken) {
|
|
40
|
+
return orgToken;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
// For auto scope, also check PERCY_ORG_TOKEN as secondary
|
|
44
|
+
if (scope === "auto") {
|
|
45
|
+
const orgToken = process.env.PERCY_ORG_TOKEN;
|
|
46
|
+
if (orgToken) {
|
|
47
|
+
return orgToken;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
// Fallback: fetch via BrowserStack credentials
|
|
51
|
+
const username = config["browserstack-username"];
|
|
52
|
+
const accessKey = config["browserstack-access-key"];
|
|
53
|
+
if (username && accessKey) {
|
|
54
|
+
const auth = `${username}:${accessKey}`;
|
|
55
|
+
const resolvedProjectName = projectName || "default";
|
|
56
|
+
try {
|
|
57
|
+
const token = await fetchPercyToken(resolvedProjectName, auth, {});
|
|
58
|
+
return token;
|
|
59
|
+
}
|
|
60
|
+
catch (error) {
|
|
61
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
62
|
+
throw new Error(`Failed to fetch Percy token via BrowserStack API: ${message}. ` +
|
|
63
|
+
`Set PERCY_TOKEN or PERCY_ORG_TOKEN environment variable as an alternative.`);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
// Nothing worked — provide actionable guidance
|
|
67
|
+
if (scope === "project") {
|
|
68
|
+
throw new Error("Percy project token not available. Set PERCY_TOKEN environment variable, " +
|
|
69
|
+
"or provide BrowserStack credentials to fetch a token automatically.");
|
|
70
|
+
}
|
|
71
|
+
if (scope === "org") {
|
|
72
|
+
throw new Error("Percy org token not available. Set PERCY_ORG_TOKEN environment variable.");
|
|
73
|
+
}
|
|
74
|
+
throw new Error("Percy token not available. Set PERCY_TOKEN (project) or PERCY_ORG_TOKEN (org) " +
|
|
75
|
+
"environment variable, or provide BrowserStack credentials (browserstack-username " +
|
|
76
|
+
"and browserstack-access-key) to fetch a token automatically.");
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Returns headers for Percy API requests.
|
|
80
|
+
* Includes Authorization, Content-Type, and User-Agent.
|
|
81
|
+
*/
|
|
82
|
+
export async function getPercyHeaders(config, options = {}) {
|
|
83
|
+
const token = await resolvePercyToken(config, options);
|
|
84
|
+
return {
|
|
85
|
+
Authorization: `Token token=${token}`,
|
|
86
|
+
"Content-Type": "application/json",
|
|
87
|
+
"User-Agent": "browserstack-mcp-server",
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Returns the Percy API base URL.
|
|
92
|
+
* Defaults to `https://percy.io/api/v1`, overridable via `PERCY_API_URL` env var.
|
|
93
|
+
*/
|
|
94
|
+
export function getPercyApiBaseUrl() {
|
|
95
|
+
return process.env.PERCY_API_URL || "https://percy.io/api/v1";
|
|
96
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared helpers for the /build-items endpoint.
|
|
3
|
+
*
|
|
4
|
+
* Pagination: the API returns everything when page[limit] is omitted, but may
|
|
5
|
+
* still paginate (meta.pagination.has_more + next_cursor) on very large
|
|
6
|
+
* builds or if server-side defaults change. fetchAllBuildItems handles both:
|
|
7
|
+
* it starts with an unbounded request and follows next_cursor if the response
|
|
8
|
+
* is paginated anyway.
|
|
9
|
+
*/
|
|
10
|
+
import { BrowserStackConfig } from "../types.js";
|
|
11
|
+
export interface BuildItemsResult {
|
|
12
|
+
items: any[];
|
|
13
|
+
/** True if pagination could not be exhausted (page cap or cursor stall). */
|
|
14
|
+
truncated: boolean;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Fetch ALL build items for the given filters, following
|
|
18
|
+
* meta.pagination.next_cursor until has_more is false.
|
|
19
|
+
*/
|
|
20
|
+
export declare function fetchAllBuildItems(config: BrowserStackConfig, baseParams: Record<string, string | string[]>): Promise<BuildItemsResult>;
|
|
21
|
+
/**
|
|
22
|
+
* Filter params that make filter[category]=changed actually return results.
|
|
23
|
+
* The API only maps the changed category to review states via
|
|
24
|
+
* filter[subcategories][]; without it the scope resolves to zero rows.
|
|
25
|
+
*/
|
|
26
|
+
export declare const CHANGED_CATEGORY_PARAMS: Record<string, string | string[]>;
|
|
27
|
+
/**
|
|
28
|
+
* Format a 0..1 diff ratio as a percentage without hiding small diffs:
|
|
29
|
+
* 0 → "0%", tiny → "<0.01%", otherwise two decimals (e.g. "0.02%").
|
|
30
|
+
*/
|
|
31
|
+
export declare function formatDiffPercent(ratio: number | null | undefined): string;
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared helpers for the /build-items endpoint.
|
|
3
|
+
*
|
|
4
|
+
* Pagination: the API returns everything when page[limit] is omitted, but may
|
|
5
|
+
* still paginate (meta.pagination.has_more + next_cursor) on very large
|
|
6
|
+
* builds or if server-side defaults change. fetchAllBuildItems handles both:
|
|
7
|
+
* it starts with an unbounded request and follows next_cursor if the response
|
|
8
|
+
* is paginated anyway.
|
|
9
|
+
*/
|
|
10
|
+
import { percyGet } from "./percy-auth.js";
|
|
11
|
+
const MAX_PAGES = 50;
|
|
12
|
+
/**
|
|
13
|
+
* Fetch ALL build items for the given filters, following
|
|
14
|
+
* meta.pagination.next_cursor until has_more is false.
|
|
15
|
+
*/
|
|
16
|
+
export async function fetchAllBuildItems(config, baseParams) {
|
|
17
|
+
const items = [];
|
|
18
|
+
let cursor;
|
|
19
|
+
let pages = 0;
|
|
20
|
+
for (;;) {
|
|
21
|
+
const params = { ...baseParams };
|
|
22
|
+
if (cursor)
|
|
23
|
+
params["page[cursor]"] = cursor;
|
|
24
|
+
const response = await percyGet("/build-items", config, params);
|
|
25
|
+
items.push(...(response?.data || []));
|
|
26
|
+
pages += 1;
|
|
27
|
+
const pagination = response?.meta?.pagination;
|
|
28
|
+
const nextCursor = pagination?.next_cursor;
|
|
29
|
+
if (!pagination?.has_more || !nextCursor) {
|
|
30
|
+
return { items, truncated: false };
|
|
31
|
+
}
|
|
32
|
+
// Guard against a stalled cursor or runaway loop.
|
|
33
|
+
if (nextCursor === cursor || pages >= MAX_PAGES) {
|
|
34
|
+
return { items, truncated: true };
|
|
35
|
+
}
|
|
36
|
+
cursor = String(nextCursor);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Filter params that make filter[category]=changed actually return results.
|
|
41
|
+
* The API only maps the changed category to review states via
|
|
42
|
+
* filter[subcategories][]; without it the scope resolves to zero rows.
|
|
43
|
+
*/
|
|
44
|
+
export const CHANGED_CATEGORY_PARAMS = {
|
|
45
|
+
"filter[category]": "changed",
|
|
46
|
+
"filter[subcategories][]": ["unreviewed", "changes_requested", "approved"],
|
|
47
|
+
};
|
|
48
|
+
/**
|
|
49
|
+
* Format a 0..1 diff ratio as a percentage without hiding small diffs:
|
|
50
|
+
* 0 → "0%", tiny → "<0.01%", otherwise two decimals (e.g. "0.02%").
|
|
51
|
+
*/
|
|
52
|
+
export function formatDiffPercent(ratio) {
|
|
53
|
+
if (ratio == null)
|
|
54
|
+
return "—";
|
|
55
|
+
const pct = ratio * 100;
|
|
56
|
+
if (pct === 0)
|
|
57
|
+
return "0%";
|
|
58
|
+
if (pct < 0.01)
|
|
59
|
+
return "<0.01%";
|
|
60
|
+
return pct.toFixed(2) + "%";
|
|
61
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Simple in-memory cache with per-entry TTL.
|
|
3
|
+
*
|
|
4
|
+
* Used to cache Percy API responses and avoid redundant network calls
|
|
5
|
+
* within short time windows (e.g., multiple tools querying the same build).
|
|
6
|
+
*/
|
|
7
|
+
export declare class PercyCache {
|
|
8
|
+
private store;
|
|
9
|
+
/**
|
|
10
|
+
* Returns the cached value if it exists and has not expired.
|
|
11
|
+
* Expired entries are deleted on access.
|
|
12
|
+
*/
|
|
13
|
+
get<T>(key: string): T | null;
|
|
14
|
+
/**
|
|
15
|
+
* Stores a value with an optional TTL (defaults to 30 seconds).
|
|
16
|
+
*/
|
|
17
|
+
set(key: string, value: unknown, ttlMs?: number): void;
|
|
18
|
+
/**
|
|
19
|
+
* Removes all entries from the cache.
|
|
20
|
+
*/
|
|
21
|
+
clear(): void;
|
|
22
|
+
/**
|
|
23
|
+
* Removes a single entry from the cache.
|
|
24
|
+
*/
|
|
25
|
+
delete(key: string): void;
|
|
26
|
+
}
|
|
27
|
+
/** Singleton cache instance shared across Percy API tools. */
|
|
28
|
+
export declare const percyCache: PercyCache;
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Simple in-memory cache with per-entry TTL.
|
|
3
|
+
*
|
|
4
|
+
* Used to cache Percy API responses and avoid redundant network calls
|
|
5
|
+
* within short time windows (e.g., multiple tools querying the same build).
|
|
6
|
+
*/
|
|
7
|
+
const DEFAULT_TTL_MS = 30_000; // 30 seconds
|
|
8
|
+
export class PercyCache {
|
|
9
|
+
store = new Map();
|
|
10
|
+
/**
|
|
11
|
+
* Returns the cached value if it exists and has not expired.
|
|
12
|
+
* Expired entries are deleted on access.
|
|
13
|
+
*/
|
|
14
|
+
get(key) {
|
|
15
|
+
const entry = this.store.get(key);
|
|
16
|
+
if (!entry) {
|
|
17
|
+
return null;
|
|
18
|
+
}
|
|
19
|
+
if (Date.now() > entry.expiresAt) {
|
|
20
|
+
this.store.delete(key);
|
|
21
|
+
return null;
|
|
22
|
+
}
|
|
23
|
+
return entry.value;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Stores a value with an optional TTL (defaults to 30 seconds).
|
|
27
|
+
*/
|
|
28
|
+
set(key, value, ttlMs = DEFAULT_TTL_MS) {
|
|
29
|
+
this.store.set(key, {
|
|
30
|
+
value,
|
|
31
|
+
expiresAt: Date.now() + ttlMs,
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Removes all entries from the cache.
|
|
36
|
+
*/
|
|
37
|
+
clear() {
|
|
38
|
+
this.store.clear();
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Removes a single entry from the cache.
|
|
42
|
+
*/
|
|
43
|
+
delete(key) {
|
|
44
|
+
this.store.delete(key);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
/** Singleton cache instance shared across Percy API tools. */
|
|
48
|
+
export const percyCache = new PercyCache();
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Percy API HTTP client.
|
|
3
|
+
*
|
|
4
|
+
* Uses native `fetch` (consistent with existing Percy tools in this repo).
|
|
5
|
+
* Handles JSON:API deserialization, rate limiting, and error enrichment.
|
|
6
|
+
*
|
|
7
|
+
* SECURITY: Token values are NEVER logged or exposed in error messages.
|
|
8
|
+
*/
|
|
9
|
+
import { BrowserStackConfig } from "../types.js";
|
|
10
|
+
type TokenScope = "project" | "org" | "auto";
|
|
11
|
+
interface ClientOptions {
|
|
12
|
+
scope?: TokenScope;
|
|
13
|
+
projectName?: string;
|
|
14
|
+
}
|
|
15
|
+
interface JsonApiResource {
|
|
16
|
+
id: string;
|
|
17
|
+
type: string;
|
|
18
|
+
attributes?: Record<string, unknown>;
|
|
19
|
+
relationships?: Record<string, {
|
|
20
|
+
data: {
|
|
21
|
+
id: string;
|
|
22
|
+
type: string;
|
|
23
|
+
} | Array<{
|
|
24
|
+
id: string;
|
|
25
|
+
type: string;
|
|
26
|
+
}> | null;
|
|
27
|
+
}>;
|
|
28
|
+
}
|
|
29
|
+
interface JsonApiEnvelope {
|
|
30
|
+
data: JsonApiResource | JsonApiResource[] | null;
|
|
31
|
+
included?: JsonApiResource[];
|
|
32
|
+
meta?: Record<string, unknown>;
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Deserializes a JSON:API envelope into plain objects.
|
|
36
|
+
*
|
|
37
|
+
* - `data: null` → returns `null`
|
|
38
|
+
* - `data: []` → returns `[]`
|
|
39
|
+
* - `data: { ... }` → returns a single deserialized object
|
|
40
|
+
* - `data: [{ ... }, ...]` → returns an array of deserialized objects
|
|
41
|
+
*/
|
|
42
|
+
export declare function deserialize(envelope: JsonApiEnvelope): {
|
|
43
|
+
data: Record<string, unknown> | Record<string, unknown>[] | null;
|
|
44
|
+
meta?: Record<string, unknown>;
|
|
45
|
+
};
|
|
46
|
+
export declare class PercyClient {
|
|
47
|
+
private config;
|
|
48
|
+
private options;
|
|
49
|
+
constructor(config: BrowserStackConfig, options?: ClientOptions);
|
|
50
|
+
/**
|
|
51
|
+
* GET request with optional query params and JSON:API `include`.
|
|
52
|
+
*/
|
|
53
|
+
get<T = Record<string, unknown>>(path: string, params?: Record<string, string>, includes?: string[]): Promise<T>;
|
|
54
|
+
/**
|
|
55
|
+
* POST request with an optional JSON body.
|
|
56
|
+
*/
|
|
57
|
+
post<T = Record<string, unknown>>(path: string, body?: unknown): Promise<T>;
|
|
58
|
+
/**
|
|
59
|
+
* PATCH request with an optional JSON body.
|
|
60
|
+
*/
|
|
61
|
+
patch<T = Record<string, unknown>>(path: string, body?: unknown): Promise<T>;
|
|
62
|
+
/**
|
|
63
|
+
* DELETE request.
|
|
64
|
+
*/
|
|
65
|
+
del(path: string): Promise<void>;
|
|
66
|
+
private buildUrl;
|
|
67
|
+
private request;
|
|
68
|
+
}
|
|
69
|
+
export {};
|
|
@@ -0,0 +1,275 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Percy API HTTP client.
|
|
3
|
+
*
|
|
4
|
+
* Uses native `fetch` (consistent with existing Percy tools in this repo).
|
|
5
|
+
* Handles JSON:API deserialization, rate limiting, and error enrichment.
|
|
6
|
+
*
|
|
7
|
+
* SECURITY: Token values are NEVER logged or exposed in error messages.
|
|
8
|
+
*/
|
|
9
|
+
import { getPercyHeaders, getPercyApiBaseUrl } from "./auth.js";
|
|
10
|
+
import { enrichPercyError } from "./errors.js";
|
|
11
|
+
// ---------------------------------------------------------------------------
|
|
12
|
+
// Helpers – kebab-case to camelCase
|
|
13
|
+
// ---------------------------------------------------------------------------
|
|
14
|
+
function kebabToCamel(str) {
|
|
15
|
+
return str.replace(/-([a-z0-9])/g, (_, char) => char.toUpperCase());
|
|
16
|
+
}
|
|
17
|
+
function camelCaseKeys(obj) {
|
|
18
|
+
if (obj === null || obj === undefined) {
|
|
19
|
+
return obj;
|
|
20
|
+
}
|
|
21
|
+
if (Array.isArray(obj)) {
|
|
22
|
+
return obj.map(camelCaseKeys);
|
|
23
|
+
}
|
|
24
|
+
if (typeof obj === "object") {
|
|
25
|
+
const result = {};
|
|
26
|
+
for (const [key, value] of Object.entries(obj)) {
|
|
27
|
+
const camelKey = kebabToCamel(key);
|
|
28
|
+
result[camelKey] =
|
|
29
|
+
value !== null && typeof value === "object"
|
|
30
|
+
? camelCaseKeys(value)
|
|
31
|
+
: value;
|
|
32
|
+
}
|
|
33
|
+
return result;
|
|
34
|
+
}
|
|
35
|
+
return obj;
|
|
36
|
+
}
|
|
37
|
+
// ---------------------------------------------------------------------------
|
|
38
|
+
// JSON:API Deserializer
|
|
39
|
+
// ---------------------------------------------------------------------------
|
|
40
|
+
/**
|
|
41
|
+
* Builds a lookup index of included resources keyed by `type:id`.
|
|
42
|
+
*/
|
|
43
|
+
function buildIncludedIndex(included) {
|
|
44
|
+
const index = new Map();
|
|
45
|
+
for (const resource of included) {
|
|
46
|
+
const flattened = flattenResource(resource);
|
|
47
|
+
index.set(`${resource.type}:${resource.id}`, flattened);
|
|
48
|
+
}
|
|
49
|
+
return index;
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Flattens a single JSON:API resource — merges `attributes` into the top
|
|
53
|
+
* level alongside `id` and `type`, converting keys to camelCase.
|
|
54
|
+
*/
|
|
55
|
+
function flattenResource(resource) {
|
|
56
|
+
const attrs = resource.attributes
|
|
57
|
+
? camelCaseKeys(resource.attributes)
|
|
58
|
+
: {};
|
|
59
|
+
return {
|
|
60
|
+
id: resource.id,
|
|
61
|
+
type: resource.type,
|
|
62
|
+
...attrs,
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Resolves relationships for a resource against the included index.
|
|
67
|
+
* Returns the resolved object(s) or the raw { id, type } ref when not found.
|
|
68
|
+
*/
|
|
69
|
+
function resolveRelationships(resource, index) {
|
|
70
|
+
if (!resource.relationships) {
|
|
71
|
+
return {};
|
|
72
|
+
}
|
|
73
|
+
const resolved = {};
|
|
74
|
+
for (const [relName, relValue] of Object.entries(resource.relationships)) {
|
|
75
|
+
const camelName = kebabToCamel(relName);
|
|
76
|
+
const { data } = relValue;
|
|
77
|
+
if (data === null || data === undefined) {
|
|
78
|
+
resolved[camelName] = null;
|
|
79
|
+
}
|
|
80
|
+
else if (Array.isArray(data)) {
|
|
81
|
+
resolved[camelName] = data.map((ref) => index.get(`${ref.type}:${ref.id}`) ?? { id: ref.id, type: ref.type });
|
|
82
|
+
}
|
|
83
|
+
else {
|
|
84
|
+
resolved[camelName] = index.get(`${data.type}:${data.id}`) ?? {
|
|
85
|
+
id: data.id,
|
|
86
|
+
type: data.type,
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
return resolved;
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* Deserializes a JSON:API envelope into plain objects.
|
|
94
|
+
*
|
|
95
|
+
* - `data: null` → returns `null`
|
|
96
|
+
* - `data: []` → returns `[]`
|
|
97
|
+
* - `data: { ... }` → returns a single deserialized object
|
|
98
|
+
* - `data: [{ ... }, ...]` → returns an array of deserialized objects
|
|
99
|
+
*/
|
|
100
|
+
export function deserialize(envelope) {
|
|
101
|
+
const included = envelope.included ?? [];
|
|
102
|
+
const index = buildIncludedIndex(included);
|
|
103
|
+
if (envelope.data === null || envelope.data === undefined) {
|
|
104
|
+
return { data: null, meta: envelope.meta };
|
|
105
|
+
}
|
|
106
|
+
if (Array.isArray(envelope.data)) {
|
|
107
|
+
const records = envelope.data.map((resource) => ({
|
|
108
|
+
...flattenResource(resource),
|
|
109
|
+
...resolveRelationships(resource, index),
|
|
110
|
+
}));
|
|
111
|
+
return { data: records, meta: envelope.meta };
|
|
112
|
+
}
|
|
113
|
+
const record = {
|
|
114
|
+
...flattenResource(envelope.data),
|
|
115
|
+
...resolveRelationships(envelope.data, index),
|
|
116
|
+
};
|
|
117
|
+
return { data: record, meta: envelope.meta };
|
|
118
|
+
}
|
|
119
|
+
// ---------------------------------------------------------------------------
|
|
120
|
+
// Rate Limit / Retry
|
|
121
|
+
// ---------------------------------------------------------------------------
|
|
122
|
+
const MAX_RETRIES = 3;
|
|
123
|
+
const BASE_RETRY_DELAY_MS = 1_000;
|
|
124
|
+
async function sleep(ms) {
|
|
125
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
126
|
+
}
|
|
127
|
+
// ---------------------------------------------------------------------------
|
|
128
|
+
// PercyClient
|
|
129
|
+
// ---------------------------------------------------------------------------
|
|
130
|
+
export class PercyClient {
|
|
131
|
+
config;
|
|
132
|
+
options;
|
|
133
|
+
constructor(config, options) {
|
|
134
|
+
this.config = config;
|
|
135
|
+
this.options = options ?? {};
|
|
136
|
+
}
|
|
137
|
+
// -----------------------------------------------------------------------
|
|
138
|
+
// Public HTTP methods
|
|
139
|
+
// -----------------------------------------------------------------------
|
|
140
|
+
/**
|
|
141
|
+
* GET request with optional query params and JSON:API `include`.
|
|
142
|
+
*/
|
|
143
|
+
async get(path, params, includes) {
|
|
144
|
+
const url = this.buildUrl(path, params, includes);
|
|
145
|
+
return this.request("GET", url);
|
|
146
|
+
}
|
|
147
|
+
/**
|
|
148
|
+
* POST request with an optional JSON body.
|
|
149
|
+
*/
|
|
150
|
+
async post(path, body) {
|
|
151
|
+
const url = this.buildUrl(path);
|
|
152
|
+
return this.request("POST", url, body);
|
|
153
|
+
}
|
|
154
|
+
/**
|
|
155
|
+
* PATCH request with an optional JSON body.
|
|
156
|
+
*/
|
|
157
|
+
async patch(path, body) {
|
|
158
|
+
const url = this.buildUrl(path);
|
|
159
|
+
return this.request("PATCH", url, body);
|
|
160
|
+
}
|
|
161
|
+
/**
|
|
162
|
+
* DELETE request.
|
|
163
|
+
*/
|
|
164
|
+
async del(path) {
|
|
165
|
+
const url = this.buildUrl(path);
|
|
166
|
+
await this.request("DELETE", url);
|
|
167
|
+
}
|
|
168
|
+
// -----------------------------------------------------------------------
|
|
169
|
+
// Internal
|
|
170
|
+
// -----------------------------------------------------------------------
|
|
171
|
+
buildUrl(path, params, includes) {
|
|
172
|
+
const base = getPercyApiBaseUrl();
|
|
173
|
+
// Ensure no double slashes between base and path
|
|
174
|
+
const normalizedPath = path.startsWith("/") ? path : `/${path}`;
|
|
175
|
+
const url = new URL(`${base}${normalizedPath}`);
|
|
176
|
+
if (params) {
|
|
177
|
+
for (const [key, value] of Object.entries(params)) {
|
|
178
|
+
url.searchParams.set(key, value);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
if (includes && includes.length > 0) {
|
|
182
|
+
url.searchParams.set("include", includes.join(","));
|
|
183
|
+
}
|
|
184
|
+
return url.toString();
|
|
185
|
+
}
|
|
186
|
+
async request(method, url, body) {
|
|
187
|
+
const headers = await getPercyHeaders(this.config, {
|
|
188
|
+
scope: this.options.scope,
|
|
189
|
+
projectName: this.options.projectName,
|
|
190
|
+
});
|
|
191
|
+
let lastError;
|
|
192
|
+
for (let attempt = 0; attempt <= MAX_RETRIES; attempt++) {
|
|
193
|
+
const fetchOptions = {
|
|
194
|
+
method,
|
|
195
|
+
headers,
|
|
196
|
+
};
|
|
197
|
+
if (body !== undefined) {
|
|
198
|
+
fetchOptions.body = JSON.stringify(body);
|
|
199
|
+
}
|
|
200
|
+
let response;
|
|
201
|
+
try {
|
|
202
|
+
response = await fetch(url, fetchOptions);
|
|
203
|
+
}
|
|
204
|
+
catch (networkError) {
|
|
205
|
+
lastError =
|
|
206
|
+
networkError instanceof Error
|
|
207
|
+
? networkError
|
|
208
|
+
: new Error(String(networkError));
|
|
209
|
+
// Network errors are not retryable via the rate-limit path,
|
|
210
|
+
// but we still respect the retry loop for consistency.
|
|
211
|
+
if (attempt < MAX_RETRIES) {
|
|
212
|
+
await sleep(BASE_RETRY_DELAY_MS * Math.pow(2, attempt));
|
|
213
|
+
continue;
|
|
214
|
+
}
|
|
215
|
+
throw lastError;
|
|
216
|
+
}
|
|
217
|
+
// 204 No Content
|
|
218
|
+
if (response.status === 204) {
|
|
219
|
+
return undefined;
|
|
220
|
+
}
|
|
221
|
+
// Rate limited — retry with backoff
|
|
222
|
+
if (response.status === 429) {
|
|
223
|
+
const retryAfter = response.headers.get("Retry-After");
|
|
224
|
+
const delayMs = retryAfter
|
|
225
|
+
? parseFloat(retryAfter) * 1_000
|
|
226
|
+
: BASE_RETRY_DELAY_MS * Math.pow(2, attempt);
|
|
227
|
+
if (attempt < MAX_RETRIES) {
|
|
228
|
+
await sleep(delayMs);
|
|
229
|
+
continue;
|
|
230
|
+
}
|
|
231
|
+
// Exhausted retries — throw enriched error
|
|
232
|
+
let errorBody;
|
|
233
|
+
try {
|
|
234
|
+
errorBody = await response.json();
|
|
235
|
+
}
|
|
236
|
+
catch {
|
|
237
|
+
errorBody = undefined;
|
|
238
|
+
}
|
|
239
|
+
throw enrichPercyError(429, errorBody, `${method} ${url}`);
|
|
240
|
+
}
|
|
241
|
+
// Non-2xx error
|
|
242
|
+
if (!response.ok) {
|
|
243
|
+
let errorBody;
|
|
244
|
+
try {
|
|
245
|
+
errorBody = await response.json();
|
|
246
|
+
}
|
|
247
|
+
catch {
|
|
248
|
+
errorBody = undefined;
|
|
249
|
+
}
|
|
250
|
+
throw enrichPercyError(response.status, errorBody, `${method} ${url}`);
|
|
251
|
+
}
|
|
252
|
+
// Successful JSON response — deserialize JSON:API
|
|
253
|
+
const json = await response.json();
|
|
254
|
+
// If the response has a JSON:API `data` key, deserialize it
|
|
255
|
+
if (json && typeof json === "object" && "data" in json) {
|
|
256
|
+
const deserialized = deserialize(json);
|
|
257
|
+
// Unwrap: return the data directly (single object or array)
|
|
258
|
+
// Attach meta as a non-enumerable property so it's accessible but doesn't clutter
|
|
259
|
+
const result = deserialized.data;
|
|
260
|
+
if (result && typeof result === "object" && deserialized.meta) {
|
|
261
|
+
Object.defineProperty(result, "__meta", {
|
|
262
|
+
value: deserialized.meta,
|
|
263
|
+
enumerable: false,
|
|
264
|
+
writable: false,
|
|
265
|
+
});
|
|
266
|
+
}
|
|
267
|
+
return result;
|
|
268
|
+
}
|
|
269
|
+
// Non-JSON:API response — return as-is
|
|
270
|
+
return json;
|
|
271
|
+
}
|
|
272
|
+
// Should not reach here, but satisfy TypeScript
|
|
273
|
+
throw lastError ?? new Error("Request failed after retries");
|
|
274
|
+
}
|
|
275
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Percy API error enrichment module.
|
|
3
|
+
* Maps Percy API error responses to actionable, user-friendly messages.
|
|
4
|
+
*/
|
|
5
|
+
export declare class PercyApiError extends Error {
|
|
6
|
+
statusCode: number;
|
|
7
|
+
errorCode?: string;
|
|
8
|
+
body?: unknown;
|
|
9
|
+
constructor(message: string, statusCode: number, errorCode?: string, body?: unknown);
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Maps Percy API error responses to actionable messages.
|
|
13
|
+
* Handles known error codes from Percy's JSON:API responses.
|
|
14
|
+
*/
|
|
15
|
+
export declare function enrichPercyError(status: number, body: unknown, context?: string): PercyApiError;
|