diff --git a/dev-packages/e2e-tests/test-applications/cloudflare-workers-send-to-sentry/deployed-worker.mjs b/dev-packages/e2e-tests/test-applications/cloudflare-workers-send-to-sentry/deployed-worker.ts similarity index 53% rename from dev-packages/e2e-tests/test-applications/cloudflare-workers-send-to-sentry/deployed-worker.mjs rename to dev-packages/e2e-tests/test-applications/cloudflare-workers-send-to-sentry/deployed-worker.ts index 9dacbe052b8b..514927322439 100644 --- a/dev-packages/e2e-tests/test-applications/cloudflare-workers-send-to-sentry/deployed-worker.mjs +++ b/dev-packages/e2e-tests/test-applications/cloudflare-workers-send-to-sentry/deployed-worker.ts @@ -6,7 +6,7 @@ import { fileURLToPath } from 'node:url'; const __dirname = dirname(fileURLToPath(import.meta.url)); -function wrangler(args, env = {}) { +function wrangler(args: string[], env: Record = {}): void { execFileSync('pnpm', ['exec', 'wrangler', ...args], { cwd: __dirname, env: { ...process.env, ...env }, @@ -18,10 +18,12 @@ function wrangler(args, env = {}) { * Workflow names are unique per Cloudflare account, so every worker gets its own. The Vite build writes the * config wrangler deploys from, and `.wrangler/deploy/config.json` points to it. */ -function nameWorkflowsAfterWorker(name) { - const redirect = JSON.parse(readFileSync(join(__dirname, '.wrangler/deploy/config.json'), 'utf8')); +function nameWorkflowsAfterWorker(name: string): void { + const redirect: { configPath: string } = JSON.parse( + readFileSync(join(__dirname, '.wrangler/deploy/config.json'), 'utf8'), + ); const configPath = join(__dirname, '.wrangler/deploy', redirect.configPath); - const config = JSON.parse(readFileSync(configPath, 'utf8')); + const config: { workflows?: { name: string }[] } = JSON.parse(readFileSync(configPath, 'utf8')); for (const workflow of config.workflows ?? []) { workflow.name = name; @@ -30,7 +32,7 @@ function nameWorkflowsAfterWorker(name) { } /** Deploys the worker under `name` and returns its workers.dev URL. */ -export function deployWorker(name, dsn) { +export function deployWorker(name: string, dsn: string): string { nameWorkflowsAfterWorker(name); const outputDir = mkdtempSync(join(tmpdir(), 'wrangler-output-')); const outputFile = join(outputDir, 'output.ndjson'); @@ -41,7 +43,7 @@ export function deployWorker(name, dsn) { const url = readFileSync(outputFile, 'utf8') .split('\n') .filter(Boolean) - .map(line => JSON.parse(line)) + .map(line => JSON.parse(line) as { type?: string; targets?: string[] }) .find(entry => entry.type === 'deploy') ?.targets?.find(target => target.endsWith('.workers.dev')); @@ -55,7 +57,7 @@ export function deployWorker(name, dsn) { } } -export function deleteWorker(name) { +export function deleteWorker(name: string): void { wrangler(['delete', '--name', name, '--force']); } @@ -63,12 +65,12 @@ export function deleteWorker(name) { * CI keeps its Workers: one per ref, overwritten by the next run of the same ref and deleted by the * cleanup workflow once a PR closes. Local runs delete theirs unless `E2E_KEEP_WORKER` is set. */ -export function keepsWorker() { +export function keepsWorker(): boolean { return Boolean(process.env.GITHUB_ACTIONS || process.env.E2E_KEEP_WORKER); } /** A freshly created workers.dev route can take a moment to become reachable. */ -export async function waitForWorker(url) { +export async function waitForWorker(url: string): Promise { const deadline = Date.now() + 60_000; while (Date.now() < deadline) { @@ -88,3 +90,39 @@ export async function waitForWorker(url) { throw new Error(`Worker at ${url} did not become reachable within 60s.`); } + +/** + * Sends a request until the Worker itself answers it, and returns the body of that answer. + * + * On the first deployment of a Worker name, Cloudflare has answered a request with a 500 while + * Workers Logs had no invocation for it. `status` is the status the Worker answers with. A Worker + * that threw answers with status 500 and Cloudflare error code 1101, which sets it apart from a 500 + * that did not come from the Worker. + */ +export async function fetchFromWorker(url: string, status: number, init?: RequestInit): Promise { + const deadline = Date.now() + 60_000; + let lastAnswer = 'no answer'; + + while (Date.now() < deadline) { + try { + const response = await fetch(url, init); + const body = await response.text(); + // Cloudflare sends its error page as HTML to some clients (Node's fetch among them) and as + // `error code: ` plain text to others, so the code is read from either format. + const errorCode = /cf-error-code">(\d+)<|^error code: (\d+)/.exec(body)?.slice(1).find(Boolean); + + if (response.status === status && (status !== 500 || errorCode === '1101')) { + return body; + } + + lastAnswer = `${response.status}, cf-ray ${response.headers.get('cf-ray')}, error code ${errorCode ?? 'none'}, body: ${body.slice(0, 200)}`; + } catch (error) { + lastAnswer = String(error); + } + + console.log(`The Worker did not answer ${url}: ${lastAnswer}`); + await new Promise(resolve => setTimeout(resolve, 2_000)); + } + + throw new Error(`The Worker did not answer ${url} with status ${status} within 60s. Last answer: ${lastAnswer}`); +} diff --git a/dev-packages/e2e-tests/test-applications/cloudflare-workers-send-to-sentry/global-setup.mjs b/dev-packages/e2e-tests/test-applications/cloudflare-workers-send-to-sentry/global-setup.ts similarity index 94% rename from dev-packages/e2e-tests/test-applications/cloudflare-workers-send-to-sentry/global-setup.mjs rename to dev-packages/e2e-tests/test-applications/cloudflare-workers-send-to-sentry/global-setup.ts index 08e9311cb7ca..b33f187e1d94 100644 --- a/dev-packages/e2e-tests/test-applications/cloudflare-workers-send-to-sentry/global-setup.mjs +++ b/dev-packages/e2e-tests/test-applications/cloudflare-workers-send-to-sentry/global-setup.ts @@ -1,6 +1,6 @@ import { randomBytes } from 'node:crypto'; import { existsSync } from 'node:fs'; -import { deleteWorker, deployWorker, keepsWorker, waitForWorker } from './deployed-worker.mjs'; +import { deleteWorker, deployWorker, keepsWorker, waitForWorker } from './deployed-worker'; const WORKER_PREFIX = 'e2e-send-to-sentry'; @@ -9,7 +9,7 @@ const WORKER_PREFIX = 'e2e-send-to-sentry'; * next run of the same ref overwrites. Pull request refs look like `123/merge` and merge queue refs * like `gh-readonly-queue//pr-123-`; both map to the PR's Worker. */ -export function getWorkerName() { +export function getWorkerName(): string { if (!process.env.GITHUB_ACTIONS) { return `${WORKER_PREFIX}-local-${randomBytes(3).toString('hex')}`; } @@ -24,7 +24,7 @@ export function getWorkerName() { return `${WORKER_PREFIX}-${slug}`.slice(0, 63).replace(/-+$/, ''); } -export default async function globalSetup() { +export default async function globalSetup(): Promise { if (!existsSync(new URL('.wrangler/deploy/config.json', import.meta.url))) { throw new Error('Run `pnpm build` first: wrangler would deploy the uninstrumented source.'); } diff --git a/dev-packages/e2e-tests/test-applications/cloudflare-workers-send-to-sentry/global-teardown.mjs b/dev-packages/e2e-tests/test-applications/cloudflare-workers-send-to-sentry/global-teardown.ts similarity index 82% rename from dev-packages/e2e-tests/test-applications/cloudflare-workers-send-to-sentry/global-teardown.mjs rename to dev-packages/e2e-tests/test-applications/cloudflare-workers-send-to-sentry/global-teardown.ts index 0fee903dc5c7..9f012cd51101 100644 --- a/dev-packages/e2e-tests/test-applications/cloudflare-workers-send-to-sentry/global-teardown.mjs +++ b/dev-packages/e2e-tests/test-applications/cloudflare-workers-send-to-sentry/global-teardown.ts @@ -1,6 +1,6 @@ -import { deleteWorker, keepsWorker } from './deployed-worker.mjs'; +import { deleteWorker, keepsWorker } from './deployed-worker'; -export default function globalTeardown() { +export default function globalTeardown(): void { const workerName = process.env.E2E_TEST_WORKER_NAME; if (!workerName) { diff --git a/dev-packages/e2e-tests/test-applications/cloudflare-workers-send-to-sentry/package.json b/dev-packages/e2e-tests/test-applications/cloudflare-workers-send-to-sentry/package.json index 75bbb77d0b7c..3bebd9caebf1 100644 --- a/dev-packages/e2e-tests/test-applications/cloudflare-workers-send-to-sentry/package.json +++ b/dev-packages/e2e-tests/test-applications/cloudflare-workers-send-to-sentry/package.json @@ -5,7 +5,7 @@ "type": "module", "scripts": { "build": "vite build", - "typecheck": "tsc --noEmit", + "typecheck": "tsc --noEmit && tsc --noEmit -p tsconfig.node.json", "test": "playwright test", "clean": "npx rimraf node_modules pnpm-lock.yaml dist .wrangler", "test:build": "pnpm install && pnpm build", diff --git a/dev-packages/e2e-tests/test-applications/cloudflare-workers-send-to-sentry/playwright.config.ts b/dev-packages/e2e-tests/test-applications/cloudflare-workers-send-to-sentry/playwright.config.ts index 0b79fb88052f..91e214f15493 100644 --- a/dev-packages/e2e-tests/test-applications/cloudflare-workers-send-to-sentry/playwright.config.ts +++ b/dev-packages/e2e-tests/test-applications/cloudflare-workers-send-to-sentry/playwright.config.ts @@ -3,8 +3,8 @@ import { defineConfig } from '@playwright/test'; export default defineConfig({ testDir: './tests', // The worker is deployed once for the whole run and deleted again afterwards. - globalSetup: './global-setup.mjs', - globalTeardown: './global-teardown.mjs', + globalSetup: './global-setup.ts', + globalTeardown: './global-teardown.ts', /* Spans take ~2min to become queryable via the trace endpoint. */ timeout: 210_000, fullyParallel: true, diff --git a/dev-packages/e2e-tests/test-applications/cloudflare-workers-send-to-sentry/tests/send-to-sentry.test.ts b/dev-packages/e2e-tests/test-applications/cloudflare-workers-send-to-sentry/tests/send-to-sentry.test.ts index ab642bf55b1a..8578ecb7bee7 100644 --- a/dev-packages/e2e-tests/test-applications/cloudflare-workers-send-to-sentry/tests/send-to-sentry.test.ts +++ b/dev-packages/e2e-tests/test-applications/cloudflare-workers-send-to-sentry/tests/send-to-sentry.test.ts @@ -8,14 +8,15 @@ import { flattenTrace, traceTarget, } from '@sentry-internal/test-utils/cli'; +import { fetchFromWorker } from '../deployed-worker'; -// Set by global-setup.mjs once the worker for this run is deployed. +// Set by global-setup.ts once the worker for this run is deployed. const workerUrl = process.env.E2E_TEST_WORKER_URL; test('Sends a captured exception to Sentry', async () => { - const response = await fetch(`${workerUrl}/test-error`); - expect(response.status).toBe(200); - const { eventId, traceId } = await response.json(); + const { eventId, traceId }: { eventId: string; traceId: string } = JSON.parse( + await fetchFromWorker(`${workerUrl}/test-error`, 200), + ); console.log(`Polling for error eventId ${eventId}: sentry trace view ${traceTarget(traceId)}`); @@ -25,13 +26,12 @@ test('Sends a captured exception to Sentry', async () => { test('Sends an unhandled exception and its request span to Sentry', async () => { const traceId = randomBytes(16).toString('hex'); const publicKey = new URL(process.env.E2E_TEST_DSN!).username; - const response = await fetch(`${workerUrl}/test-unhandled-error`, { + await fetchFromWorker(`${workerUrl}/test-unhandled-error`, 500, { headers: { 'sentry-trace': `${traceId}-${randomBytes(8).toString('hex')}-1`, baggage: `sentry-trace_id=${traceId},sentry-public_key=${publicKey},sentry-sampled=true,sentry-sample_rate=1`, }, }); - expect(response.status).toBe(500); console.log(`Polling for unhandled error: sentry trace view ${traceTarget(traceId)}`); @@ -40,9 +40,9 @@ test('Sends an unhandled exception and its request span to Sentry', async () => }); test('Sends a request span to Sentry', async () => { - const response = await fetch(`${workerUrl}/test-span`); - expect(response.status).toBe(200); - const { spanId, traceId } = await response.json(); + const { spanId, traceId }: { spanId: string; traceId: string } = JSON.parse( + await fetchFromWorker(`${workerUrl}/test-span`, 200), + ); console.log(`Polling for request spanId ${spanId}: sentry trace view ${traceTarget(traceId)}`); @@ -52,9 +52,9 @@ test('Sends a request span to Sentry', async () => { }); test('Sends the spans of Workflow steps before the Workflow goes to sleep', async () => { - const response = await fetch(`${workerUrl}/test-workflow-sleep`); - expect(response.status).toBe(200); - const { instanceId, traceId } = await response.json(); + const { instanceId, traceId }: { instanceId: string; traceId: string } = JSON.parse( + await fetchFromWorker(`${workerUrl}/test-workflow-sleep`, 200), + ); console.log(`Polling for the Workflow step spans: sentry trace view ${traceTarget(traceId)}`); @@ -68,6 +68,8 @@ test('Sends the spans of Workflow steps before the Workflow goes to sleep', asyn ) .toBe(3); - const { status } = await fetch(`${workerUrl}/test-workflow-status?id=${instanceId}`).then(res => res.json()); + const { status }: { status: string } = JSON.parse( + await fetchFromWorker(`${workerUrl}/test-workflow-status?id=${instanceId}`, 200), + ); expect(['running', 'waiting']).toContain(status); }); diff --git a/dev-packages/e2e-tests/test-applications/cloudflare-workers-send-to-sentry/tsconfig.node.json b/dev-packages/e2e-tests/test-applications/cloudflare-workers-send-to-sentry/tsconfig.node.json new file mode 100644 index 000000000000..657ecc4ef24a --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/cloudflare-workers-send-to-sentry/tsconfig.node.json @@ -0,0 +1,7 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "types": ["node"] + }, + "include": ["tests/**/*", "deployed-worker.ts", "global-setup.ts", "global-teardown.ts", "playwright.config.ts"] +} diff --git a/dev-packages/e2e-tests/test-applications/cloudflare-workers-send-to-sentry/wrangler.jsonc b/dev-packages/e2e-tests/test-applications/cloudflare-workers-send-to-sentry/wrangler.jsonc index 351a68bd023f..d6b15b1dbf5e 100644 --- a/dev-packages/e2e-tests/test-applications/cloudflare-workers-send-to-sentry/wrangler.jsonc +++ b/dev-packages/e2e-tests/test-applications/cloudflare-workers-send-to-sentry/wrangler.jsonc @@ -1,6 +1,6 @@ { "$schema": "node_modules/wrangler/config-schema.json", - // Placeholder only: every test run deploys under a unique name, see global-setup.mjs. + // Placeholder only: every test run deploys under a unique name, see global-setup.ts. "name": "cloudflare-workers-send-to-sentry", "main": "src/index.ts", "compatibility_date": "2026-05-20",