Skip to content

Commit 975af2c

Browse files
committed
feat: 添加追踪 ID 支持,增强错误处理和调试功能
1 parent bd57018 commit 975af2c

11 files changed

Lines changed: 267 additions & 19 deletions

File tree

.gitignore

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,4 +42,5 @@ build/Release
4242
/test.md
4343
logs.txt
4444
/dist
45-
/files
45+
/files
46+
.ace-tool/

packages/js-sdk/src/api/index.ts

Lines changed: 31 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,11 @@ import { defaultHeaders } from './metadata'
55
import { ConnectionConfig } from '../connectionConfig'
66
import { AuthenticationError, RateLimitError, SandboxError } from '../errors'
77
import { createApiLogger } from '../logs'
8+
import {
9+
appendTraceIdToMessage,
10+
createTraceIdMiddleware,
11+
getTraceIdFromResponse,
12+
} from '../trace'
813

914
export function handleApiError(
1015
response: FetchResponse<any, any, any>,
@@ -18,28 +23,47 @@ export function handleApiError(
1823
return
1924
}
2025

26+
const traceId = getTraceIdFromResponse(response.response)
27+
2128
if (response.response.status === 401) {
2229
const message = 'Unauthorized, please check your credentials.'
2330
const content = response.error?.message ?? response.error
2431

2532
if (content) {
26-
return new AuthenticationError(`${message} - ${content}`)
33+
const err = new AuthenticationError(
34+
appendTraceIdToMessage(`${message} - ${content}`, traceId)
35+
)
36+
;(err as any).traceId = traceId
37+
return err
2738
}
28-
return new AuthenticationError(message)
39+
const err = new AuthenticationError(appendTraceIdToMessage(message, traceId))
40+
;(err as any).traceId = traceId
41+
return err
2942
}
3043

3144
if (response.response.status === 429) {
3245
const message = 'Rate limit exceeded, please try again later'
3346
const content = response.error?.message ?? response.error
3447

3548
if (content) {
36-
return new RateLimitError(`${message} - ${content}`)
49+
const err = new RateLimitError(
50+
appendTraceIdToMessage(`${message} - ${content}`, traceId)
51+
)
52+
;(err as any).traceId = traceId
53+
return err
3754
}
38-
return new RateLimitError(message)
55+
const err = new RateLimitError(appendTraceIdToMessage(message, traceId))
56+
;(err as any).traceId = traceId
57+
return err
3958
}
4059

4160
const message = response.error?.message ?? response.error
42-
return new errorClass(`${response.response.status}: ${message}`, stackTrace)
61+
const err = new errorClass(
62+
appendTraceIdToMessage(`${response.response.status}: ${message}`, traceId),
63+
stackTrace
64+
)
65+
;(err as any).traceId = traceId
66+
return err
4367
}
4468

