Skip to content

Commit 0224444

Browse files
authored
test(v9/e2e): Look up events via the organization trace endpoint (#24804)
Backport of: #23371 Cherry-picked from the v10 backport (#23680), which applied cleanly. The only difference to the original is formatting, since v9 still uses Prettier.
1 parent f0b295b commit 0224444

10 files changed

Lines changed: 223 additions & 149 deletions

File tree

‎dev-packages/e2e-tests/test-applications/node-express-send-to-sentry/playwright.config.mjs‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,8 @@ const expressPort = 3030;
77
*/
88
const config = {
99
testDir: './tests',
10-
/* Maximum time one test can run for. */
11-
timeout: 150_000,
10+
/* Maximum time one test can run for. Spans take ~2min to become queryable via the trace endpoint. */
11+
timeout: 210_000,
1212
expect: {
1313
/**
1414
* Maximum time expect() should wait for the condition to be met.

‎dev-packages/e2e-tests/test-applications/node-express-send-to-sentry/src/app.ts‎

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,21 @@
11
import * as Sentry from '@sentry/node';
22

33
let lastTransactionId: string | undefined;
4+
let lastTransactionTraceId: string | undefined;
5+
let lastErrorTraceId: string | undefined;
46

57
Sentry.init({
68
environment: 'qa', // dynamic sampling bias to keep transactions
79
dsn: process.env.E2E_TEST_DSN,
810
includeLocalVariables: true,
911
tracesSampleRate: 1,
12+
beforeSend(event) {
13+
lastErrorTraceId = event.contexts?.trace?.trace_id;
14+
return event;
15+
},
1016
beforeSendTransaction(event) {
1117
lastTransactionId = event.event_id;
18+
lastTransactionTraceId = event.contexts?.trace?.trace_id;
1219
return event;
1320
},
1421
});
@@ -36,6 +43,7 @@ app.get('/test-transaction', function (req, res) {
3643

3744
res.send({
3845
transactionId: lastTransactionId,
46+
traceId: lastTransactionTraceId,
3947
});
4048
});
4149
});
@@ -45,7 +53,7 @@ app.get('/test-error', async function (req, res) {
4553

4654
await Sentry.flush(2000);
4755

48-
res.send({ exceptionId });
56+
res.send({ exceptionId, traceId: lastErrorTraceId });
4957
});
5058

5159
app.get('/test-exception/:id', function (req, _res) {
Lines changed: 8 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -1,45 +1,22 @@
11
import { expect, test } from '@playwright/test';
2-
3-
const EVENT_POLLING_TIMEOUT = 90_000;
4-
5-
const authToken = process.env.E2E_TEST_AUTH_TOKEN;
6-
const sentryTestOrgSlug = process.env.E2E_TEST_SENTRY_ORG_SLUG;
7-
const sentryTestProject = process.env.E2E_TEST_SENTRY_PROJECT;
2+
import { EVENT_POLLING_OPTIONS, findErrorInTrace, findTransactionInTrace } from './utils/sentry-api';
83

94
test('Sends exception to Sentry', async ({ baseURL }) => {
105
const response = await fetch(`${baseURL}/test-error`);
11-
const { exceptionId } = await response.json();
12-
13-
const url = `https://sentry.io/api/0/projects/${sentryTestOrgSlug}/${sentryTestProject}/events/${exceptionId}/`;
6+
const { exceptionId, traceId } = await response.json();
147

15-
console.log(`Polling for error eventId: ${exceptionId}`);
8+
console.log(`Polling for error eventId: ${exceptionId} in trace: ${traceId}`);
169

17-
await expect
18-
.poll(
19-
async () => {
20-
const response = await fetch(url, { headers: { Authorization: `Bearer ${authToken}` } });
21-
return response.status;
22-
},
23-
{ timeout: EVENT_POLLING_TIMEOUT },
24-
)
25-
.toBe(200);
10+
await expect.poll(() => findErrorInTrace(traceId, exceptionId), EVENT_POLLING_OPTIONS).toBeDefined();
2611
});
2712

2813
test('Sends transaction to Sentry', async ({ baseURL }) => {
2914
const response = await fetch(`${baseURL}/test-transaction`);
30-
const { transactionId } = await response.json();
31-
32-
const url = `https://sentry.io/api/0/projects/${sentryTestOrgSlug}/${sentryTestProject}/events/${transactionId}/`;
15+
const { transactionId, traceId } = await response.json();
3316

34-
console.log(`Polling for transaction eventId: ${transactionId}`);
17+
console.log(`Polling for transaction eventId: ${transactionId} in trace: ${traceId}`);
3518

3619
await expect
37-
.poll(
38-
async () => {
39-
const response = await fetch(url, { headers: { Authorization: `Bearer ${authToken}` } });
40-
return response.status;
41-
},
42-
{ timeout: EVENT_POLLING_TIMEOUT },
43-
)
44-
.toBe(200);
20+
.poll(() => findTransactionInTrace(traceId, transactionId), EVENT_POLLING_OPTIONS)
21+
.toMatchObject({ op: 'e2e-test' });
4522
});
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
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+
}

‎dev-packages/e2e-tests/test-applications/react-send-to-sentry/playwright.config.mjs‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,8 @@ import { devices } from '@playwright/test';
55
*/
66
const config = {
77
testDir: './tests',
8-
/* Maximum time one test can run for. */
9-
timeout: 150_000,
8+
/* Maximum time one test can run for. Spans take ~2min to become queryable via the trace endpoint. */
9+
timeout: 210_000,
1010
expect: {
1111
/**
1212
* Maximum time expect() should wait for the condition to be met.
Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,11 @@
1+
interface RecordedEvent {
2+
eventId: string;
3+
traceId: string;
4+
op?: string;
5+
}
6+
17
interface Window {
2-
recordedTransactions?: string[];
3-
capturedExceptionId?: string;
8+
recordedTransactions?: RecordedEvent[];
9+
capturedException?: RecordedEvent;
410
sentryReplayId?: string;
511
}

‎dev-packages/e2e-tests/test-applications/react-send-to-sentry/src/index.tsx‎

Lines changed: 15 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -44,16 +44,22 @@ Object.defineProperty(window, 'sentryReplayId', {
4444
},
4545
});
4646

47+
// The trace id is recorded alongside the event id because events are looked up through the
48+
// organization trace endpoint, which is keyed by trace rather than by event.
4749
Sentry.addEventProcessor(event => {
48-
if (
49-
event.type === 'transaction' &&
50-
(event.contexts?.trace?.op === 'pageload' || event.contexts?.trace?.op === 'navigation')
51-
) {
52-
const eventId = event.event_id;
53-
if (eventId) {
54-
window.recordedTransactions = window.recordedTransactions || [];
55-
window.recordedTransactions.push(eventId);
56-
}
50+
const eventId = event.event_id;
51+
const traceId = event.contexts?.trace?.trace_id;
52+
const op = event.contexts?.trace?.op;
53+
54+
if (!eventId || !traceId) {
55+
return event;
56+
}
57+
58+
if (event.type === 'transaction' && (op === 'pageload' || op === 'navigation')) {
59+
window.recordedTransactions = window.recordedTransactions || [];
60+
window.recordedTransactions.push({ eventId, traceId, op });
61+
} else if (!event.type && event.exception) {
62+
window.capturedException = { eventId, traceId };
5763
}
5864

5965
return event;

‎dev-packages/e2e-tests/test-applications/react-send-to-sentry/src/pages/Index.tsx‎

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,7 @@ const Index = () => {
1010
value="Capture Exception"
1111
id="exception-button"
1212
onClick={() => {
13-
const eventId = Sentry.captureException(new Error('I am an error!'));
14-
window.capturedExceptionId = eventId;
13+
Sentry.captureException(new Error('I am an error!'));
1514
}}
1615
/>
1716
<Link to="/user/5" id="navigation">

0 commit comments

Comments
 (0)