Skip to content

Commit c9812ae

Browse files
authored
test(cloudflare): Enable multi-worker tests for CF integration tests (#19938)
This adds tests for multi worker in integration tests by adding a `wrangler-sub-worker.jsonc` into it. Everything else is then according the official [Service Binding docs](https://developers.cloudflare.com/workers/runtime-apis/bindings/service-bindings/). So in the `worker-service-bindings` folder there is now a test which connects "Worker A" with "Worker B". This is enabled by having "Worker B" running as `wrangler-sub-worker.jsonc` and "Worker A" is referencing to "Worker B" inside the normal "wrangler.jsonc". Inside "Worker A" you can then use `env.ANOTHER_WORKER` to have an RPC between two workers. This will be important for the tests for #16898
1 parent 83cabf3 commit c9812ae

File tree

6 files changed

+157
-14
lines changed

6 files changed

+157
-14
lines changed

dev-packages/cloudflare-integration-tests/runner.ts

Lines changed: 49 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,7 @@ export function createRunner(...paths: string[]) {
105105
let envelopeCount = 0;
106106
const { resolve: setWorkerPort, promise: workerPortPromise } = deferredPromise<number>();
107107
let child: ReturnType<typeof spawn> | undefined;
108+
let childSubWorker: ReturnType<typeof spawn> | undefined;
108109

109110
/** Called after each expect callback to check if we're complete */
110111
function expectCallbackCalled(): void {
@@ -168,7 +169,7 @@ export function createRunner(...paths: string[]) {
168169
}
169170

170171
createBasicSentryServer(newEnvelope)
171-
.then(([mockServerPort, mockServerClose]) => {
172+
.then(async ([mockServerPort, mockServerClose]) => {
172173
if (mockServerClose) {
173174
CLEANUP_STEPS.add(() => {
174175
mockServerClose();
@@ -181,6 +182,49 @@ export function createRunner(...paths: string[]) {
181182
? ['inherit', 'inherit', 'inherit', 'ipc']
182183
: ['ignore', 'ignore', 'ignore', 'ipc'];
183184

185+
const onChildError = (e: Error) => {
186+
// eslint-disable-next-line no-console
187+
console.error('Error starting child process:', e);
188+
reject(e);
189+
};
190+
191+
function onChildMessage(message: string, onReady?: (port: number) => void): void {
192+
const msg = JSON.parse(message) as { event: string; port?: number };
193+
if (msg.event === 'DEV_SERVER_READY' && typeof msg.port === 'number') {
194+
if (process.env.DEBUG) log('worker ready on port', msg.port);
195+
onReady?.(msg.port);
196+
}
197+
}
198+
199+
if (existsSync(join(testPath, 'wrangler-sub-worker.jsonc'))) {
200+
childSubWorker = spawn(
201+
'wrangler',
202+
[
203+
'dev',
204+
'--config',
205+
join(testPath, 'wrangler-sub-worker.jsonc'),
206+
'--show-interactive-dev-session',
207+
'false',
208+
'--var',
209+
`SENTRY_DSN:http://public@localhost:${mockServerPort}/1337`,
210+
'--port',
211+
'0',
212+
'--inspector-port',
213+
'0',
214+
],
215+
{ stdio, signal },
216+
);
217+
218+
// Wait for the sub-worker to be ready before starting the main worker
219+
await new Promise<void>((resolveSubWorker, rejectSubWorker) => {
220+
childSubWorker!.on('message', (msg: string) => onChildMessage(msg, () => resolveSubWorker()));
221+
childSubWorker!.on('error', rejectSubWorker);
222+
childSubWorker!.on('exit', code => {
223+
rejectSubWorker(new Error(`Sub-worker exited with code ${code}`));
224+
});
225+
});
226+
}
227+
184228
child = spawn(
185229
'wrangler',
186230
[
@@ -199,21 +243,12 @@ export function createRunner(...paths: string[]) {
199243

200244
CLEANUP_STEPS.add(() => {
201245
child?.kill();
246+
childSubWorker?.kill();
202247
});
203248

204-
child.on('error', e => {
205-
// eslint-disable-next-line no-console
206-
console.error('Error starting child process:', e);
207-
reject(e);
208-
});
209-
210-
child.on('message', (message: string) => {
211-
const msg = JSON.parse(message) as { event: string; port?: number };
212-
if (msg.event === 'DEV_SERVER_READY' && typeof msg.port === 'number') {
213-
setWorkerPort(msg.port);
214-
if (process.env.DEBUG) log('worker ready on port', msg.port);
215-
}
216-
});
249+
childSubWorker?.on('error', onChildError);
250+
child.on('error', onChildError);
251+
child.on('message', (msg: string) => onChildMessage(msg, setWorkerPort));
217252
})
218253
.catch(e => reject(e));
219254

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import * as Sentry from '@sentry/cloudflare';
2+
3+
interface Env {
4+
SENTRY_DSN: string;
5+
}
6+
7+
const myWorker = {
8+
async fetch(request: Request) {
9+
return new Response('Hello from another worker!');
10+
},
11+
};
12+
13+
export default Sentry.withSentry(
14+
(env: Env) => ({
15+
dsn: env.SENTRY_DSN,
16+
tracesSampleRate: 1.0,
17+
}),
18+
myWorker,
19+
);
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import * as Sentry from '@sentry/cloudflare';
2+
3+
interface Env {
4+
SENTRY_DSN: string;
5+
ANOTHER_WORKER: Fetcher;
6+
}
7+
8+
export default Sentry.withSentry(
9+
(env: Env) => ({
10+
dsn: env.SENTRY_DSN,
11+
tracesSampleRate: 1.0,
12+
}),
13+
{
14+
async fetch(request, env) {
15+
const response = await env.ANOTHER_WORKER.fetch(new Request('http://fake-host/hello'));
16+
const text = await response.text();
17+
return new Response(text);
18+
},
19+
} satisfies ExportedHandler<Env>,
20+
);
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
import { expect, it } from 'vitest';
2+
import type { Event } from '@sentry/core';
3+
import { createRunner } from '../../../runner';
4+
5+
it('adds a trace to a worker via service binding', async ({ signal }) => {
6+
const runner = createRunner(__dirname)
7+
.expect(envelope => {
8+
const transactionEvent = envelope[1]?.[0]?.[1] as Event;
9+
expect(transactionEvent).toEqual(
10+
expect.objectContaining({
11+
contexts: expect.objectContaining({
12+
trace: expect.objectContaining({
13+
op: 'http.server',
14+
data: expect.objectContaining({
15+
'sentry.origin': 'auto.http.cloudflare',
16+
}),
17+
origin: 'auto.http.cloudflare',
18+
}),
19+
}),
20+
transaction: 'GET /',
21+
}),
22+
);
23+
})
24+
.expect(envelope => {
25+
const transactionEvent = envelope[1]?.[0]?.[1] as Event;
26+
expect(transactionEvent).toEqual(
27+
expect.objectContaining({
28+
contexts: expect.objectContaining({
29+
trace: expect.objectContaining({
30+
op: 'http.server',
31+
data: expect.objectContaining({
32+
'sentry.origin': 'auto.http.cloudflare',
33+
}),
34+
origin: 'auto.http.cloudflare',
35+
}),
36+
}),
37+
transaction: 'GET /hello',
38+
}),
39+
);
40+
})
41+
.unordered()
42+
.start(signal);
43+
await runner.makeRequest('get', '/');
44+
await runner.completed();
45+
});
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
{
2+
"name": "cloudflare-service-binding-sub-worker",
3+
"main": "index-sub-worker.ts",
4+
"compatibility_date": "2025-06-17",
5+
"compatibility_flags": ["nodejs_als"],
6+
"vars": {
7+
"SENTRY_DSN": "https://932e620ee3921c3b4a61c72558ad88ce@o447951.ingest.us.sentry.io/4509553159831552",
8+
},
9+
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
{
2+
"name": "cloudflare-worker-service-binding",
3+
"main": "index.ts",
4+
"compatibility_date": "2025-06-17",
5+
"compatibility_flags": ["nodejs_als"],
6+
"vars": {
7+
"SENTRY_DSN": "https://932e620ee3921c3b4a61c72558ad88ce@o447951.ingest.us.sentry.io/4509553159831552",
8+
},
9+
"services": [
10+
{
11+
"binding": "ANOTHER_WORKER",
12+
"service": "cloudflare-service-binding-sub-worker",
13+
},
14+
],
15+
}

0 commit comments

Comments
 (0)