4569
/**
@@ -86,6 +110,8 @@ class ApiClient {
86110
},
87111
})
88112

113+
this.api.use(createTraceIdMiddleware())
114+
89115
if (config.logger) {
90116
this.api.use(createApiLogger(config.logger))
91117
}

packages/js-sdk/src/envd/api.ts

Lines changed: 32 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,11 @@ import {
1111
formatSandboxTimeoutError,
1212
AuthenticationError,
1313
} from '../errors'
14+
import {
15+
appendTraceIdToMessage,
16+
createTraceIdMiddleware,
17+
getTraceIdFromResponse,
18+
} from '../trace'
1419
import { StartResponse, ConnectResponse } from './process/process_pb'
1520
import { Code, ConnectError } from '@connectrpc/connect'
1621
import { WatchDirResponse } from './filesystem/filesystem_pb'
@@ -25,29 +30,46 @@ export async function handleEnvdApiError(res: {
2530
return
2631
}
2732

33+
const traceId = getTraceIdFromResponse(res.response)
34+
2835
const message: string =
2936
typeof res.error == 'string'
3037
? res.error
3138
: res.error?.message || (await res.response.text())
3239

40+
let err: Error
3341
switch (res.response.status) {
3442
case 400:
35-
return new InvalidArgumentError(message)
43+
err = new InvalidArgumentError(message)
44+
break
3645
case 401:
37-
return new AuthenticationError(message)
46+
err = new AuthenticationError(message)
47+
break
3848
case 404:
39-
return new NotFoundError(message)
49+
err = new NotFoundError(message)
50+
break
4051
case 429:
41-
return new SandboxError(
42-
`${res.response.status}: ${message}: The requests are being rate limited.`
52+
err = new SandboxError(
53+
appendTraceIdToMessage(
54+
`${res.response.status}: ${message}: The requests are being rate limited.`,
55+
traceId
56+
)
4357
)
58+
break
4459
case 502:
45-
return formatSandboxTimeoutError(message)
60+
err = formatSandboxTimeoutError(message)
61+
break
4662
case 507:
47-
return new NotEnoughSpaceError(message)
63+
err = new NotEnoughSpaceError(message)
64+
break
4865
default:
49-
return new SandboxError(`${res.response.status}: ${message}`)
66+
err = new SandboxError(`${res.response.status}: ${message}`)
67+
break
5068
}
69+
70+
err.message = appendTraceIdToMessage(err.message, traceId)
71+
;(err as any).traceId = traceId
72+
return err
5173
}
5274

5375
export async function handleProcessStartEvent(
@@ -117,6 +139,8 @@ class EnvdApiClient {
117139
})
118140
this.version = metadata.version
119141

142+
this.api.use(createTraceIdMiddleware())
143+
120144
if (config.logger) {
121145
this.api.use(createApiLogger(config.logger))
122146
}

packages/js-sdk/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ export {
1616
FileUploadError,
1717
} from './errors'
1818
export type { Logger } from './logs'
19+
export { getLastTraceId } from './trace'
1920

2021
export { getSignature } from './sandbox/signature'
2122

packages/js-sdk/src/sandbox/sandboxApi.ts

Lines changed: 20 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import {
66
} from '../connectionConfig'
77
import { compareVersions } from 'compare-versions'
88
import { NotFoundError, TemplateError } from '../errors'
9+
import { appendTraceIdToMessage, getTraceIdFromResponse } from '../trace'
910
import { timeoutToSeconds } from '../utils'
1011
import type { McpServer as BaseMcpServer } from './mcp'
1112

@@ -432,7 +433,10 @@ export class SandboxApi {
432433
})
433434

434435
if (res.error?.code === 404) {
435-
throw new NotFoundError(`Sandbox ${sandboxId} not found`)
436+
const traceId = getTraceIdFromResponse(res.response)
437+
throw new NotFoundError(
438+
appendTraceIdToMessage(`Sandbox ${sandboxId} not found`, traceId)
439+
)
436440
}
437441

438442
const err = handleApiError(res)
@@ -455,7 +459,10 @@ export class SandboxApi {
455459
})
456460

457461
if (res.error?.code === 404) {
458-
throw new NotFoundError(`Sandbox ${sandboxId} not found`)
462+
const traceId = getTraceIdFromResponse(res.response)
463+
throw new NotFoundError(
464+
appendTraceIdToMessage(`Sandbox ${sandboxId} not found`, traceId)
465+
)
459466
}
460467

461468
const err = handleApiError(res)
@@ -508,7 +515,10 @@ export class SandboxApi {
508515
})
509516

510517
if (res.error?.code === 404) {
511-
throw new NotFoundError(`Sandbox ${sandboxId} not found`)
518+
const traceId = getTraceIdFromResponse(res.response)
519+
throw new NotFoundError(
520+
appendTraceIdToMessage(`Sandbox ${sandboxId} not found`, traceId)
521+
)
512522
}
513523

514524
if (res.error?.code === 409) {
@@ -591,7 +601,13 @@ export class SandboxApi {
591601
})
592602

593603
if (res.error?.code === 404) {
594-
throw new NotFoundError(`Paused sandbox ${sandboxId} not found`)
604+
const traceId = getTraceIdFromResponse(res.response)
605+
throw new NotFoundError(
606+
appendTraceIdToMessage(
607+
`Paused sandbox ${sandboxId} not found`,
608+
traceId
609+
)
610+
)
595611
}
596612

597613
const err = handleApiError(res)

packages/js-sdk/src/trace.ts

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
import type { Middleware } from 'openapi-fetch'
2+
3+
export const TRACE_ID_HEADER = 'x-trace-id'
4+
5+
let lastTraceId: string | undefined
6+
7+
export function getLastTraceId(): string | undefined {
8+
return lastTraceId
9+
}
10+
11+
export function setLastTraceId(traceId: string | null | undefined) {
12+
if (typeof traceId !== 'string') {
13+
return
14+
}
15+
16+
const trimmed = traceId.trim()
17+
if (!trimmed) {
18+
return
19+
}
20+
21+
lastTraceId = trimmed
22+
}
23+
24+
export function getTraceIdFromResponse(
25+
response: Response | undefined | null
26+
): string | undefined {
27+
return response?.headers?.get(TRACE_ID_HEADER) ?? undefined
28+
}
29+
30+
export function appendTraceIdToMessage(
31+
message: string,
32+
traceId: string | undefined
33+
): string {
34+
if (!traceId) {
35+
return message
36+
}
37+
38+
if (message.toLowerCase().includes('x-trace-id:')) {
39+
return message
40+
}
41+
42+
return `${message}\nX-Trace-ID: ${traceId}`
43+
}
44+
45+
export function createTraceIdMiddleware(): Middleware {
46+
return {
47+
async onResponse({ response }) {
48+
setLastTraceId(getTraceIdFromResponse(response))
49+
return response
50+
},
51+
}
52+
}
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
import { afterAll, afterEach, beforeAll, describe, expect, it } from 'vitest'
2+
import { http, HttpResponse } from 'msw'
3+
import { setupServer } from 'msw/node'
4+
5+
import { handleApiError } from '../src/api'
6+
import { handleEnvdApiError } from '../src/envd/api'
7+
import { Sandbox, getLastTraceId } from '../src'
8+
import { SandboxError } from '../src/errors'
9+
10+
const server = setupServer(
11+
http.get(
12+
'https://api.sandbox.ucloudai.com/sandboxes/:sandboxID',
13+
async () => {
14+
return HttpResponse.json(
15+
{ code: 404, message: 'not found' },
16+
{
17+
status: 404,
18+
headers: {
19+
'X-Trace-ID': 'trace-404',
20+
},
21+
}
22+
)
23+
}
24+
)
25+
)
26+
27+
beforeAll(() => server.listen({ onUnhandledRequest: 'error' }))
28+
afterEach(() => server.resetHandlers())
29+
afterAll(() => server.close())
30+
31+
describe('X-Trace-ID propagation', () => {
32+
it('appends X-Trace-ID to handleApiError messages', () => {
33+
const response = new Response(
34+
JSON.stringify({ code: 500, message: 'internal error' }),
35+
{
36+
status: 500,
37+
headers: {
38+
'X-Trace-ID': 'trace-500',
39+
},
40+
}
41+
)
42+
43+
const err = handleApiError({
44+
error: { message: 'internal error' },
45+
response,
46+
} as any)
47+
48+
expect(err).toBeInstanceOf(SandboxError)
49+
expect(err?.message).toContain('X-Trace-ID: trace-500')
50+
expect((err as any)?.traceId).toBe('trace-500')
51+
})
52+
53+
it('appends X-Trace-ID to handleEnvdApiError messages', async () => {
54+
const response = new Response('unauthorized', {
55+
status: 401,
56+
headers: {
57+
'X-Trace-ID': 'trace-envd-401',
58+
},
59+
})
60+
61+
const err = await handleEnvdApiError({
62+
error: 'unauthorized',
63+
response,
64+
})
65+
66+
expect(err?.message).toContain('X-Trace-ID: trace-envd-401')
67+
expect((err as any)?.traceId).toBe('trace-envd-401')
68+
})
69+
70+
it('includes X-Trace-ID in NotFoundError messages and updates last trace id', async () => {
71+
await expect(Sandbox.getInfo('missing-sandbox')).rejects.toMatchObject({
72+
message: expect.stringContaining('X-Trace-ID: trace-404'),
73+
})
74+
75+
expect(getLastTraceId()).toBe('trace-404')
76+
})
77+
})
78+

packages/js-sdk/vitest.config.ts

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import { defineConfig } from 'vitest/config'
2+
import { config as loadDotenv } from 'dotenv'
3+
4+
const env = loadDotenv()
5+
6+
export default defineConfig({
7+
test: {
8+
include: ['tests/**/*.test.ts'],
9+
exclude: [
10+
'tests/runtimes/**',
11+
'tests/integration/**',
12+
'tests/template/**',
13+
'tests/connectionConfig.test.ts',
14+
],
15+
isolate: false,
16+
globals: false,
17+
testTimeout: 30_000,
18+
environment: 'node',
19+
bail: 0,
20+
deps: {
21+
interopDefault: true,
22+
},
23+
env: {
24+
...(process.env as Record<string, string>),
25+
...env.parsed,
26+
},
27+
},
28+
})
29+

src/commands/index.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,3 +19,12 @@ Visit ${asPrimary(
1919
.addCommand(authCommand)
2020
.addCommand(templateCommand)
2121
.addCommand(sandboxCommand)
22+
23+
function addDebugOption(cmd: commander.Command) {
24+
cmd.option('--debug', 'print Trace ID for debugging')
25+
for (const subcommand of cmd.commands) {
26+
addDebugOption(subcommand)
27+
}
28+
}
29+
30+
addDebugOption(program)

0 commit comments

Comments
 (0)