Skip to content

Commit b99ef4b

Browse files
kaladinlightclaude
andauthored
fix(swap-service): trim status polling logs to one line per event (#59)
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
1 parent c2773a8 commit b99ef4b

7 files changed

Lines changed: 85 additions & 10 deletions

File tree

‎apps/swap-service/.env.example‎

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@ VITE_CHAINFLIP_API_KEY=
99
VITE_NEAR_INTENTS_API_KEY=
1010
VITE_RELAY_API_KEY=
1111
VITE_ACROSS_API_KEY=
12-
VITE_BOB_GATEWAY_API_KEY=
1312

1413
# Database Configuration
1514
DATABASE_URL="postgresql://postgres:password@db:5432/microservices"

‎apps/swap-service/src/env.ts‎

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,6 @@ const schema = z.object({
1919
VITE_CHAINFLIP_API_KEY: z.string().min(1),
2020
VITE_RELAY_API_KEY: z.string().min(1),
2121
VITE_ACROSS_API_KEY: z.string().min(1),
22-
VITE_BOB_GATEWAY_API_KEY: z.string().length(32),
2322

2423
// Database
2524
DATABASE_URL: z.string().min(1),

‎apps/swap-service/src/polling/swap-polling.service.ts‎

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ import { WebsocketGateway } from '../websocket/websocket.gateway'
77

88
const POLL_CONCURRENCY = 10
99

10+
const swapIds = (swaps: Swap[]): string => swaps.map((swap) => swap.swapId).join(', ')
11+
1012
@Injectable()
1113
export class SwapPollingService {
1214
private readonly logger = new Logger(SwapPollingService.name)
@@ -28,7 +30,7 @@ export class SwapPollingService {
2830
const swaps = await this.swapsService.getPendingTxSwaps()
2931
if (swaps.length === 0) return
3032

31-
this.logger.log(`Polling tx status for ${swaps.length} swaps`)
33+
this.logger.log(`Polling tx status for ${swaps.length} swaps (${swapIds(swaps)})`)
3234
await this.runWorkers(swaps, (swap) => this.pollTxStatus(swap))
3335
} catch (err) {
3436
this.logger.error('Failed to poll pending tx status:', err)
@@ -46,7 +48,7 @@ export class SwapPollingService {
4648
const swaps = await this.swapsService.getPendingVerificationSwaps()
4749
if (swaps.length === 0) return
4850

49-
this.logger.log(`Polling verification for ${swaps.length} swaps`)
51+
this.logger.log(`Polling verification for ${swaps.length} swaps (${swapIds(swaps)})`)
5052
await this.runWorkers(swaps, (swap) => this.pollVerification(swap))
5153
} catch (err) {
5254
this.logger.error('Failed to poll pending verification:', err)

‎apps/swap-service/src/swaps/__tests__/utils.test.ts‎

Lines changed: 54 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { Logger } from '@nestjs/common'
33
import { mayachainAssetId } from '@shapeshiftoss/caip'
44

55
import type { Swap } from '../types'
6-
import { calculateFeeForSwap } from '../utils'
6+
import { calculateFeeForSwap, describeError } from '../utils'
77

88
// Minimal swap shape exercising calculateFeeForSwap's fee/volume math. CACAO fee asset so a stored
99
// '0' fee amount resolves to actualFeeUsd = 0 (the real 0-bps case this branch introduced).
@@ -80,3 +80,56 @@ describe('calculateFeeForSwap volume reconstruction', () => {
8080
expect(result?.volumeUsd).toBe(100)
8181
})
8282
})
83+
84+
describe('describeError', () => {
85+
// Shaped like a real AxiosError: isAxiosError is the flag axios.isAxiosError checks.
86+
const axiosError = (status: number | undefined, data?: unknown, message = 'Request failed with status code 500') =>
87+
Object.assign(new Error(message), {
88+
isAxiosError: true,
89+
response: status === undefined ? undefined : { status, data },
90+
})
91+
92+
it('prefers the message the server sent over the generic axios message', () => {
93+
expect(describeError(axiosError(404, { message: 'tx not found' }))).toBe('tx not found')
94+
})
95+
96+
it('reads an error key when the body has no message', () => {
97+
expect(describeError(axiosError(429, { error: 'rate limited' }))).toBe('rate limited')
98+
})
99+
100+
it('serialises a message or error that is not a string', () => {
101+
expect(describeError(axiosError(400, { error: { message: 'nested' } }))).toBe('{"message":"nested"}')
102+
})
103+
104+
it('falls back when the body has neither key', () => {
105+
const reason = describeError(
106+
axiosError(400, { errors: { amount: ['too small'] } }, 'Request failed with status code 400'),
107+
)
108+
109+
expect(reason).toBe('Request failed with status code 400')
110+
})
111+
112+
it('ignores a string body so an error page never reaches the log', () => {
113+
const reason = describeError(axiosError(403, '<html>'.padEnd(5000, 'x'), 'Request failed with status code 403'))
114+
115+
expect(reason).toBe('Request failed with status code 403')
116+
})
117+
118+
it('falls back to the message for an axios error that never got a response', () => {
119+
expect(describeError(axiosError(undefined, undefined, 'connect ETIMEDOUT'))).toBe('connect ETIMEDOUT')
120+
})
121+
122+
it('returns the message for a plain error', () => {
123+
expect(describeError(new Error('Non-JSON response: HTTP 403 Forbidden'))).toBe(
124+
'Non-JSON response: HTTP 403 Forbidden',
125+
)
126+
})
127+
128+
it('handles a thrown string', () => {
129+
expect(describeError('boom')).toBe('boom')
130+
})
131+
132+
it('does not stringify a thrown object as [object Object]', () => {
133+
expect(describeError({ nope: true })).toBe('Unknown error')
134+
})
135+
})

‎apps/swap-service/src/swaps/swapper-config.ts‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ export const getSwapperConfig = (): SwapperConfig => ({
1717
VITE_ACROSS_API_URL: env.VITE_ACROSS_API_URL,
1818
VITE_ACROSS_INTEGRATOR_ID: '',
1919
VITE_BEBOP_API_KEY: env.VITE_BEBOP_API_KEY,
20-
VITE_BOB_GATEWAY_API_KEY: env.VITE_BOB_GATEWAY_API_KEY,
20+
VITE_BOB_GATEWAY_API_KEY: '',
2121
VITE_CHAINFLIP_API_KEY: env.VITE_CHAINFLIP_API_KEY,
2222
VITE_CHAINFLIP_API_URL: env.VITE_CHAINFLIP_API_URL,
2323
VITE_COWSWAP_BASE_URL: env.VITE_COWSWAP_BASE_URL,

‎apps/swap-service/src/swaps/swaps.service.ts‎

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ import {
3434
buildStatusNotification,
3535
calculateFeeForSwap,
3636
computeSellAmountUsd,
37+
describeError,
3738
fetchUsdPrices,
3839
toSwap,
3940
toSwapperSwap,
@@ -360,8 +361,6 @@ export class SwapsService {
360361
}
361362

362363
async checkSwapStatus(swapId: string): Promise<SwapStatusResponse> {
363-
logger.log(`Checking status for swap: ${swapId}`)
364-
365364
const prismaSwap = await this.prisma.swap.findUnique({ where: { swapId } })
366365
if (!prismaSwap) throw new NotFoundException(`Swap not found: ${swapId}`)
367366

@@ -393,10 +392,13 @@ export class SwapsService {
393392
statusMessage: typeof statusMessage === 'string' ? statusMessage : '',
394393
}
395394
} catch (error) {
396-
logger.error(`Failed to check swap status for ${swapId}:`, error)
395+
const reason = describeError(error)
396+
397+
logger.error(`Failed to check swap status for ${swapId}: ${reason}`)
398+
397399
return {
398400
status: 'PENDING',
399-
statusMessage: `Error polling status: ${error instanceof Error ? error.message : 'Unknown error'}`,
401+
statusMessage: `Error polling status: ${reason}`,
400402
}
401403
}
402404
}

‎apps/swap-service/src/swaps/utils.ts‎

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { Logger } from '@nestjs/common'
22
import type { Swap as PrismaSwap } from '@prisma/client'
3+
import axios from 'axios'
34

45
import type { CreateSwapDto } from '@shapeshift/shared-types'
56
import { baseUnitToPrecision } from '@shapeshift/shared-utils'
@@ -49,6 +50,25 @@ export const toSwapperSwap = (swap: Swap): SwapperSwap =>
4950
updatedAt: swap.updatedAt.getTime(),
5051
}) as unknown as SwapperSwap
5152

53+
const responseDetail = (data: unknown): string | undefined => {
54+
if (!data || typeof data !== 'object') return
55+
56+
const { message, error } = data as { message?: unknown; error?: unknown }
57+
const detail = message ?? error
58+
59+
if (detail === undefined || detail === null) return
60+
61+
return typeof detail === 'string' ? detail : JSON.stringify(detail)
62+
}
63+
64+
export const describeError = (error: unknown): string => {
65+
if (axios.isAxiosError(error)) return responseDetail(error.response?.data) ?? error.message
66+
if (error instanceof Error) return error.message
67+
if (typeof error === 'string') return error
68+
69+
return 'Unknown error'
70+
}
71+
5272
// formatAmount up to 8 decimals, no trailing zeros
5373
export const formatAmount = (amount: string | number): string => {
5474
return bnOrZero(amount)

0 commit comments

Comments
 (0)