Skip to content

Commit 6955360

Browse files
committed
refactor: update Percy integration to fetch changed snapshot IDs and remove unused simulate handler
1 parent 3190749 commit 6955360

File tree

6 files changed

+118
-139
lines changed

6 files changed

+118
-139
lines changed

src/tools/percy-change.ts

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { BrowserStackConfig } from "../lib/types.js";
33
import { getBrowserStackAuth } from "../lib/get-auth.js";
44
import { CallToolResult } from "@modelcontextprotocol/sdk/types.js";
55
import { getPercyBuildCount } from "./review-agent-utils/build-counts.js";
6-
import { getPercySnapshotIds } from "./review-agent-utils/percy-snapshots.js";
6+
import { getChangedPercySnapshotIds } from "./review-agent-utils/percy-snapshots.js";
77
import { PercyIntegrationTypeEnum } from "./sdk-utils/common/types.js";
88
import { fetchPercyToken } from "./sdk-utils/percy-web/fetchPercyToken.js";
99

@@ -25,7 +25,7 @@ export async function fetchPercyChanges(
2525
});
2626

2727
// Get build info (noBuilds, isFirstBuild, lastBuildId)
28-
const { noBuilds, isFirstBuild, lastBuildId } =
28+
const { noBuilds, isFirstBuild, lastBuildId, orgId } =
2929
await getPercyBuildCount(percyToken);
3030

3131
if (noBuilds) {
@@ -51,7 +51,11 @@ export async function fetchPercyChanges(
5151
}
5252

5353
// Get snapshot IDs for the latest build
54-
const snapshotIds = await getPercySnapshotIds(lastBuildId, percyToken);
54+
const snapshotIds = await getChangedPercySnapshotIds(
55+
lastBuildId,
56+
config,
57+
orgId,
58+
);
5559
logger.info(
5660
`Fetched ${snapshotIds.length} snapshot IDs for build: ${lastBuildId} as ${snapshotIds.join(", ")}`,
5761
);

src/tools/percy-sdk.ts

Lines changed: 2 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -7,15 +7,10 @@ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
77
import { SetUpPercyParamsShape } from "./sdk-utils/common/schema.js";
88
import { updateTestsWithPercyCommands } from "./add-percy-snapshots.js";
99
import { approveOrDeclinePercyBuild } from "./review-agent-utils/percy-approve-reject.js";
10-
11-
import {
12-
setUpPercyHandler,
13-
setUpSimulatePercyChangeHandler,
14-
} from "./sdk-utils/handler.js";
10+
import { setUpPercyHandler } from "./sdk-utils/handler.js";
1511

1612
import {
1713
SETUP_PERCY_DESCRIPTION,
18-
SIMULATE_PERCY_CHANGE_DESCRIPTION,
1914
LIST_TEST_FILES_DESCRIPTION,
2015
PERCY_SNAPSHOT_COMMANDS_DESCRIPTION,
2116
} from "./sdk-utils/common/constants.js";
@@ -70,39 +65,6 @@ export function registerPercyTools(
7065
},
7166
);
7267

