Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -197,7 +197,7 @@ Edit an issue alert rule
| `triggers` | object \| null | Conditions on which the alert triggers |
| `actionFilters` | array \| null | Filters to run before actions fire, each with the actions to fire |
| `environment` | string \| null | Name of the environment the alert is limited to. Null for all environments |
| `config` | unknown | Configuration of the alert, such as frequency in minutes |
| `config` | object | Configuration of the alert, such as frequency in minutes |
| `detectorIds` | array \| null | IDs of the monitors connected to the alert |
| `enabled` | boolean | Whether the alert is enabled |
| `lastTriggered` | string \| null | When the alert last fired (ISO 8601) |
Expand Down Expand Up @@ -402,7 +402,7 @@ Edit a metric alert rule
| `dateUpdated` | string | When the monitor was last updated (ISO 8601) |
| `dataSources` | array \| null | Data sources the monitor evaluates, such as a query subscription |
| `conditionGroup` | object \| null | Conditions that decide the monitor's status |
| `config` | object \| unknown | |
| `config` | object | |
| `enabled` | boolean | Whether the monitor is enabled |
| `org` | string | Organization slug |
| `status` | string | Rule status (active, disabled) |
Expand Down
1 change: 1 addition & 0 deletions packages/cli/script/lib/output-schema-fields.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ const VALIBOT_TYPE_MAP: Record<string, string> = {
boolean: "boolean",
object: "object",
loose_object: "object",
strict_object: "object",
array: "array",
record: "object",
null: "null",
Expand Down
23 changes: 20 additions & 3 deletions packages/cli/src/lib/api/issue-app-links.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import {
array,
boolean,
type InferOutput,
looseObject,
nullish,
number,
object,
Expand All @@ -31,6 +32,7 @@ import {
string,
tuple,
union,
unknown,
} from "valibot";
import { ApiError, ValidationError } from "../errors.js";
import { resolveOrgRegion } from "../region.js";
Expand Down Expand Up @@ -73,6 +75,21 @@ type Choice = InferOutput<typeof ChoiceSchema>;
type Field = InferOutput<typeof FieldSchema>;
type LinkForm = InferOutput<typeof LinkFormSchema>;

/**
* Sentry App components, with each app's `schema` left unchecked.
*
* `@sentry/api` types `schema` as the UI element union Sentry checks apps
* against when they are saved. One installed app whose stored schema doesn't
* match it would fail the whole page and hide every other app's form, so only
* the selected app's link form is checked, against {@link LinkFormSchema}.
*/
const ComponentsSchema = array(
object({
...vListOrganizationSentryAppComponentsResponse.item.entries,
schema: looseObject({ link: optional(unknown()) }),
}),
);

/** Inputs for a read-only preflight of the app's existing-issue link action. */
export type ResolveAppIssueLinkOptions = {
/** Organization containing the Sentry issue and app installation. */
Expand Down Expand Up @@ -296,7 +313,7 @@ async function getLinkForm(
});
return unwrapPaginatedResult(result, "Failed to list app components");
},
vListOrganizationSentryAppComponentsResponse,
ComponentsSchema,
"listing Sentry App components",
);
const matches = components.filter(
Expand All @@ -318,8 +335,8 @@ async function getLinkForm(
JSON.stringify(component.error),
);
}
// The component schema is typed per component type, but its form fields
// are app-defined, so validate the link form here.
// ComponentsSchema leaves each app's schema unchecked, so validate only
// this app's link form here.
const { schema } = component;
const form = safeParse(
LinkFormSchema,
Expand Down
23 changes: 21 additions & 2 deletions packages/cli/src/types/sentry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -388,8 +388,27 @@ export type ReplayContext = {
[key: string]: unknown;
};

/** High-level metadata returned by the organization trace-meta endpoint. */
export const TraceMetaSchema = vOrganizationTraceMetaResponse;
const sdkTraceMeta = vOrganizationTraceMetaResponse.entries;
const sdkTransactionChildCount =
sdkTraceMeta.transactionChildCountMap.item.entries;

/**
* High-level metadata returned by the organization trace-meta endpoint.
*
* Built from the `@sentry/api` schema. The spec says `transaction.event_id` is
* a string, but spans that belong to no transaction are counted under null.
*/
export const TraceMetaSchema = object({
...sdkTraceMeta,
transactionChildCountMap: array(
object({
...sdkTransactionChildCount,
"transaction.event_id": nullable(
sdkTransactionChildCount["transaction.event_id"],
),
}),
),
});

export type TraceMeta = InferOutput<typeof TraceMetaSchema>;

Expand Down
22 changes: 22 additions & 0 deletions packages/cli/test/lib/api/issue-app-links.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ let calls: Request[];
let links: AppIssueLink[];
let choices: [string, string][];
let form: unknown;
let otherComponents: unknown[];
let installation: typeof INSTALLATION;
let actionStatus: number;
let actionLink: AppIssueLink;
Expand All @@ -86,6 +87,7 @@ beforeEach(async () => {
links = [];
choices = [["linear-uuid", "ENG-42: Fix the crash"]];
form = FORM;
otherComponents = [];
installation = INSTALLATION;
actionStatus = 201;
actionLink = LINK;
Expand All @@ -102,6 +104,7 @@ beforeEach(async () => {
if (path.endsWith("/sentry-app-components/")) {
return json([
{ ...COMPONENT, schema: { ...COMPONENT.schema, link: form } },
...otherComponents,
]);
}
if (path.endsWith("/external-requests/")) {
Expand Down Expand Up @@ -416,6 +419,25 @@ describe("app issue-link action", () => {
expect(writes()).toHaveLength(0);
});

test("ignores another app's component schema that the API spec doesn't describe", async () => {
otherComponents = [
{
...COMPONENT,
uuid: "5e0b7d2c-8f4a-4c1e-a3b6-9d2f7e1c0a48",
sentryApp: {
uuid: "6b1f8e3d-2c5a-4d7b-9e0f-3a8c1d6b2e74",
slug: "legacy-tracker",
name: "Legacy Tracker",
avatars: [],
},
// No `create` form, and a link URI without the leading slash.
schema: { type: "issue-link", link: { uri: "link" } },
},
];
const prepared = await resolveAppIssueLink(OPTIONS);
expect(prepared.fields).toEqual({ issueId: "linear-uuid" });
});

test("does not allow user fields to override the action URI or target", async () => {
await expect(
resolveAppIssueLink({ ...OPTIONS, fields: { uri: "/create" } }),
Expand Down
51 changes: 50 additions & 1 deletion packages/cli/test/lib/api/traces.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
/**
* Tests for the traces API helpers (getDetailedTrace, listTransactions, listSpans).
* Tests for the traces API helpers (getDetailedTrace, getTraceMeta,
* listTransactions, listSpans).
*
* Verifies URL construction, query parameter encoding, schema validation,
* pagination cursor extraction, auto-pagination, and the 14d→90d empty-trace retry.
Expand All @@ -10,6 +11,7 @@ import {
fetchMultiSpanDetails,
getDetailedTrace,
getSpanDetails,
getTraceMeta,
listSpans,
listTransactions,
} from "../../../src/lib/api/traces.js";
Expand Down Expand Up @@ -875,3 +877,50 @@ describe("getDetailedTrace", () => {
}
});
});

// ---------------------------------------------------------------------------
// getTraceMeta
// ---------------------------------------------------------------------------

describe("getTraceMeta", () => {
useTestConfigDir("traces-meta-test-");

let originalFetch: typeof globalThis.fetch;

beforeEach(() => {
originalFetch = globalThis.fetch;
});

afterEach(() => {
globalThis.fetch = originalFetch;
});

test("accepts spans counted under a null transaction.event_id", async () => {
const transactionChildCountMap = [
{
"transaction.event_id": "0daf40dc453a429c8c57e4c215c4e82c",
"count()": 10,
},
{ "transaction.event_id": null, "count()": 15 },
];
globalThis.fetch = mockFetch(
async () =>
new Response(
JSON.stringify({
errorsCount: 2,
logsCount: 3,
metricsCount: 0,
performanceIssuesCount: 1,
spansCount: 25,
transactionChildCountMap,
spansCountMap: { "http.client": 25 },
}),
{ status: 200, headers: { "Content-Type": "application/json" } },
),
);

const meta = await getTraceMeta("my-org", "abc123def456");

expect(meta.transactionChildCountMap).toEqual(transactionChildCountMap);
});
});
18 changes: 18 additions & 0 deletions packages/cli/test/script/output-schema-fields.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,11 @@ import {
object,
optional,
pipe,
record,
strictObject,
string,
union,
unknown,
} from "valibot";
import { describe, expect, test } from "vitest";
import {
Expand Down Expand Up @@ -34,6 +38,20 @@ describe("extractSchemaFields", () => {
});
});

test("reports strict objects as objects", () => {
// `@sentry/api` 1.52.0 emits `strictObject` for objects whose spec sets
// `additionalProperties: false`, often as one member of a union.
const [field] = extractSchemaFields(
object({
config: union([
record(string(), unknown()),
strictObject({ frequency: optional(string()) }),
]),
}),
);
expect(field?.type).toBe("object");
});

test("prefers a description the CLI adds over the API's", () => {
const [field] = extractSchemaFields(
object({ formatted: pipe(apiField, description("CLI description")) }),
Expand Down
Loading