|
| 1 | +const authToken = process.env.E2E_TEST_AUTH_TOKEN; |
| 2 | +const sentryTestOrgSlug = process.env.E2E_TEST_SENTRY_ORG_SLUG; |
| 3 | + |
| 4 | +/** |
| 5 | + * Spans only become queryable once they have made it through to EAP, which takes |
| 6 | + * noticeably longer than the error pipeline (~2min vs ~20s when this was measured). |
| 7 | + */ |
| 8 | +export const EVENT_POLLING_OPTIONS = { timeout: 180_000, intervals: [5_000] }; |
| 9 | + |
| 10 | +/** |
| 11 | + * A node of the span tree returned by the organization trace endpoint. Spans, errors and |
| 12 | + * occurrences all share this shape and are discriminated by `event_type`. |
| 13 | + */ |
| 14 | +export interface TraceItem { |
| 15 | + event_id?: string; |
| 16 | + /** On spans this is the event id of the transaction the span belongs to. */ |
| 17 | + transaction_id?: string; |
| 18 | + event_type?: 'span' | 'error' | 'occurrence' | 'uptime_check'; |
| 19 | + op?: string; |
| 20 | + is_transaction?: boolean; |
| 21 | + children?: TraceItem[]; |
| 22 | + errors?: TraceItem[]; |
| 23 | + occurrences?: TraceItem[]; |
| 24 | +} |
| 25 | + |
| 26 | +export async function fetchTrace(traceId: string): Promise<TraceItem[]> { |
| 27 | + const response = await fetch( |
| 28 | + `https://sentry.io/api/0/organizations/${sentryTestOrgSlug}/trace/${traceId}/?statsPeriod=1h`, |
| 29 | + { headers: { Authorization: `Bearer ${authToken}` } }, |
| 30 | + ); |
| 31 | + |
| 32 | + // The trace endpoint is org scoped, so the auth token needs `org:read` on top of the |
| 33 | + // project scopes the other assertions rely on. That never resolves by waiting, so fail |
| 34 | + // loudly instead of polling until the timeout and reporting it as a missing event. |
| 35 | + if (response.status === 401 || response.status === 403) { |
| 36 | + throw new Error( |
| 37 | + `Trace lookup for ${traceId} was rejected with ${response.status}: ${await response.text()}. ` + |
| 38 | + 'E2E_TEST_AUTH_TOKEN needs the `org:read` scope.', |
| 39 | + ); |
| 40 | + } |
| 41 | + |
| 42 | + // Empty traces and the occasional rate limit are expected while polling, so treat anything |
| 43 | + // else that is not a success as "not there yet" -- but log it, since a rejected request and |
| 44 | + // a trace that has not landed are otherwise indistinguishable. |
| 45 | + if (!response.ok) { |
| 46 | + console.log(`Trace lookup for ${traceId} returned ${response.status}: ${await response.text()}`); |
| 47 | + return []; |
| 48 | + } |
| 49 | + |
| 50 | + return await response.json(); |
| 51 | +} |
| 52 | + |
| 53 | +/** |
| 54 | + * Errors attach to whichever span was active when they were captured, and relocate from the |
| 55 | + * top level into that span once it lands, so a given event can surface at any depth. |
| 56 | + */ |
| 57 | +export function flattenTrace(items: TraceItem[]): TraceItem[] { |
| 58 | + return items.flatMap(item => [ |
| 59 | + item, |
| 60 | + ...flattenTrace(item.children ?? []), |
| 61 | + ...flattenTrace(item.errors ?? []), |
| 62 | + ...flattenTrace(item.occurrences ?? []), |
| 63 | + ]); |
| 64 | +} |
| 65 | + |
| 66 | +export async function findErrorInTrace(traceId: string, eventId: string): Promise<TraceItem | undefined> { |
| 67 | + return flattenTrace(await fetchTrace(traceId)).find(item => item.event_type === 'error' && item.event_id === eventId); |
| 68 | +} |
| 69 | + |
| 70 | +export async function findTransactionInTrace(traceId: string, eventId: string): Promise<TraceItem | undefined> { |
| 71 | + return flattenTrace(await fetchTrace(traceId)).find(item => item.is_transaction && item.transaction_id === eventId); |
| 72 | +} |
0 commit comments