From 693ad00c8457bd07ca9000a6162cf024dcdc4fab Mon Sep 17 00:00:00 2001 From: "sentry-junior[bot]" <264270552+sentry-junior[bot]@users.noreply.github.com> Date: Wed, 28 Oct 2026 23:55:39 +0000 Subject: [PATCH] feat(sdk): Default collapse=adjacentEvents on getProjectEvent getsentry/sentry#129123 documented the collapse query parameter on the project event-details endpoint. Apply the same client-side default as getOrganizationIssueEvent so the SDK skips the next/previous-event lookups unless the caller passes collapse; collapse: [] restores them. Co-Authored-By: BYK --- README.md | 1 + lib/query-defaults.mjs | 17 +++++++++++------ test/client-types.ts | 7 +++++++ test/query-defaults.test.ts | 28 ++++++++++++++++++++++++---- 4 files changed, 43 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index 8f3ec87..8d67676 100644 --- a/README.md +++ b/README.md @@ -90,6 +90,7 @@ A few operations send a query parameter by default to skip expensive work most c | Operation | Default | Effect | | --- | --- | --- | | `getOrganizationIssueEvent` | `collapse: ["adjacentEvents"]` | Skips the next/previous event lookups; `nextEventID` and `previousEventID` are `null` | +| `getProjectEvent` | `collapse: ["adjacentEvents"]` | Skips the next/previous event lookups; `nextEventID` and `previousEventID` are `null` | The default applies only when the parameter is omitted or `undefined`. Any value you pass replaces it, and an empty list sends nothing: diff --git a/lib/query-defaults.mjs b/lib/query-defaults.mjs index 279953e..00570f2 100644 --- a/lib/query-defaults.mjs +++ b/lib/query-defaults.mjs @@ -9,17 +9,22 @@ */ import ts from "typescript"; +// Skips the next/previous-event Snuba queries. No known SDK consumer reads the +// adjacent IDs; pass `collapse: []` to get them. +const SKIP_ADJACENT_EVENTS = { + query: { collapse: ["adjacentEvents"] }, + effect: "skips looking up the neighboring events (`nextEventID` and `previousEventID` are `null`)", +}; + /** * Keyed by `${METHOD} ${path}`. Each value must be in the parameter's enum. * `effect` completes "By default, the SDK sends `…`, which …" in the JSDoc. */ export const QUERY_DEFAULTS = new Map([ - // getsentry/sentry#129094: skips the next/previous-event Snuba queries. No - // known SDK consumer reads the adjacent IDs; pass `collapse: []` to get them. - ["GET /api/0/organizations/{organization_id_or_slug}/issues/{issue_id}/events/{event_id}/", { - query: { collapse: ["adjacentEvents"] }, - effect: "skips looking up the neighboring events (`nextEventID` and `previousEventID` are `null`)", - }], + // getsentry/sentry#129094 + ["GET /api/0/organizations/{organization_id_or_slug}/issues/{issue_id}/events/{event_id}/", SKIP_ADJACENT_EVENTS], + // getsentry/sentry#129123 + ["GET /api/0/projects/{organization_id_or_slug}/{project_id_or_slug}/events/{event_id}/", SKIP_ADJACENT_EVENTS], ]); /** Fail the build when the schema no longer documents a configured default. */ diff --git a/test/client-types.ts b/test/client-types.ts index 3bba6cc..bda1c56 100644 --- a/test/client-types.ts +++ b/test/client-types.ts @@ -60,6 +60,13 @@ async function operationArguments(sentry: SentryClient) { await sentry.getOrganizationIssueEvent({ ...event, query: { collapse: [] } }); // @ts-expect-error Query defaults do not widen the documented enum. await sentry.getOrganizationIssueEvent({ ...event, query: { collapse: ["everything"] } }); + const projectEvent = { + path: { organization_id_or_slug: "example", project_id_or_slug: "example", event_id: "a".repeat(32) }, + }; + await sentry.getProjectEvent(projectEvent); + await sentry.getProjectEvent({ ...projectEvent, query: { collapse: [] } }); + // @ts-expect-error Query defaults do not widen the documented enum. + await sentry.getProjectEvent({ ...projectEvent, query: { collapse: ["everything"] } }); } async function responseAndThrowingTypes(sentry: SentryClient, throwOnError: boolean) { diff --git a/test/query-defaults.test.ts b/test/query-defaults.test.ts index 5121de4..980aa70 100644 --- a/test/query-defaults.test.ts +++ b/test/query-defaults.test.ts @@ -2,7 +2,9 @@ import { describe, expect, test } from "bun:test"; import { createSentryClient, getOrganizationIssueEvent, + getProjectEvent, narrowError_getOrganizationIssueEvent, + narrowError_getProjectEvent, type Config, type GetOrganizationIssueEventQueryCollapse, } from "@sentry/api"; @@ -22,6 +24,9 @@ function recordingClient() { } const event = { path: { organization_id_or_slug: "org", issue_id: "1", event_id: "latest" as const } }; +const projectEvent = { + path: { organization_id_or_slug: "org", project_id_or_slug: "project", event_id: "a".repeat(32) }, +}; // The server also honors values the schema does not document yet, such as // fullRelease. JavaScript callers can pass them; TypeScript callers need a cast. const fullRelease = "fullRelease" as GetOrganizationIssueEventQueryCollapse; @@ -47,6 +52,24 @@ describe("client-side query defaults", () => { ]); }); + test("skips adjacent-event lookups on project events unless the caller sets collapse", async () => { + const { queries, fetch, sentry } = recordingClient(); + + await sentry.getProjectEvent(projectEvent); + await sentry.getProjectEvent({ ...projectEvent, query: { environment: ["prod"] } }); + await getProjectEvent({ ...projectEvent, baseUrl: "https://example.test", fetch }); + await narrowError_getProjectEvent({ ...projectEvent, client: sentry.client }); + await sentry.getProjectEvent({ ...projectEvent, query: { collapse: [] } }); + + expect(queries).toEqual([ + "?collapse=adjacentEvents", + "?environment=prod&collapse=adjacentEvents", + "?collapse=adjacentEvents", + "?collapse=adjacentEvents", + "", + ]); + }); + test("lets callers replace, combine, or opt out of the default", async () => { const { queries, sentry } = recordingClient(); const query = { collapse: [] as GetOrganizationIssueEventQueryCollapse[], environment: ["prod"] }; @@ -67,13 +90,10 @@ describe("client-side query defaults", () => { test("leaves other operations unchanged", async () => { const { queries, sentry } = recordingClient(); - await sentry.getProjectEvent({ - path: { organization_id_or_slug: "org", project_id_or_slug: "project", event_id: "a".repeat(32) }, - }); await sentry.listOrganizationIssueEvents({ path: { organization_id_or_slug: "org", issue_id: "1" } }); await sentry.listOrganizations({ query: { owner: true } }); - expect(queries).toEqual(["", "", "?owner=true"]); + expect(queries).toEqual(["", "?owner=true"]); }); });