Skip to content

Commit 57fd26b

Browse files
committed
more fixes
1 parent a7a9d8a commit 57fd26b

File tree

5 files changed

+1610
-4
lines changed

5 files changed

+1610
-4
lines changed

packages/e2e-tests/test-applications/sveltekit/playwright.config.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ const port = Number(process.env.BASE_PORT) + Number(process.env.PORT_MODULO);
1313
* See https://playwright.dev/docs/test-configuration.
1414
*/
1515
const config: PlaywrightTestConfig = {
16-
testDir: './tests',
16+
testDir: './test',
1717
/* Maximum time one test can run for. */
1818
timeout: 60 * 1000,
1919
expect: {
@@ -55,7 +55,7 @@ const config: PlaywrightTestConfig = {
5555
/* Run your local dev server before starting the tests */
5656
webServer: [
5757
{
58-
command: testEnv === 'development' ? `yarn preview --port ${port}` : `yarn preview --port ${port}`,
58+
command: testEnv === 'development' ? `yarn dev --port ${port}` : `yarn preview --port ${port}`,
5959
port,
6060
},
6161
{

packages/e2e-tests/test-applications/sveltekit/start-event-proxy.ts

Lines changed: 264 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,267 @@
1-
import { startEventProxyServer } from '../../test-utils/event-proxy-server';
1+
// This is vendored from packages/e2e-tests/test-utils/event-proxy-server.ts
2+
// This is done because otherwise we were getting `ERR_MODULE_NOT_FOUND`:
3+
//
4+
// CustomError: Cannot find module '/sentry-javascript/packages/e2e-tests/test-utils/event-proxy-server' imported from /sentry-javascript/packages/e2e-tests/test-applications/sveltekit/start-event-proxy.ts
5+
// at finalizeResolution (/sentry-javascript/packages/e2e-tests/test-applications/sveltekit/node_modules/ts-node/dist-raw/node-internal-modules-esm-resolve.js:366:11)
6+
// at moduleResolve (/sentry-javascript/packages/e2e-tests/test-applications/sveltekit/node_modules/ts-node/dist-raw/node-internal-modules-esm-resolve.js:801:10)
7+
// at Object.defaultResolve (/sentry-javascript/packages/e2e-tests/test-applications/sveltekit/node_modules/ts-node/dist-raw/node-internal-modules-esm-resolve.js:912:11)
8+
// at /sentry-javascript/packages/e2e-tests/test-applications/sveltekit/node_modules/ts-node/src/esm.ts:218:35
9+
// at entrypointFallback (/sentry-javascript/packages/e2e-tests/test-applications/sveltekit/node_modules/ts-node/src/esm.ts:168:34)
10+
// at /sentry-javascript/packages/e2e-tests/test-applications/sveltekit/node_modules/ts-node/src/esm.ts:217:14
11+
// at addShortCircuitFlag (/sentry-javascript/packages/e2e-tests/test-applications/sveltekit/node_modules/ts-node/src/esm.ts:409:21)
12+
// at resolve (/sentry-javascript/packages/e2e-tests/test-applications/sveltekit/node_modules/ts-node/src/esm.ts:197:12)
13+
// at resolve (/sentry-javascript/packages/e2e-tests/test-applications/sveltekit/node_modules/ts-node/src/child/child-loader.ts:15:39)
14+
// at nextResolve (node:internal/modules/esm/loader:163:28)
15+
16+
import type { Envelope, EnvelopeItem, Event } from '@sentry/types';
17+
import { parseEnvelope } from '@sentry/utils';
18+
import * as fs from 'fs';
19+
import * as http from 'http';
20+
import * as https from 'https';
21+
import type { AddressInfo } from 'net';
22+
import * as os from 'os';
23+
import * as path from 'path';
24+
import * as util from 'util';
25+
26+
const readFile = util.promisify(fs.readFile);
27+
const writeFile = util.promisify(fs.writeFile);
28+
29+
interface EventProxyServerOptions {
30+
/** Port to start the event proxy server at. */
31+
port: number;
32+
/** The name for the proxy server used for referencing it with listener functions */
33+
proxyServerName: string;
34+
}
35+
36+
interface SentryRequestCallbackData {
37+
envelope: Envelope;
38+
rawProxyRequestBody: string;
39+
rawSentryResponseBody: string;
40+
sentryResponseStatusCode?: number;
41+
}
42+
43+
/**
44+
* Starts an event proxy server that will proxy events to sentry when the `tunnel` option is used. Point the `tunnel`
45+
* option to this server (like this `tunnel: http://localhost:${port option}/`).
46+
*/
47+
export async function startEventProxyServer(options: EventProxyServerOptions): Promise<void> {
48+
const eventCallbackListeners: Set<(data: string) => void> = new Set();
49+
50+
const proxyServer = http.createServer((proxyRequest, proxyResponse) => {
51+
const proxyRequestChunks: Uint8Array[] = [];
52+
53+
proxyRequest.addListener('data', (chunk: Buffer) => {
54+
proxyRequestChunks.push(chunk);
55+
});
56+
57+
proxyRequest.addListener('error', err => {
58+
throw err;
59+
});
60+
61+
proxyRequest.addListener('end', () => {
62+
const proxyRequestBody = Buffer.concat(proxyRequestChunks).toString();
63+
const envelopeHeader: { dsn?: string } = JSON.parse(proxyRequestBody.split('\n')[0]);
64+
65+
if (!envelopeHeader.dsn) {
66+
throw new Error('[event-proxy-server] No dsn on envelope header. Please set tunnel option.');
67+
}
68+
69+
const { origin, pathname, host } = new URL(envelopeHeader.dsn);
70+
71+
const projectId = pathname.substring(1);
72+
const sentryIngestUrl = `${origin}/api/${projectId}/envelope/`;
73+
74+
proxyRequest.headers.host = host;
75+
76+
const sentryResponseChunks: Uint8Array[] = [];
77+
78+
const sentryRequest = https.request(
79+
sentryIngestUrl,
80+
{ headers: proxyRequest.headers, method: proxyRequest.method },
81+
sentryResponse => {
82+
sentryResponse.addListener('data', (chunk: Buffer) => {
83+
proxyResponse.write(chunk, 'binary');
84+
sentryResponseChunks.push(chunk);
85+
});
86+
87+
sentryResponse.addListener('end', () => {
88+
eventCallbackListeners.forEach(listener => {
89+
const rawProxyRequestBody = Buffer.concat(proxyRequestChunks).toString();
90+
const rawSentryResponseBody = Buffer.concat(sentryResponseChunks).toString();
91+
92+
const data: SentryRequestCallbackData = {
93+
envelope: parseEnvelope(rawProxyRequestBody, new TextEncoder(), new TextDecoder()),
94+
rawProxyRequestBody,
95+
rawSentryResponseBody,
96+
sentryResponseStatusCode: sentryResponse.statusCode,
97+
};
98+
99+
listener(Buffer.from(JSON.stringify(data)).toString('base64'));
100+
});
101+
proxyResponse.end();
102+
});
103+
104+
sentryResponse.addListener('error', err => {
105+
throw err;
106+
});
107+
108+
proxyResponse.writeHead(sentryResponse.statusCode || 500, sentryResponse.headers);
109+
},
110+
);
111+
112+
sentryRequest.write(Buffer.concat(proxyRequestChunks), 'binary');
113+
sentryRequest.end();
114+
});
115+
});
116+
117+
const proxyServerStartupPromise = new Promise<void>(resolve => {
118+
proxyServer.listen(options.port, () => {
119+
resolve();
120+
});
121+
});
122+
123+
const eventCallbackServer = http.createServer((eventCallbackRequest, eventCallbackResponse) => {
124+
eventCallbackResponse.statusCode = 200;
125+
eventCallbackResponse.setHeader('connection', 'keep-alive');
126+
127+
const callbackListener = (data: string): void => {
128+
eventCallbackResponse.write(data.concat('\n'), 'utf8');
129+
};
130+
131+
eventCallbackListeners.add(callbackListener);
132+
133+
eventCallbackRequest.on('close', () => {
134+
eventCallbackListeners.delete(callbackListener);
135+
});
136+
137+
eventCallbackRequest.on('error', () => {
138+
eventCallbackListeners.delete(callbackListener);
139+
});
140+
});
141+
142+
const eventCallbackServerStartupPromise = new Promise<void>(resolve => {
143+
eventCallbackServer.listen(0, () => {
144+
const port = String((eventCallbackServer.address() as AddressInfo).port);
145+
void registerCallbackServerPort(options.proxyServerName, port).then(resolve);
146+
});
147+
});
148+
149+
await eventCallbackServerStartupPromise;
150+
await proxyServerStartupPromise;
151+
return;
152+
}
153+
154+
export async function waitForRequest(
155+
proxyServerName: string,
156+
callback: (eventData: SentryRequestCallbackData) => Promise<boolean> | boolean,
157+
): Promise<SentryRequestCallbackData> {
158+
const eventCallbackServerPort = await retrieveCallbackServerPort(proxyServerName);
159+
160+
return new Promise<SentryRequestCallbackData>((resolve, reject) => {
161+
const request = http.request(`http://localhost:${eventCallbackServerPort}/`, {}, response => {
162+
let eventContents = '';
163+
164+
response.on('error', err => {
165+
reject(err);
166+
});
167+
168+
response.on('data', (chunk: Buffer) => {
169+
const chunkString = chunk.toString('utf8');
170+
chunkString.split('').forEach(char => {
171+
if (char === '\n') {
172+
const eventCallbackData: SentryRequestCallbackData = JSON.parse(
173+
Buffer.from(eventContents, 'base64').toString('utf8'),
174+
);
175+
const callbackResult = callback(eventCallbackData);
176+
if (typeof callbackResult !== 'boolean') {
177+
callbackResult.then(
178+
match => {
179+
if (match) {
180+
response.destroy();
181+
resolve(eventCallbackData);
182+
}
183+
},
184+
err => {
185+
throw err;
186+
},
187+
);
188+
} else if (callbackResult) {
189+
response.destroy();
190+
resolve(eventCallbackData);
191+
}
192+
eventContents = '';
193+
} else {
194+
eventContents = eventContents.concat(char);
195+
}
196+
});
197+
});
198+
});
199+
200+
request.end();
201+
});
202+
}
203+
204+
export function waitForEnvelopeItem(
205+
proxyServerName: string,
206+
callback: (envelopeItem: EnvelopeItem) => Promise<boolean> | boolean,
207+
): Promise<EnvelopeItem> {
208+
return new Promise((resolve, reject) => {
209+
waitForRequest(proxyServerName, async eventData => {
210+
const envelopeItems = eventData.envelope[1];
211+
for (const envelopeItem of envelopeItems) {
212+
if (await callback(envelopeItem)) {
213+
resolve(envelopeItem);
214+
return true;
215+
}
216+
}
217+
return false;
218+
}).catch(reject);
219+
});
220+
}
221+
222+
export function waitForError(
223+
proxyServerName: string,
224+
callback: (transactionEvent: Event) => Promise<boolean> | boolean,
225+
): Promise<Event> {
226+
return new Promise((resolve, reject) => {
227+
waitForEnvelopeItem(proxyServerName, async envelopeItem => {
228+
const [envelopeItemHeader, envelopeItemBody] = envelopeItem;
229+
if (envelopeItemHeader.type === 'event' && (await callback(envelopeItemBody as Event))) {
230+
resolve(envelopeItemBody as Event);
231+
return true;
232+
}
233+
return false;
234+
}).catch(reject);
235+
});
236+
}
237+
238+
export function waitForTransaction(
239+
proxyServerName: string,
240+
callback: (transactionEvent: Event) => Promise<boolean> | boolean,
241+
): Promise<Event> {
242+
return new Promise((resolve, reject) => {
243+
waitForEnvelopeItem(proxyServerName, async envelopeItem => {
244+
const [envelopeItemHeader, envelopeItemBody] = envelopeItem;
245+
if (envelopeItemHeader.type === 'transaction' && (await callback(envelopeItemBody as Event))) {
246+
resolve(envelopeItemBody as Event);
247+
return true;
248+
}
249+
return false;
250+
}).catch(reject);
251+
});
252+
}
253+
254+
const TEMP_FILE_PREFIX = 'event-proxy-server-';
255+
256+
async function registerCallbackServerPort(serverName: string, port: string): Promise<void> {
257+
const tmpFilePath = path.join(os.tmpdir(), `${TEMP_FILE_PREFIX}${serverName}`);
258+
await writeFile(tmpFilePath, port, { encoding: 'utf8' });
259+
}
260+
261+
async function retrieveCallbackServerPort(serverName: string): Promise<string> {
262+
const tmpFilePath = path.join(os.tmpdir(), `${TEMP_FILE_PREFIX}${serverName}`);
263+
return await readFile(tmpFilePath, 'utf8');
264+
}
2265

3266
startEventProxyServer({
4267
port: Number(process.env.BASE_PORT) + Number(process.env.PORT_MODULO) + Number(process.env.PORT_GAP),
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
import { test, expect } from '@playwright/test';
2+
import { waitForTransaction } from '../start-event-proxy';
3+
import axios, { AxiosError } from 'axios';
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_TEST_PROJECT;
8+
const EVENT_POLLING_TIMEOUT = 30_000;
9+
10+
test('Sends a pageload transaction', async ({ page }) => {
11+
const pageloadTransactionEventPromise = waitForTransaction('sveltekit', transactionEvent => {
12+
return transactionEvent?.contexts?.trace?.op === 'pageload' && transactionEvent?.transaction === '/';
13+
});
14+
15+
await page.goto('/');
16+
17+
const transactionEvent = await pageloadTransactionEventPromise;
18+
const transactionEventId = transactionEvent.event_id;
19+
20+
await expect
21+
.poll(
22+
async () => {
23+
try {
24+
const response = await axios.get(
25+
`https://sentry.io/api/0/projects/${sentryTestOrgSlug}/${sentryTestProject}/events/${transactionEventId}/`,
26+
{ headers: { Authorization: `Bearer ${authToken}` } },
27+
);
28+
29+
return response.status;
30+
} catch (e) {
31+
if (e instanceof AxiosError && e.response) {
32+
if (e.response.status !== 404) {
33+
throw e;
34+
} else {
35+
return e.response.status;
36+
}
37+
} else {
38+
throw e;
39+
}
40+
}
41+
},
42+
{
43+
timeout: EVENT_POLLING_TIMEOUT,
44+
},
45+
)
46+
.toBe(200);
47+
});

packages/e2e-tests/test-applications/sveltekit/tsconfig.json

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@
99
"skipLibCheck": true,
1010
"sourceMap": true,
1111
"strict": true
12-
// "allowImportingTsExtensions": true
1312
}
1413
// "include": ["./start-event-proxy.ts", "../../test-utils/**/*"]
1514
// Path aliases are handled by https://kit.svelte.dev/docs/configuration#alias

0 commit comments

Comments
 (0)