Skip to content

Commit 577825e

Browse files
committed
Add button to view full request and response details
1 parent f36de0e commit 577825e

File tree

7 files changed

+259
-12
lines changed

7 files changed

+259
-12
lines changed

services/backend-api/client/src/features/feed/components/UserFeedLogs/DeliveryHistory/index.tsx

+6-7
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,6 @@ import {
1919
Thead,
2020
Tr,
2121
Link as ChakraLink,
22-
IconButton,
2322
Modal,
2423
ModalOverlay,
2524
ModalContent,
@@ -286,16 +285,16 @@ export const DeliveryHistory = () => {
286285
<Skeleton isLoaded={fetchStatus === "idle"}>
287286
{item.details?.message}
288287
{item.details?.data && (
289-
<IconButton
290-
aria-label="View details"
291-
ml={1}
292-
icon={<Search2Icon />}
288+
<Button
289+
leftIcon={<Search2Icon />}
293290
size="xs"
294-
variant="link"
291+
variant="outline"
295292
onClick={() =>
296293
setDetailsData(JSON.stringify(item.details?.data, null, 2))
297294
}
298-
/>
295+
>
296+
View Details
297+
</Button>
299298
)}
300299
</Skeleton>
301300
</Td>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,181 @@
1+
import {
2+
Button,
3+
Heading,
4+
HStack,
5+
Link,
6+
Modal,
7+
ModalBody,
8+
ModalCloseButton,
9+
ModalContent,
10+
ModalFooter,
11+
ModalHeader,
12+
ModalOverlay,
13+
Stack,
14+
Table,
15+
TableContainer,
16+
Tbody,
17+
Td,
18+
Text,
19+
Th,
20+
Thead,
21+
Tr,
22+
useDisclosure,
23+
} from "@chakra-ui/react";
24+
import { cloneElement } from "react";
25+
import { ExternalLinkIcon } from "@chakra-ui/icons";
26+
import dayjs from "dayjs";
27+
import { UserFeedRequest } from "../../../types";
28+
29+
interface Props {
30+
request: UserFeedRequest;
31+
trigger: React.ReactElement;
32+
}
33+
34+
export const RequestDetails = ({ request, trigger }: Props) => {
35+
const { isOpen, onOpen, onClose } = useDisclosure();
36+
37+
const requestHeaders = request.headers
38+
? (Object.entries(request.headers) as [string, string][])
39+
: null;
40+
41+
const responseHeaders = request.response.headers
42+
? (Object.entries(request.response.headers) as [string, string][])
43+
: null;
44+
45+
const statusCode = request.response.statusCode || null;
46+
const durationMs = request.finishedAtIso
47+
? `${new Date(request.finishedAtIso).getTime() - new Date(request.createdAtIso).getTime()}ms`
48+
: null;
49+
50+
return (
51+
<>
52+
{cloneElement(trigger, { onClick: onOpen })}
53+
<Modal isOpen={isOpen} onClose={onClose} size="6xl">
54+
<ModalOverlay />
55+
<ModalContent>
56+
<ModalHeader>Request Details</ModalHeader>
57+
<ModalCloseButton />
58+
<ModalBody>
59+
<Stack spacing={8}>
60+
<Stack spacing={4}>
61+
<Heading size="md" as="h2">
62+
Request
63+
</Heading>
64+
<HStack flexWrap="wrap" gap={16}>
65+
<Stack>
66+
<Heading size="sm" as="h3">
67+
URL
68+
</Heading>
69+
<Link
70+
href={request.url}
71+
target="_blank"
72+
rel="noopener noreferrer"
73+
color="blue.300"
74+
>
75+
<HStack alignItems="center">
76+
<Text wordBreak="break-all">{request.url}</Text>
77+
<ExternalLinkIcon />
78+
</HStack>
79+
</Link>
80+
</Stack>
81+
<Stack>
82+
<Heading size="sm" as="h3">
83+
Initiated At
84+
</Heading>
85+
<Text>{dayjs(request.createdAtIso).format("DD MMM YYYY, HH:mm:ss.SSS")}</Text>
86+
</Stack>
87+
</HStack>
88+
<Heading size="sm" as="h3">
89+
Headers
90+
</Heading>
91+
{!requestHeaders && <Text color="gray.400">No request headers available.</Text>}
92+
{!!requestHeaders && (
93+
<TableContainer bg="gray.800" p={4} borderRadius="md">
94+
<Table size="sm" variant="simple">
95+
<Thead>
96+
<Tr>
97+
<Th>Name</Th>
98+
<Th>Value</Th>
99+
</Tr>
100+
</Thead>
101+
<Tbody>
102+
{requestHeaders.map(([key, val]) => {
103+
return (
104+
<Tr key={key}>
105+
<Td>{key}</Td>
106+
<Td overflow="auto">{val}</Td>
107+
</Tr>
108+
);
109+
})}
110+
</Tbody>
111+
</Table>
112+
</TableContainer>
113+
)}
114+
</Stack>
115+
<Stack spacing={4}>
116+
<Heading size="md" as="h2">
117+
Response
118+
</Heading>
119+
<HStack flexWrap="wrap" gap={16}>
120+
<Stack>
121+
<Heading size="sm" as="h3">
122+
Status Code
123+
</Heading>
124+
<Text>{statusCode || "N/A"}</Text>
125+
</Stack>
126+
<Stack>
127+
<Heading size="sm" as="h3">
128+
Received At
129+
</Heading>
130+
<Text>
131+
{request.finishedAtIso
132+
? dayjs(request.finishedAtIso).format("DD MMM YYYY, HH:mm:ss.SSS")
133+
: "N/A"}
134+
</Text>
135+
</Stack>
136+
<Stack>
137+
<Heading size="sm" as="h3">
138+
Duration
139+
</Heading>
140+
<Text>{durationMs || "N/A"}</Text>
141+
</Stack>
142+
</HStack>
143+
<Heading size="sm" as="h3">
144+
Headers
145+
</Heading>
146+
{!responseHeaders && <Text color="gray.400">No response headers available.</Text>}
147+
{!!responseHeaders && (
148+
<TableContainer bg="gray.800" p={4} borderRadius="md">
149+
<Table size="sm" variant="simple">
150+
<Thead>
151+
<Tr>
152+
<Th>Name</Th>
153+
<Th>Value</Th>
154+
</Tr>
155+
</Thead>
156+
<Tbody>
157+
{responseHeaders.map(([key, val]) => {
158+
return (
159+
<Tr key={key}>
160+
<Td>{key}</Td>
161+
<Td>{val}</Td>
162+
</Tr>
163+
);
164+
})}
165+
</Tbody>
166+
</Table>
167+
</TableContainer>
168+
)}
169+
</Stack>
170+
</Stack>
171+
</ModalBody>
172+
<ModalFooter>
173+
<Button colorScheme="blue" mr={3} onClick={onClose}>
174+
Close
175+
</Button>
176+
</ModalFooter>
177+
</ModalContent>
178+
</Modal>
179+
</>
180+
);
181+
};

services/backend-api/client/src/features/feed/components/UserFeedLogs/RequestHistory/index.tsx

+20-1
Original file line numberDiff line numberDiff line change
@@ -28,12 +28,13 @@ import {
2828
} from "@chakra-ui/react";
2929
import { useTranslation } from "react-i18next";
3030
import dayjs from "dayjs";
31-
import { QuestionOutlineIcon } from "@chakra-ui/icons";
31+
import { QuestionOutlineIcon, Search2Icon } from "@chakra-ui/icons";
3232
import { forwardRef } from "react";
3333
import { useUserFeedRequestsWithPagination } from "../../../hooks";
3434
import { UserFeedRequestStatus } from "../../../types";
3535
import { InlineErrorAlert } from "../../../../../components";
3636
import { useUserFeedContext } from "../../../../../contexts/UserFeedContext";
37+
import { RequestDetails } from "./RequestDetails";
3738

3839
const QuestionOutlineComponent = forwardRef<any>((props, ref) => (
3940
<QuestionOutlineIcon fontSize={12} tabIndex={-1} ref={ref} aria-hidden {...props} />
@@ -188,6 +189,7 @@ export const RequestHistory = () => {
188189
</PopoverContent>
189190
</Popover>
190191
</Th>
192+
<Th>Details</Th>
191193
</Tr>
192194
</Thead>
193195
<Tbody>
@@ -212,6 +214,23 @@ export const RequestHistory = () => {
212214
: "N/A"}
213215
</Skeleton>
214216
</Td>
217+
<Td>
218+
<Skeleton isLoaded={fetchStatus === "idle"}>
219+
<RequestDetails
220+
trigger={
221+
<Button
222+
leftIcon={<Search2Icon />}
223+
variant="outline"
224+
size="xs"
225+
onClick={() => {}}
226+
>
227+
View Details
228+
</Button>
229+
}
230+
request={req}
231+
/>
232+
</Skeleton>
233+
</Td>
215234
</Tr>
216235
))}
217236
</Tbody>

services/backend-api/client/src/features/feed/types/UserFeedRequest.ts

+5
Original file line numberDiff line numberDiff line change
@@ -12,10 +12,15 @@ export enum UserFeedRequestStatus {
1212

1313
export const UserFeedRequestSchema = object({
1414
id: string().required(),
15+
url: string().required(),
1516
status: string().oneOf(Object.values(UserFeedRequestStatus)).required(),
1617
createdAt: number().required(),
18+
createdAtIso: string().required(),
19+
finishedAtIso: string().optional().nullable(),
20+
headers: object().nullable(),
1721
response: object({
1822
statusCode: number().nullable(),
23+
headers: object().nullable(),
1924
}).required(),
2025
freshnessLifetimeMs: number().nullable().optional(),
2126
});

services/backend-api/client/src/mocks/data/userFeedRequests.ts

+30
Original file line numberDiff line numberDiff line change
@@ -5,39 +5,69 @@ export const mockUserFeedRequests: UserFeedRequest[] = [
55
id: "1",
66
status: UserFeedRequestStatus.OK,
77
createdAt: Math.floor(new Date(2020).getTime() / 1000),
8+
createdAtIso: new Date(2020).toISOString(),
9+
finishedAtIso: new Date(2020).toISOString(),
10+
freshnessLifetimeMs: 0,
11+
url: "https://example.com",
12+
headers: {},
813
response: {
14+
headers: {},
915
statusCode: 200,
1016
},
1117
},
1218
{
1319
id: "2",
1420
status: UserFeedRequestStatus.INTERNAL_ERROR,
1521
createdAt: Math.floor(new Date(2021).getTime() / 1000),
22+
createdAtIso: new Date(2020).toISOString(),
23+
finishedAtIso: new Date(2020).toISOString(),
24+
freshnessLifetimeMs: 0,
25+
url: "https://example.com",
26+
headers: {},
1627
response: {
28+
headers: {},
1729
statusCode: 200,
1830
},
1931
},
2032
{
2133
id: "3",
2234
status: UserFeedRequestStatus.FETCH_ERROR,
2335
createdAt: Math.floor(new Date(2022).getTime() / 1000),
36+
createdAtIso: new Date(2020).toISOString(),
37+
finishedAtIso: new Date(2020).toISOString(),
38+
freshnessLifetimeMs: 0,
39+
url: "https://example.com",
40+
headers: {},
2441
response: {
42+
headers: {},
2543
statusCode: 200,
2644
},
2745
},
2846
{
2947
id: "4",
3048
status: UserFeedRequestStatus.PARSE_ERROR,
3149
createdAt: Math.floor(new Date(2023).getTime() / 1000),
50+
createdAtIso: new Date(2020).toISOString(),
51+
finishedAtIso: new Date(2020).toISOString(),
52+
freshnessLifetimeMs: 0,
53+
url: "https://example.com",
54+
headers: {},
3255
response: {
56+
headers: {},
3357
statusCode: 200,
3458
},
3559
},
3660
{
3761
id: "4",
3862
status: UserFeedRequestStatus.BAD_STATUS_CODE,
3963
createdAt: Math.floor(new Date(2023).getTime() / 1000),
64+
createdAtIso: new Date(2020).toISOString(),
65+
finishedAtIso: new Date(2020).toISOString(),
66+
freshnessLifetimeMs: 0,
67+
url: "https://example.com",
68+
headers: {},
4069
response: {
70+
headers: {},
4171
statusCode: 403,
4272
},
4373
},

services/feed-requests/src/feed-fetcher/feed-fetcher.controller.ts

+2
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,8 @@ export class FeedFetcherController {
6969
result: {
7070
requests: requests.map((r) => ({
7171
createdAt: dayjs(r.createdAt).unix(),
72+
finishedAtIso: r.finishedAt?.toISOString(),
73+
createdAtIso: r.createdAt.toISOString(),
7274
id: r.id,
7375
url: r.url,
7476
status: r.status,

services/feed-requests/src/partitioned-requests-store/partitioned-requests-store.service.ts

+15-4
Original file line numberDiff line numberDiff line change
@@ -261,12 +261,22 @@ export default class PartitionedRequestsStoreService {
261261
limit: number;
262262
url: string;
263263
lookupKey?: string;
264-
}): Promise<Request[]> {
264+
}) {
265265
const em = this.orm.em.getConnection();
266266

267-
const results = await em.execute(
267+
const results: Array<{
268+
id: number;
269+
url: string;
270+
created_at: Date;
271+
next_retry_date: Date;
272+
status: RequestStatus;
273+
response_status_code: number | null;
274+
fetch_options: Record<string, string> | null;
275+
response_headers: Record<string, string> | null;
276+
request_initiated_at: Date | null;
277+
}> = await em.execute(
268278
`SELECT id, url, created_at, next_retry_date, status, response_status_code,` +
269-
` fetch_options, response_headers FROM request_partitioned
279+
` fetch_options, response_headers, request_initiated_at FROM request_partitioned
270280
WHERE lookup_key = ?
271281
ORDER BY created_at DESC
272282
LIMIT ?
@@ -276,7 +286,8 @@ export default class PartitionedRequestsStoreService {
276286

277287
return results.map((result) => ({
278288
id: result.id,
279-
createdAt: new Date(result.created_at),
289+
createdAt: new Date(result.request_initiated_at || result.created_at),
290+
finishedAt: new Date(result.created_at),
280291
nextRetryDate: result.next_retry_date,
281292
url: result.url,
282293
status: result.status,

0 commit comments

Comments
 (0)