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 @@ -173,11 +173,11 @@ Show the top values of an issue tag

| Field | Type | Description |
|-------|------|-------------|
| `uniqueValues` | number \| null | |
| `uniqueValues` | number \| null | Number of distinct values seen for the tag key |
| `totalValues` | number \| null | Total retained events, including missing tag values; not a lifetime count |
| `topValues` | array \| null | Up to 9 values ordered by count, with nullable counts and first/last seen timestamps |
| `key` | string | |
| `name` | string | |
| `key` | string | Tag key, with the `sentry:` prefix removed from built-in keys |
| `name` | string | Display label of the tag key |

**Examples:**

Expand Down
102 changes: 44 additions & 58 deletions packages/cli/src/lib/api/conversations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,17 @@
* Functions for listing and retrieving agent conversation data from the Sentry
* Explore agent conversations endpoints.
*
* Both endpoints are in `@sentry/api`. The list still goes through
* `apiRequestToRegion` because its generated query has no `sort`. The details
* call uses the generated operation; its spans are untyped records there, so
* the response is still checked with `AgentConversationDetailsSchema`.
* Both endpoints use their generated `@sentry/api` operations, and their
* responses are still checked with the CLI's own schemas
* (`ConversationListItemSchema`, `AgentConversationDetailsSchema`).
* Pagination follows the `Link` header.
*/

import { retrieveOrganizationAiConversation } from "@sentry/api";
import {
type ListOrganizationAiConversationsData,
listOrganizationAiConversations,
retrieveOrganizationAiConversation,
} from "@sentry/api";
import { array } from "valibot";

