Skip to content

Commit 950be09

Browse files
dcramercodex
andcommitted
fix(api): Preserve SDK query behavior after rebase
Handle SDK response shapes and replay query validation after rebasing the API SDK branch onto main. Keep API detail messages in tests and preserve multi-environment replay queries. Co-Authored-By: GPT-5 Codex <codex@openai.com>
1 parent 1a285de commit 950be09

3 files changed

Lines changed: 62 additions & 47 deletions

File tree

packages/mcp-core/src/api-client/client.test.ts

Lines changed: 39 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -1470,41 +1470,47 @@ describe("API query builders", () => {
14701470
});
14711471
const urls: string[] = [];
14721472

1473-
globalThis.fetch = vi.fn().mockImplementation((url: string) => {
1474-
urls.push(url);
1475-
const requestUrl = new URL(url);
1476-
const attributeType = requestUrl.searchParams.get("attributeType");
1477-
const body =
1478-
attributeType === "boolean"
1479-
? [
1480-
{
1481-
key: "tags[enabled,boolean]",
1482-
name: "enabled",
1483-
attributeType: "boolean",
1484-
attributeSource: { source_type: "user" },
1485-
},
1486-
]
1487-
: [
1488-
{
1489-
key: "tags[type]",
1490-
name: "type",
1491-
attributeType: "string",
1492-
attributeSource: {
1493-
source_type: "sentry",
1494-
is_transformed_alias: true,
1473+
globalThis.fetch = vi
1474+
.fn()
1475+
.mockImplementation((input: string | Request) => {
1476+
const url =
1477+
typeof input === "string"
1478+
? input
1479+
: input instanceof Request
1480+
? input.url
1481+
: String(input);
1482+
urls.push(url);
1483+
const requestUrl = new URL(url);
1484+
const attributeType = requestUrl.searchParams.get("attributeType");
1485+
const body =
1486+
attributeType === "boolean"
1487+
? [
1488+
{
1489+
key: "tags[enabled,boolean]",
1490+
name: "enabled",
1491+
attributeType: "boolean",
1492+
attributeSource: { source_type: "user" },
14951493
},
1496-
},
1497-
];
1498-
1499-
return Promise.resolve({
1500-
ok: true,
1501-
headers: {
1502-
get: (key: string) =>
1503-
key === "content-type" ? "application/json" : null,
1504-
},
1505-
json: () => Promise.resolve(body),
1494+
]
1495+
: [
1496+
{
1497+
key: "tags[type]",
1498+
name: "type",
1499+
attributeType: "string",
1500+
attributeSource: {
1501+
source_type: "sentry",
1502+
is_transformed_alias: true,
1503+
},
1504+
},
1505+
];
1506+
1507+
return Promise.resolve(
1508+
new Response(JSON.stringify(body), {
1509+
status: 200,
1510+
headers: { "Content-Type": "application/json" },
1511+
}),
1512+
);
15061513
});
1507-
});
15081514

15091515
const result = await apiService.listTraceItemAttributes({
15101516
organizationSlug: "test-org",

packages/mcp-core/src/api-client/client.ts

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -312,12 +312,13 @@ function parseTraceItemAttributes(
312312
body: unknown,
313313
fallbackType: TraceItemAttributeType,
314314
): TraceItemAttribute[] {
315-
if (!Array.isArray(body)) {
315+
const values = isRecord(body) && Array.isArray(body.data) ? body.data : body;
316+
if (!Array.isArray(values)) {
316317
return [];
317318
}
318319

319320
const attributes: TraceItemAttribute[] = [];
320-
for (const value of body) {
321+
for (const value of values) {
321322
if (!isRecord(value) || typeof value.key !== "string") {
322323
continue;
323324
}
@@ -2633,6 +2634,17 @@ export class SentryApiService {
26332634
},
26342635
opts?: RequestOptions,
26352636
): Promise<ReplayList> {
2637+
if (statsPeriod && (start || end)) {
2638+
throw new ApiValidationError(
2639+
"Cannot use both statsPeriod and start/end parameters",
2640+
);
2641+
}
2642+
if ((start && !end) || (!start && end)) {
2643+
throw new ApiValidationError(
2644+
"Both start and end parameters must be provided together",
2645+
);
2646+
}
2647+
26362648
const result = await sdkListAnOrganizationSReplays({
26372649
...this.getSdkConfig(opts),
26382650
path: { organization_id_or_slug: organizationSlug },
@@ -2643,8 +2655,7 @@ export class SentryApiService {
26432655
statsPeriod,
26442656
start,
26452657
end,
2646-
// SDK types environment as a single string
2647-
environment: Array.isArray(environment) ? environment[0] : environment,
2658+
environment,
26482659
...(projectId ? { project: [Number(projectId)] } : {}),
26492660
// SDK types field as a strict enum — the API accepts arbitrary strings at runtime
26502661
...(fields?.length
@@ -2655,7 +2666,7 @@ export class SentryApiService {
26552666
}
26562667
: {}),
26572668
},
2658-
});
2669+
} as Parameters<typeof sdkListAnOrganizationSReplays>[0]);
26592670
const data = this.unwrapSdkResult(result, "searchReplays");
26602671

26612672
return ReplayListResponseSchema.parse(data).data;

packages/mcp-core/src/tools/support/search-events/utils.test.ts

Lines changed: 7 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -258,11 +258,12 @@ describe("fetchCustomAttributes", () => {
258258
),
259259
);
260260

261-
// Should throw ApiPermissionError with the improved error message
262-
// The SDK wraps errors with context, but the detail message is preserved
261+
// Should throw ApiPermissionError with the improved error message.
263262
await expect(
264263
fetchCustomAttributes(apiService, "test-org", "spans"),
265-
).rejects.toThrow("listTraceItemAttributes(string): 403 Forbidden");
264+
).rejects.toThrow(
265+
"You do not have access to query across multiple projects. Please select a project for your query.",
266+
);
266267

267268
// Should NOT log - the caller handles logging
268269
});
@@ -280,11 +281,10 @@ describe("fetchCustomAttributes", () => {
280281
),
281282
);
282283

283-
// Should throw ApiPermissionError with the raw error message
284-
// The SDK wraps errors with context prefix
284+
// Should throw ApiPermissionError with the API detail message.
285285
await expect(
286286
fetchCustomAttributes(apiService, "test-org", "logs", "project-123"),
287-
).rejects.toThrow("listTraceItemAttributes(string): 403 Forbidden");
287+
).rejects.toThrow("Permission denied");
288288
});
289289

290290
it("should throw 404 errors for errors dataset", async () => {
@@ -326,9 +326,7 @@ describe("fetchCustomAttributes", () => {
326326
).catch((e) => e);
327327

328328
expect(error).toBeInstanceOf(Error);
329-
expect(error.message).toBe(
330-
"listTraceItemAttributes(string): 500 Internal Server Error",
331-
);
329+
expect(error.message).toBe("Internal server error");
332330
});
333331

334332
it("should re-throw 502 errors", async () => {

0 commit comments

Comments
 (0)