73-
// Register simulatePercyChange
74-
tools.simulatePercyChange = server.tool(
75-
"simulatePercyChange",
76-
SIMULATE_PERCY_CHANGE_DESCRIPTION,
77-
SetUpPercyParamsShape,
78-
async (args) => {
79-
try {
80-
trackMCP(
81-
"simulatePercyChange",
82-
server.server.getClientVersion()!,
83-
config,
84-
);
85-
return setUpSimulatePercyChangeHandler(args, config);
86-
} catch (error) {
87-
trackMCP(
88-
"simulatePercyChange",
89-
server.server.getClientVersion()!,
90-
error,
91-
config,
92-
);
93-
return {
94-
content: [
95-
{
96-
type: "text",
97-
text: error instanceof Error ? error.message : String(error),
98-
},
99-
],
100-
isError: true,
101-
};
102-
}
103-
},
104-
);
105-
10668
// Register addPercySnapshotCommands
10769
tools.addPercySnapshotCommands = server.tool(
10870
"addPercySnapshotCommands",
@@ -167,7 +129,7 @@ export function registerPercyTools(
167129

168130
tools.runPercyScan = server.tool(
169131
"runPercyScan",
170-
"Run a Percy visual test scan. Example prompts : Run this Percy build/scan.Never run percy scan/build without this tool",
132+
"Run a Percy visual test scan. Example prompts : Run this Percy build/scan. Never run percy scan/build without this tool",
171133
RunPercyScanParamsShape,
172134
async (args) => {
173135
return runPercyScan(args, config);

src/tools/review-agent-utils/build-counts.ts

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
// Utility for fetching the count of Percy builds.
1+
// Utility for fetching the count of Percy builds and orgId.
22
export async function getPercyBuildCount(percyToken: string) {
33
const apiUrl = `https://percy.io/api/v1/builds`;
44

@@ -15,12 +15,19 @@ export async function getPercyBuildCount(percyToken: string) {
1515

1616
const data = await response.json();
1717
const builds = data.data ?? [];
18+
const included = data.included ?? [];
1819

1920
let isFirstBuild = false;
20-
let lastBuildId;
21+
let lastBuildId: string | undefined;
22+
let orgId: string | undefined;
2123

2224
if (builds.length === 0) {
23-
return { noBuilds: true, isFirstBuild: false, lastBuildId: undefined };
25+
return {
26+
noBuilds: true,
27+
isFirstBuild: false,
28+
lastBuildId: undefined,
29+
orgId,
30+
};
2431
} else if (builds.length === 1) {
2532
isFirstBuild = true;
2633
lastBuildId = builds[0].id;
@@ -29,5 +36,11 @@ export async function getPercyBuildCount(percyToken: string) {
2936
lastBuildId = builds[0].id;
3037
}
3138

32-
return { noBuilds: false, isFirstBuild, lastBuildId };
39+
// Extract orgId from the `included` projects block
40+
const project = included.find((item: any) => item.type === "projects");
41+
if (project?.relationships?.organization?.data?.id) {
42+
orgId = project.relationships.organization.data.id;
43+
}
44+
45+
return { noBuilds: false, isFirstBuild, lastBuildId, orgId };
3346
}

src/tools/review-agent-utils/percy-diffs.ts

Lines changed: 1 addition & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,3 @@
1-
/**
2-
* Utility for fetching and aggregating Percy snapshot diffs.
3-
*/
4-
51
export interface PercySnapshotDiff {
62
id: string;
73
name: string | null;
@@ -10,18 +6,12 @@ export interface PercySnapshotDiff {
106
coordinates: any;
117
}
128

13-
/**
14-
* Fetches diffs for a single Percy snapshot.
15-
* @param snapshotId - The Percy snapshot ID.
16-
* @param percyToken - The Percy API token.
17-
* @returns Array of PercySnapshotDiff objects.
18-
*/
199
export async function getPercySnapshotDiff(
2010
snapshotId: string,
2111
percyToken: string,
2212
): Promise<PercySnapshotDiff[]> {
2313
const apiUrl = `https://percy.io/api/v1/snapshots/${snapshotId}`;
24-
14+
2515
const response = await fetch(apiUrl, {
2616
headers: {
2717
Authorization: `Token token=${percyToken}`,
@@ -59,12 +49,6 @@ export async function getPercySnapshotDiff(
5949
return changes;
6050
}
6151

62-
/**
63-
* Fetches and flattens all diffs for an array of Percy snapshot IDs.
64-
* @param snapshotIds - Array of Percy snapshot IDs.
65-
* @param percyToken - The Percy API token.
66-
* @returns Flat array of PercySnapshotDiff objects.
67-
*/
6852
export async function getPercySnapshotDiffs(
6953
snapshotIds: string[],
7054
percyToken: string,
Lines changed: 91 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -1,45 +1,102 @@
1-
// Utility for fetching Percy snapshot IDs for a given build (up to `maxSnapshots`).
2-
export async function getPercySnapshotIds(
1+
import { getBrowserStackAuth } from "../../lib/get-auth.js";
2+
import { BrowserStackConfig } from "../../lib/types.js";
3+
import { sanitizeUrlParam } from "../../lib/utils.js";
4+
5+
// Utility for fetching only the IDs of changed Percy snapshots for a given build.
6+
export async function getChangedPercySnapshotIds(
37
buildId: string,
4-
percyToken: string,
5-
maxSnapshots = 300,
8+
config: BrowserStackConfig,
9+
orgId: string | undefined,
610
): Promise<string[]> {
7-
const perPage = 30;
8-
const allSnapshotIds: string[] = [];
9-
let cursor: string | undefined = undefined;
11+
12+
if (!buildId || !orgId) {
13+
throw new Error(
14+
"Failed to fetch AI Summary: Missing build ID or organization ID",
15+
);
16+
}
1017

11-
while (allSnapshotIds.length < maxSnapshots) {
12-
const url = new URL(`https://percy.io/api/v1/snapshots`);
13-
url.searchParams.set("build_id", buildId);
14-
url.searchParams.set("page[limit]", String(perPage));
15-
if (cursor) url.searchParams.set("page[cursor]", cursor);
18+
const urlStr = constructPercyBuildItemsUrl({
19+
buildId,
20+
orgId,
21+
category: ["changed"],
22+
subcategories: ["unreviewed","approved","changes_requested"],
23+
groupSnapshotsBy: "similar_diff",
24+
browserIds: ["63", "64", "69", "70", "71"],
25+
widths: ["375","1280","1920"],
26+
});
1627

17-
const response = await fetch(url.toString(), {
18-
headers: {
19-
Authorization: `Token token=${percyToken}`,
20-
"Content-Type": "application/json",
21-
},
22-
});
28+
const authString = getBrowserStackAuth(config);
29+
const auth = Buffer.from(authString).toString("base64");
30+
const response = await fetch(urlStr, {
31+
headers: {
32+
Authorization: `Basic ${auth}`,
33+
"Content-Type": "application/json",
34+
},
35+
});
2336

24-
if (!response.ok) {
25-
throw new Error(
26-
`Failed to fetch Percy snapshots: ${response.statusText}`,
27-
);
28-
}
37+
if (!response.ok) {
38+
throw new Error(
39+
`Failed to fetch changed Percy snapshots: ${response.status} ${response.statusText}`,
40+
);
41+
}
2942

30-
const data = await response.json();
31-
const snapshots = data.data ?? [];
32-
if (snapshots.length === 0) break; // no more snapshots
43+
const responseData = await response.json();
44+
const buildItems = responseData.data ?? [];
3345

34-
allSnapshotIds.push(...snapshots.map((s: any) => String(s.id)));
46+
if (buildItems.length === 0) {
47+
return [];
48+
}
3549

36-
// Set cursor to last snapshot ID of this page for next iteration
37-
cursor = snapshots[snapshots.length - 1].id;
50+
const snapshotIds = buildItems
51+
.flatMap((item: any) => item.attributes?.["snapshot-ids"] ?? [])
52+
.map((id: any) => String(id));
3853

39-
// Stop if we've collected enough
40-
if (allSnapshotIds.length >= maxSnapshots) break;
41-
}
54+
return snapshotIds;
55+
}
4256

43-
// Return only up to maxSnapshots
44-
return allSnapshotIds.slice(0, maxSnapshots);
57+
export function constructPercyBuildItemsUrl({
58+
buildId,
59+
orgId,
60+
category = [],
61+
subcategories = [],
62+
browserIds = [],
63+
widths = [],
64+
groupSnapshotsBy,
65+
}: {
66+
buildId: string;
67+
orgId: string;
68+
category?: string[];
69+
subcategories?: string[];
70+
browserIds?: string[];
71+
widths?: string[];
72+
groupSnapshotsBy?: string;
73+
}): string {
74+
const url = new URL("https://percy.io/api/v1/build-items");
75+
url.searchParams.set("filter[build-id]", sanitizeUrlParam(buildId));
76+
url.searchParams.set("filter[organization-id]", sanitizeUrlParam(orgId));
77+
78+
if (category && category.length > 0) {
79+
category.forEach((cat) =>
80+
url.searchParams.append("filter[category][]", sanitizeUrlParam(cat))
81+
);
82+
}
83+
if (subcategories && subcategories.length > 0) {
84+
subcategories.forEach((sub) =>
85+
url.searchParams.append("filter[subcategories][]", sanitizeUrlParam(sub))
86+
);
87+
}
88+
if (browserIds && browserIds.length > 0) {
89+
browserIds.forEach((id) =>
90+
url.searchParams.append("filter[browser_ids][]", sanitizeUrlParam(id))
91+
);
92+
}
93+
if (widths && widths.length > 0) {
94+
widths.forEach((w) =>
95+
url.searchParams.append("filter[widths][]", sanitizeUrlParam(w))
96+
);
97+
}
98+
if (groupSnapshotsBy) {
99+
url.searchParams.set("filter[group_snapshots_by]", sanitizeUrlParam(groupSnapshotsBy));
100+
}
101+
return url.toString();
45102
}

src/tools/sdk-utils/handler.ts

Lines changed: 0 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -17,11 +17,6 @@ import { runPercyAutomateOnly } from "./percy-automate/handler.js";
1717
import { runBstackSDKOnly } from "./bstack/sdkHandler.js";
1818
import { runPercyWithBrowserstackSDK } from "./percy-bstack/handler.js";
1919
import { checkPercyIntegrationSupport } from "./common/utils.js";
20-
import {
21-
PERCY_SIMULATE_INSTRUCTION,
22-
PERCY_REPLACE_REGEX,
23-
PERCY_SIMULATION_DRIVER_INSTRUCTION,
24-
} from "./common/constants.js";
2520

2621
export async function runTestsOnBrowserStackHandler(
2722
rawInput: unknown,
@@ -166,39 +161,3 @@ export async function setUpPercyHandler(
166161
throw new Error(getBootstrapFailedMessage(error, { config }));
167162
}
168163
}
169-
170-
export async function setUpSimulatePercyChangeHandler(
171-
rawInput: unknown,
172-
config: BrowserStackConfig,
173-
): Promise<CallToolResult> {
174-
try {
175-
const percyInstruction = await setUpPercyHandler(rawInput, config);
176-
177-
if (percyInstruction.isError) {
178-
return percyInstruction;
179-
}
180-
181-
if (Array.isArray(percyInstruction.content)) {
182-
percyInstruction.content.forEach((item) => {
183-
if (
184-
typeof item.text === "string" &&
185-
PERCY_REPLACE_REGEX.test(item.text)
186-
) {
187-
item.text = item.text.replace(
188-
PERCY_REPLACE_REGEX,
189-
PERCY_SIMULATE_INSTRUCTION,
190-
);
191-
}
192-
});
193-
}
194-
195-
percyInstruction.content?.push({
196-
type: "text" as const,
197-
text: PERCY_SIMULATION_DRIVER_INSTRUCTION,
198-
});
199-
200-
return percyInstruction;
201-
} catch (error) {
202-
throw new Error(getBootstrapFailedMessage(error, { config }));
203-
}
204-
}

0 commit comments

Comments
 (0)