import {
Expand All @@ -25,21 +28,30 @@ import {

import { logger } from "../logger.js";
import { resolveOrgRegion } from "../region.js";
import { getSdkClientConfig } from "../sentry-sdk-client.js";

import {
API_MAX_PER_PAGE,
apiRequestToRegion,
getOrgSdkConfig,
MAX_PAGINATION_PAGES,
type PaginatedResponse,
paginate,
parseLinkHeader,
unwrapPaginatedResult,
validateResponse,
} from "./infrastructure.js";

const log = logger.withTag("api.conversations");

type ConversationListOptions = {
query?: string;
sort?: NonNullable<ListOrganizationAiConversationsData["query"]>["sort"];
cursor?: string;
statsPeriod?: string;
start?: string;
end?: string;
project?: string;
};

/**
* Fetch a single page of conversations from the agent conversations endpoint.
*
Expand All @@ -49,51 +61,34 @@ const log = logger.withTag("api.conversations");
async function fetchConversationsPage(
regionUrl: string,
orgSlug: string,
options: {
query?: string;
sort?: string;
cursor?: string;
statsPeriod?: string;
start?: string;
end?: string;
project?: string;
},
options: ConversationListOptions,
perPage: number,
): Promise<PaginatedResponse<ConversationListItem[]>> {
const params: Record<string, string> = {
per_page: String(perPage),
};
if (options.statsPeriod) {
params.statsPeriod = options.statsPeriod;
}
if (options.start) {
params.start = options.start;
}
if (options.end) {
params.end = options.end;
}
if (options.cursor) {
params.cursor = options.cursor;
}
if (options.query) {
params.query = options.query;
}
if (options.sort) {
params.sort = options.sort;
}
if (options.project) {
params.project = options.project;
}

const { data, headers } = await apiRequestToRegion<ConversationListItem[]>(
regionUrl,
const result = await listOrganizationAiConversations({
...getSdkClientConfig(regionUrl),
path: { organization_id_or_slug: orgSlug },
query: {
per_page: perPage,
statsPeriod: options.statsPeriod || undefined,
start: options.start || undefined,
end: options.end || undefined,
cursor: options.cursor || undefined,
query: options.query || undefined,
sort: options.sort,
project: options.project ? [options.project] : undefined,
},
});
const page = unwrapPaginatedResult<unknown>(
result,
"Failed to list agent conversations",
);
const data = validateResponse(
array(ConversationListItemSchema),
page.data,
`/organizations/${orgSlug}/agents/conversations/`,
{ params, schema: array(ConversationListItemSchema) },
page.response?.status ?? 0,
);

const { nextCursor } = parseLinkHeader(headers.get("link") ?? null);

return { data, nextCursor };
return { data, nextCursor: page.nextCursor };
}

/**
Expand All @@ -108,16 +103,7 @@ async function fetchConversationsPage(
*/
export async function listConversations(
orgSlug: string,
options: {
query?: string;
sort?: string;
limit?: number;
cursor?: string;
statsPeriod?: string;
start?: string;
end?: string;
project?: string;
} = {},
options: ConversationListOptions & { limit?: number } = {},
): Promise<PaginatedResponse<ConversationListItem[]>> {
const regionUrl = await resolveOrgRegion(orgSlug);
return await paginate(options, (perPage, cursor) =>
Expand Down
56 changes: 31 additions & 25 deletions packages/cli/src/lib/api/dashboards.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,9 @@ import {
createOrganizationDashboard,
getOrganizationDashboard,
listOrganizationDashboards,
type ListOrganizationEventsTimeseriesData,
listOrganizationEvents,
listOrganizationEventsTimeseries,
updateOrganizationDashboard,
} from "@sentry/api";
import { resolveEventsSort } from "@sentry/toolkit-core/events-sort";
Expand Down Expand Up @@ -466,12 +468,15 @@ function buildTopEventsParams(
columns: string[],
aggregates: string[],
orderby: string | undefined,
): Record<string, string | string[] | number | undefined> {
): Pick<
EventsTimeseriesQuery,
"groupBy" | "sort" | "topEvents" | "excludeOther"
> {
const { fields, sort } = resolveEventsSort({
fields: [...columns, ...aggregates],
sort: orderby,
});
const params: Record<string, string | string[] | number | undefined> = {
const params: ReturnType<typeof buildTopEventsParams> = {
groupBy: fields,
sort,
topEvents: widget.limit ?? 5,
Expand Down Expand Up @@ -510,6 +515,8 @@ type WidgetQueryParams = {
options?: WidgetQueryOptions;
};

type EventsTimeseriesQuery = ListOrganizationEventsTimeseriesData["query"];

/** Widget query params once the widget's type has resolved to a dataset. */
type DatasetWidgetQueryParams = WidgetQueryParams & { dataset: EventsDataset };

Expand All @@ -533,30 +540,29 @@ async function queryWidgetTimeseries(
const aggregates = query.aggregates ?? [];
const columns = query.columns ?? [];

const reqParams: Record<string, string | string[] | number | undefined> = {
yAxis: aggregates,
query: query.conditions || undefined,
dataset,
statsPeriod,
start,
end,
interval: computeOptimalInterval(statsPeriod, widget, periodSeconds),
environment: options.environment,
project: options.project?.map(String),
};

// Group-by columns enable topEvents mode
if (columns.length > 0) {
Object.assign(
reqParams,
buildTopEventsParams(widget, columns, aggregates, query.orderby),
);
}

const { data } = await apiRequestToRegion<EventsTimeseriesResponse>(
regionUrl,
const result = await listOrganizationEventsTimeseries({
...getSdkClientConfig(regionUrl),
path: { organization_id_or_slug: orgSlug },
query: {
yAxis: aggregates,
query: query.conditions || undefined,
dataset,
statsPeriod,
start,
end,
interval: computeOptimalInterval(statsPeriod, widget, periodSeconds),
environment: options.environment,
project: options.project,
// Group-by columns enable topEvents mode
...(columns.length > 0 &&
buildTopEventsParams(widget, columns, aggregates, query.orderby)),
},
});
const data = unwrapValidatedResult(
result,
"Failed to query widget data",
EventsTimeseriesResponseSchema,
`/organizations/${orgSlug}/events-timeseries/`,
{ params: reqParams, schema: EventsTimeseriesResponseSchema },
);

const parsed = parseEventsTimeseriesResponse(data, aggregates);
Expand Down
45 changes: 23 additions & 22 deletions packages/cli/src/lib/api/explore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@

import {
listOrganizationEvents,
listOrganizationEventsTimeseries,
type OrganizationEventsTimeseriesResponse,
} from "@sentry/api";

Expand All @@ -18,11 +19,12 @@ import { resolveOrgRegion } from "../region.js";
import { getSdkClientConfig } from "../sentry-sdk-client.js";
import {
API_MAX_PER_PAGE,
apiRequestToRegion,
getOrgSdkConfig,
MAX_PAGINATION_PAGES,
type PaginatedResponse,
paginate,
unwrapPaginatedResult,
unwrapResult,
validateResponse,
} from "./infrastructure.js";

Expand Down Expand Up @@ -93,35 +95,34 @@ export type EventsTimeSeriesResponse = Omit<
/**
* Query aggregate values over time without grouping or pagination.
*
* Uses the same regional transport as `queryEvents` because the generated
* SDK input types do not support repeated `yAxis` or duration-string intervals.
* Returns the complete API response, including sampling and ingestion metadata.
*/
export async function queryEventsTimeSeries(
orgSlug: string,
options: ExploreTimeSeriesQueryOptions,
): Promise<EventsTimeSeriesResponse> {
const regionUrl = await resolveOrgRegion(orgSlug);
const { data } = await apiRequestToRegion<EventsTimeSeriesResponse>(
regionUrl,
`/organizations/${orgSlug}/events-timeseries/`,
{
params: {
dataset: options.dataset ?? "errors",
yAxis: options.fields,
query: options.query || undefined,
project: options.projectId,
interval: options.interval,
statsPeriod:
options.start || options.end
? undefined
: (options.statsPeriod ?? "24h"),
start: options.start,
end: options.end,
},
const result = await listOrganizationEventsTimeseries({
...(await getOrgSdkConfig(orgSlug)),
path: { organization_id_or_slug: orgSlug },
query: {
dataset: options.dataset ?? "errors",
yAxis: options.fields,
query: options.query || undefined,
project:
options.projectId === undefined ? undefined : [options.projectId],
interval: options.interval,
statsPeriod:
options.start || options.end
? undefined
: (options.statsPeriod ?? "24h"),
start: options.start,
end: options.end,
},
});
return unwrapResult<EventsTimeSeriesResponse>(
result,
"Failed to query events time series",
);
return data;
}

/**
Expand Down
Loading
Loading