-
-
Notifications
You must be signed in to change notification settings - Fork 10
Expand file tree
/
Copy pathevents.ts
More file actions
261 lines (232 loc) · 7.56 KB
/
Copy pathevents.ts
File metadata and controls
261 lines (232 loc) · 7.56 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
/**
* Event API functions
*
* Functions for retrieving, listing, and resolving Sentry events.
*/
import {
listAnIssue_sEvents,
retrieveAnEventForAProject,
retrieveAnIssueEvent,
resolveAnEventId as sdkResolveAnEventId,
} from "@sentry/api";
import pLimit from "p-limit";
import type { IssueEvent, SentryEvent } from "../../types/index.js";
import { ApiError, AuthError } from "../errors.js";
import {
API_MAX_PER_PAGE,
getOrgSdkConfig,
MAX_PAGINATION_PAGES,
ORG_FANOUT_CONCURRENCY,
type PaginatedResponse,
unwrapPaginatedResult,
unwrapResult,
} from "./infrastructure.js";
import { listOrganizations } from "./organizations.js";
/**
* Get the latest event for an issue.
* Uses region-aware routing for multi-region support.
*
* @param orgSlug - Organization slug (required for multi-region routing)
* @param issueId - Issue ID (numeric)
*/
export async function getLatestEvent(
orgSlug: string,
issueId: string
): Promise<SentryEvent> {
const config = await getOrgSdkConfig(orgSlug);
const result = await retrieveAnIssueEvent({
...config,
path: {
organization_id_or_slug: orgSlug,
issue_id: Number(issueId),
event_id: "latest",
},
});
const data = unwrapResult(result, "Failed to get latest event");
return data as unknown as SentryEvent;
}
/**
* Get a specific event by ID.
* Uses region-aware routing for multi-region support.
*/
export async function getEvent(
orgSlug: string,
projectSlug: string,
eventId: string
): Promise<SentryEvent> {
const config = await getOrgSdkConfig(orgSlug);
const result = await retrieveAnEventForAProject({
...config,
path: {
organization_id_or_slug: orgSlug,
project_id_or_slug: projectSlug,
event_id: eventId,
},
});
const data = unwrapResult(result, "Failed to get event");
return data as unknown as SentryEvent;
}
/**
* Result of resolving an event ID to an org and project.
* Includes the full event so the caller can avoid a second API call.
*/
export type ResolvedEvent = {
org: string;
project: string;
event: SentryEvent;
};
/**
* Resolve an event ID to its org and project using the
* `/organizations/{org}/eventids/{event_id}/` endpoint.
*
* Returns the resolved org, project, and full event on success,
* or null if the event is not found in the given org.
*/
export async function resolveEventInOrg(
orgSlug: string,
eventId: string
): Promise<ResolvedEvent | null> {
const config = await getOrgSdkConfig(orgSlug);
const result = await sdkResolveAnEventId({
...config,
path: { organization_id_or_slug: orgSlug, event_id: eventId },
});
try {
const data = unwrapResult(result, "Failed to resolve event ID");
return {
org: data.organizationSlug,
project: data.projectSlug,
event: data.event as unknown as SentryEvent,
};
} catch (error) {
// 404 means the event doesn't exist in this org — not an error
if (error instanceof ApiError && error.status === 404) {
return null;
}
throw error;
}
}
/** Options for {@link findEventAcrossOrgs}. */
export type FindEventAcrossOrgsOptions = {
/** Org slugs to skip (already searched by the caller). */
excludeOrgs?: string[];
};
/**
* Search for an event across all accessible organizations by event ID.
*
* Fans out to every org in parallel using the eventids resolution endpoint.
* Returns the first match found, or null if the event is not accessible.
*
* @param eventId - The event ID (UUID) to look up
* @param options - Optional settings (e.g., orgs to skip)
*/
export async function findEventAcrossOrgs(
eventId: string,
options?: FindEventAcrossOrgsOptions
): Promise<ResolvedEvent | null> {
const excludeSet = options?.excludeOrgs
? new Set(options.excludeOrgs)
: undefined;
const allOrgs = await listOrganizations();
const orgs = excludeSet
? allOrgs.filter((o) => !excludeSet.has(o.slug))
: allOrgs;
const limit = pLimit(ORG_FANOUT_CONCURRENCY);
const results = await Promise.allSettled(
orgs.map((org) => limit(() => resolveEventInOrg(org.slug, eventId)))
);
// First pass: return the first successful match
for (const result of results) {
if (result.status === "fulfilled" && result.value !== null) {
return result.value;
}
}
// Second pass (only reached when no org had the event): propagate
// AuthError since it indicates a global problem (expired/missing token).
// Transient per-org failures (network, 5xx) are swallowed — they are not
// global, and if the event existed in any accessible org it would have matched.
for (const result of results) {
if (result.status === "rejected" && result.reason instanceof AuthError) {
throw result.reason;
}
}
return null;
}
// ---------------------------------------------------------------------------
// Issue event listing
// ---------------------------------------------------------------------------
/** Options for {@link listIssueEvents}. */
export type ListIssueEventsOptions = {
/** Max items to return (total across all auto-paginated pages). @default 25 */
limit?: number;
/** Search query (Sentry search syntax). */
query?: string;
/** Include full event body (stacktraces, breadcrumbs). */
full?: boolean;
/** Pagination cursor from a previous response. */
cursor?: string;
/** Relative time period (e.g., "7d", "24h"). Overrides start/end on the API. */
statsPeriod?: string;
/** Absolute start datetime (ISO-8601). Mutually exclusive with statsPeriod. */
start?: string;
/** Absolute end datetime (ISO-8601). Mutually exclusive with statsPeriod. */
end?: string;
};
/**
* List events for a specific issue.
*
* Uses the SDK's `listAnIssue_sEvents` endpoint with region-aware routing.
* When `limit` exceeds {@link API_MAX_PER_PAGE} (100), auto-paginates through
* multiple API calls to fill the requested limit, bounded by {@link MAX_PAGINATION_PAGES}.
*
* @param orgSlug - Organization slug for region routing
* @param issueId - Numeric issue ID
* @param options - Query and pagination options
* @returns Paginated response with events array and optional next cursor
*/
export async function listIssueEvents(
orgSlug: string,
issueId: string,
options: ListIssueEventsOptions = {}
): Promise<PaginatedResponse<IssueEvent[]>> {
const { limit = 25, query, full, cursor, statsPeriod, start, end } = options;
const config = await getOrgSdkConfig(orgSlug);
const allEvents: IssueEvent[] = [];
let currentCursor = cursor;
let nextCursor: string | undefined;
for (let page = 0; page < MAX_PAGINATION_PAGES; page++) {
const result = await listAnIssue_sEvents({
...config,
path: {
organization_id_or_slug: orgSlug,
issue_id: Number(issueId),
},
query: {
query: query || undefined,
full,
cursor: currentCursor,
statsPeriod,
start,
end,
},
});
const paginated = unwrapPaginatedResult(
result,
"Failed to list issue events"
);
allEvents.push(...(paginated.data as IssueEvent[]));
nextCursor = paginated.nextCursor;
if (allEvents.length >= limit || !nextCursor) {
break;
}
currentCursor = nextCursor;
}
// Trim to exact limit. Unlike listIssuesAllPages (which controls per_page),
// the issue events endpoint has no per-page parameter, so the API may return
// more items than requested. When trimming, drop nextCursor — it points past
// the trimmed items and would cause the cursor stack to skip events.
if (allEvents.length > limit) {
return { data: allEvents.slice(0, limit) };
}
return { data: allEvents, nextCursor };
}