Skip to content

Commit 8f57ccf

Browse files
author
cod1k
committed
Add WebSocket support and Sentry tracking in Cloudflare Workers
Implemented WebSocket handling with message reception and connection closure logic, including error recording in Sentry. Updated tests to verify WebSocket behaviors and added the "ws" package for WebSocket functionality.
1 parent 43bcb2e commit 8f57ccf

File tree

3 files changed

+82
-11
lines changed

3 files changed

+82
-11
lines changed

dev-packages/e2e-tests/test-applications/cloudflare-workers/package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,8 @@
2424
"@sentry-internal/test-utils": "link:../../../test-utils",
2525
"typescript": "^5.5.2",
2626
"vitest": "~3.2.0",
27-
"wrangler": "^4.23.0"
27+
"wrangler": "^4.23.0",
28+
"ws": "^8.18.3"
2829
},
2930
"volta": {
3031
"extends": "../../package.json"

dev-packages/e2e-tests/test-applications/cloudflare-workers/src/index.ts

Lines changed: 35 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -14,17 +14,42 @@ import * as Sentry from '@sentry/cloudflare';
1414
import { DurableObject } from "cloudflare:workers";
1515

1616
class MyDurableObjectBase extends DurableObject<Env> {
17-
async throwException(): Promise<string> {
18-
throw new Error("Should be recorded in Sentry.")
19-
}
17+
private throwOnExit = new WeakMap<WebSocket, Error>();
18+
async throwException(): Promise<void> {
19+
throw new Error('Should be recorded in Sentry.');
20+
}
2021

21-
async fetch(request: Request){
22-
const {pathname} = new URL(request.url)
23-
if(pathname === '/throwException'){
24-
await this.throwException()
25-
}
26-
return new Response('DO is fine')
27-
}
22+
async fetch(request: Request) {
23+
const { pathname } = new URL(request.url);
24+
switch (pathname) {
25+
case '/throwException': {
26+
await this.throwException();
27+
break;
28+
}
29+
case '/ws':
30+
const webSocketPair = new WebSocketPair();
31+
const [client, server] = Object.values(webSocketPair);
32+
this.ctx.acceptWebSocket(server);
33+
return new Response(null, { status: 101, webSocket: client });
34+
}
35+
return new Response('DO is fine');
36+
}
37+
38+
webSocketMessage(ws: WebSocket, message: string | ArrayBuffer): void | Promise<void> {
39+
if (message === 'throwException') {
40+
throw new Error('Should be recorded in Sentry: webSocketMessage');
41+
} else if (message === 'throwOnExit') {
42+
this.throwOnExit.set(ws, new Error('Should be recorded in Sentry: webSocketClose'));
43+
}
44+
}
45+
46+
webSocketClose(ws: WebSocket): void | Promise<void> {
47+
if (this.throwOnExit.has(ws)) {
48+
const error = this.throwOnExit.get(ws)!;
49+
this.throwOnExit.delete(ws);
50+
throw error;
51+
}
52+
}
2853
}
2954

3055
export const MyDurableObject = Sentry.instrumentDurableObjectWithSentry(

dev-packages/e2e-tests/test-applications/cloudflare-workers/tests/index.test.ts

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { expect, test } from '@playwright/test';
22
import { waitForError } from '@sentry-internal/test-utils';
3+
import {WebSocket} from 'ws'
34

45
test('Index page', async ({ baseURL }) => {
56
const result = await fetch(baseURL!);
@@ -35,3 +36,47 @@ test('Request processed by DurableObject\'s fetch is recorded', async ({baseURL}
3536
const event = await eventWaiter;
3637
expect(event.exception?.values?.[0]?.value).toBe('Should be recorded in Sentry.');
3738
});
39+
test('Websocket.webSocketMessage', async ({baseURL}) => {
40+
const eventWaiter = waitForError('cloudflare-workers', (event) => {
41+
return event.exception?.values?.[0]?.mechanism?.type === 'cloudflare_durableobject';
42+
});
43+
const url = new URL('/pass-to-object/ws', baseURL);
44+
url.protocol = url.protocol.replace('http', 'ws');
45+
const socket = new WebSocket(url.toString());
46+
socket.addEventListener('open', () => {
47+
socket.send('throwException')
48+
});
49+
const event = await eventWaiter;
50+
socket.close();
51+
expect(event.exception?.values?.[0]?.value).toBe('Should be recorded in Sentry: webSocketMessage');
52+
})
53+
54+
test('Websocket.webSocketClose', async ({baseURL}) => {
55+
const eventWaiter = waitForError('cloudflare-workers', (event) => {
56+
return event.exception?.values?.[0]?.mechanism?.type === 'cloudflare_durableobject';
57+
});
58+
const url = new URL('/pass-to-object/ws', baseURL);
59+
url.protocol = url.protocol.replace('http', 'ws');
60+
const socket = new WebSocket(url.toString());
61+
socket.addEventListener('open', () => {
62+
socket.send('throwOnExit')
63+
socket.close()
64+
});
65+
const event = await eventWaiter;
66+
expect(event.exception?.values?.[0]?.value).toBe('Should be recorded in Sentry: webSocketClose');
67+
})
68+
69+
test('Websocket.webSocketClose', async ({baseURL}) => {
70+
const eventWaiter = waitForError('cloudflare-workers', (event) => {
71+
return event.exception?.values?.[0]?.mechanism?.type === 'cloudflare_durableobject';
72+
});
73+
const url = new URL('/pass-to-object/ws', baseURL);
74+
url.protocol = url.protocol.replace('http', 'ws');
75+
const socket = new WebSocket(url.toString());
76+
socket.addEventListener('open', () => {
77+
socket.send('throwOnExit')
78+
socket.close()
79+
});
80+
const event = await eventWaiter;
81+
expect(event.exception?.values?.[0]?.value).toBe('Should be recorded in Sentry: webSocketClose');
82+
})

0 commit comments

Comments
 (0)