Skip to content

Commit afe8cf0

Browse files
feat: add debug option that prints trace id + close reason (#40)
1 parent 4082db8 commit afe8cf0

File tree

4 files changed

+107
-11
lines changed

4 files changed

+107
-11
lines changed

src/deno_sandbox/rpc.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ def __init__(self, transport: WebSocketTransport):
5151
self.__loop: asyncio.AbstractEventLoop | None = None
5252
self._signal_id = 0
5353
self._stream_id = 0
54+
self._debug = transport._debug
5455

5556
@property
5657
def _loop(self) -> asyncio.AbstractEventLoop:

src/deno_sandbox/sandbox.py

Lines changed: 24 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -184,7 +184,7 @@ async def create(
184184
url = self._client._options["sandbox_ws_url"].join("/api/v3/sandboxes/create")
185185
token = self._client._options["token"]
186186

187-
transport = WebSocketTransport()
187+
transport = WebSocketTransport(debug=debug if debug is not None else False)
188188
ws = await transport.connect(
189189
url=url,
190190
headers={
@@ -204,10 +204,18 @@ async def create(
204204
if sandbox_id is None:
205205
raise Exception("Sandbox ID not found in response headers")
206206

207+
if debug:
208+
print(f"Trace ID: {response.headers.get('x-deno-trace-id', 'n/a')}")
209+
207210
sandbox = None
208211
try:
209212
rpc = AsyncRpcClient(transport)
210-
sandbox = AsyncSandbox(self._client, rpc, sandbox_id)
213+
sandbox = AsyncSandbox(
214+
self._client,
215+
rpc,
216+
sandbox_id,
217+
trace_id=response.headers.get("x-deno-trace-id"),
218+
)
211219
yield sandbox
212220
finally:
213221
if sandbox is not None:
@@ -217,17 +225,20 @@ async def create(
217225
async def connect(
218226
self,
219227
sandbox_id: str,
228+
*,
229+
debug: Optional[bool] = None,
220230
) -> AsyncIterator[AsyncSandbox]:
221231
"""Connects to an existing sandbox instance.
222232
223233
Args:
224234
sandbox_id: The unique id of the sandbox to connect to.
235+
debug: Enable debug logging for the sandbox connection.
225236
"""
226237
url = self._client._options["sandbox_ws_url"].join(
227238
f"/api/v3/sandbox/{sandbox_id}/connect"
228239
)
229240
token = self._client._options["token"]
230-
transport = WebSocketTransport()
241+
transport = WebSocketTransport(debug=debug if debug is not None else False)
231242
await transport.connect(
232243
url=url,
233244
headers={
@@ -331,16 +342,16 @@ def create(
331342
def connect(
332343
self,
333344
sandbox_id: str,
345+
*,
346+
debug: Optional[bool] = None,
334347
):
335348
"""Connects to an existing sandbox instance.
336349
337350
Args:
338351
sandbox_id: The unique id of the sandbox to connect to.
339-
region: If the sandbox was created in a non-default region, the region where the sandbox is running.
340352
debug: Enable debug logging for the sandbox connection.
341-
ssh: Whether to expose SSH access to the sandbox.
342353
"""
343-
async_cm = self._async.connect(sandbox_id)
354+
async_cm = self._async.connect(sandbox_id, debug=debug)
344355
async_handle = self._bridge.run(async_cm.__aenter__())
345356

346357
try:
@@ -885,7 +896,11 @@ def deploy(
885896

886897
class AsyncSandbox:
887898
def __init__(
888-
self, client: AsyncConsoleClient, rpc: AsyncRpcClient, sandbox_id: str
899+
self,
900+
client: AsyncConsoleClient,
901+
rpc: AsyncRpcClient,
902+
sandbox_id: str,
903+
trace_id: str | None = None,
889904
):
890905
self._client = client
891906
self._rpc = rpc
@@ -894,6 +909,7 @@ def __init__(
894909
self.url: str | None = None
895910
self.ssh: None = None
896911
self.id = sandbox_id
912+
self.trace_id: str | None = trace_id
897913
self.fs = AsyncSandboxFs(rpc)
898914
self.deno = AsyncSandboxDeno(rpc, self._processes, client, sandbox_id)
899915
self.env = AsyncSandboxEnv(rpc)
@@ -1088,6 +1104,7 @@ def __init__(
10881104
self.url: str | None = None
10891105
self.ssh: None = None
10901106
self.id = async_sandbox.id
1107+
self.trace_id: str | None = async_sandbox.trace_id
10911108
self.fs = SandboxFs(rpc, bridge)
10921109
self.deno = SandboxDeno(
10931110
rpc, bridge, self._async._processes, client, async_sandbox.id

src/deno_sandbox/transport.py

Lines changed: 65 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,56 @@
66
from .errors import AuthenticationError
77

88

9+
# WebSocket close status codes
10+
class WebSocketStatus:
11+
NORMAL_CLOSURE = 1000
12+
GOING_AWAY = 1001
13+
PROTOCOL_ERROR = 1002
14+
UNSUPPORTED_DATA = 1003
15+
NO_STATUS_RECEIVED = 1005
16+
ABNORMAL_CLOSURE = 1006
17+
INVALID_FRAME_PAYLOAD_DATA = 1007
18+
POLICY_VIOLATION = 1008
19+
MESSAGE_TOO_BIG = 1009
20+
MANDATORY_EXT = 1010
21+
INTERNAL_SERVER_ERROR = 1011
22+
INVALID_JSON = 4000
23+
INVALID_JSON_RPC = 4001
24+
25+
26+
def get_close_code_description(code: int) -> str:
27+
"""Returns a human-readable description for a WebSocket close code."""
28+
descriptions = {
29+
WebSocketStatus.NORMAL_CLOSURE: "Normal closure (1000)",
30+
WebSocketStatus.GOING_AWAY: "Server going away (1001)",
31+
WebSocketStatus.PROTOCOL_ERROR: "Protocol error (1002)",
32+
WebSocketStatus.UNSUPPORTED_DATA: "Unsupported data (1003)",
33+
WebSocketStatus.NO_STATUS_RECEIVED: "No status received (1005)",
34+
WebSocketStatus.ABNORMAL_CLOSURE: "Abnormal closure (1006)",
35+
WebSocketStatus.INVALID_FRAME_PAYLOAD_DATA: "Invalid frame payload data (1007)",
36+
WebSocketStatus.POLICY_VIOLATION: "Policy violation (1008)",
37+
WebSocketStatus.MESSAGE_TOO_BIG: "Message too big (1009)",
38+
WebSocketStatus.MANDATORY_EXT: "Mandatory extension (1010)",
39+
WebSocketStatus.INTERNAL_SERVER_ERROR: "Internal server error (1011)",
40+
WebSocketStatus.INVALID_JSON: "Invalid JSON (4000)",
41+
WebSocketStatus.INVALID_JSON_RPC: "Invalid JSON-RPC (4001)",
42+
}
43+
44+
if code in descriptions:
45+
return descriptions[code]
46+
elif 3000 <= code < 4000:
47+
return f"Library/framework code ({code})"
48+
elif 4000 <= code < 5000:
49+
return f"Application code ({code})"
50+
else:
51+
return f"Unknown code ({code})"
52+
53+
954
class WebSocketTransport:
10-
def __init__(self) -> None:
55+
def __init__(self, debug: bool = False) -> None:
1156
self._ws: ClientConnection | None = None
1257
self._closed = False
58+
self._debug = debug
1359

1460
@property
1561
def closed(self) -> bool:
@@ -49,5 +95,22 @@ async def __aiter__(self):
4995
try:
5096
async for message in self._ws:
5197
yield message
52-
except ConnectionClosed:
98+
except ConnectionClosed as e:
99+
if self._debug:
100+
# Extract close code and reason from the received close frame
101+
code = e.rcvd.code if e.rcvd else WebSocketStatus.NO_STATUS_RECEIVED
102+
reason = e.rcvd.reason if e.rcvd else ""
103+
104+
if code in (
105+
WebSocketStatus.NORMAL_CLOSURE,
106+
WebSocketStatus.GOING_AWAY,
107+
WebSocketStatus.NO_STATUS_RECEIVED,
108+
):
109+
# Expected closure (including sandbox timeout)
110+
print(f"Sandbox connection closed: {reason or 'server going away'}")
111+
else:
112+
# Unexpected closure
113+
code_description = get_close_code_description(code)
114+
reason_str = f" ({reason})" if reason else ""
115+
print(f"WebSocket closed: {code_description}{reason_str}")
53116
return

tests/conftest.py

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,13 +7,28 @@
77
async def async_shared_sandbox():
88
sdk = AsyncDenoDeploy()
99

10-
async with sdk.sandbox.create() as sandbox:
10+
async with sdk.sandbox.create(debug=True) as sandbox:
1111
yield sandbox
1212

1313

1414
@pytest.fixture(scope="session")
1515
def shared_sandbox():
1616
sdk = DenoDeploy()
1717

18-
with sdk.sandbox.create() as sandbox:
18+
with sdk.sandbox.create(debug=True) as sandbox:
1919
yield sandbox
20+
21+
22+
@pytest.hookimpl(tryfirst=True, hookwrapper=True)
23+
def pytest_runtest_makereport(item, call): # noqa: ARG001
24+
outcome = yield
25+
report = outcome.get_result()
26+
27+
if report.when == "call" and report.failed:
28+
# Check for sandbox fixtures
29+
for name in ("async_shared_sandbox", "shared_sandbox"):
30+
if name in item.funcargs:
31+
sandbox = item.funcargs[name]
32+
if hasattr(sandbox, "trace_id") and sandbox.trace_id:
33+
print(f"\nTrace ID: {sandbox.trace_id}")
34+
break

0 commit comments

Comments
 (0)