From d2428355b1c6a9bf45bd77a8a6df56430b1bb6a2 Mon Sep 17 00:00:00 2001
From: Ghost Jake <89829542+Deepak-Kesavan@users.noreply.github.com>
Date: Tue, 12 May 2026 09:15:49 +0530
Subject: [PATCH 1/5] UN-3444 [FEAT] Propagate frontend request ID and surface
it on error notifications
- Add `X-Request-ID` request interceptor (uuidv4) on the global axios instance
and on each `useAxiosPrivate` axios instance so every API call carries a
client-generated correlation ID. Django/Flask backends already honor
incoming `X-Request-ID`, so backend logs reuse the same value.
- Extract the request ID in `useExceptionHandler` from response headers with
a fallback to the outgoing request headers (covers network/cancel errors).
- Thread `requestId` through the alert store and render it in the error
notification with an antd `Typography.Text copyable` for a one-click copy.
---
frontend/src/App.jsx | 33 ++++++++++++++++++++--
frontend/src/helpers/requestId.js | 22 +++++++++++++++
frontend/src/hooks/useAxiosPrivate.js | 3 ++
frontend/src/hooks/useExceptionHandler.jsx | 27 ++++++++++++------
frontend/src/index.css | 18 ++++++++++++
frontend/src/store/alert-store.js | 1 +
6 files changed, 94 insertions(+), 10 deletions(-)
create mode 100644 frontend/src/helpers/requestId.js
diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx
index 2a21fd639c..1e6f72a4f7 100644
--- a/frontend/src/App.jsx
+++ b/frontend/src/App.jsx
@@ -1,4 +1,5 @@
-import { Button, ConfigProvider, notification, theme } from "antd";
+import { Button, ConfigProvider, notification, Typography, theme } from "antd";
+import axios from "axios";
import { useEffect } from "react";
import { HelmetProvider } from "react-helmet-async";
import { BrowserRouter } from "react-router-dom";
@@ -6,12 +7,15 @@ import { GenericLoader } from "./components/generic-loader/GenericLoader";
import CustomMarkdown from "./components/helpers/custom-markdown/CustomMarkdown.jsx";
import { PageTitle } from "./components/widgets/page-title/PageTitle.jsx";
import { THEME } from "./helpers/GetStaticData.js";
+import { attachRequestIdInterceptor } from "./helpers/requestId.js";
import PostHogPageviewTracker from "./PostHogPageviewTracker.js";
import { Router } from "./routes/Router.jsx";
import { useAlertStore } from "./store/alert-store.js";
import { useSessionStore } from "./store/session-store.js";
import { useSocketLogsStore } from "./store/socket-logs-store.js";
+attachRequestIdInterceptor(axios);
+
let GoogleTagManagerHelper;
try {
const mod = await import(
@@ -53,9 +57,34 @@ function App() {
return;
}
+ const showRequestId =
+ alertDetails?.type === "error" && alertDetails?.requestId;
+ const description = (
+ <>
+
+ {showRequestId && (
+
+
+ Request ID:
+ {" "}
+
+ {alertDetails?.requestId}
+
+
+ )}
+ >
+ );
+
notificationAPI.open({
message: alertDetails?.title,
- description: ,
+ description,
type: alertDetails?.type,
duration: alertDetails?.duration,
btn,
diff --git a/frontend/src/helpers/requestId.js b/frontend/src/helpers/requestId.js
new file mode 100644
index 0000000000..1ca57febae
--- /dev/null
+++ b/frontend/src/helpers/requestId.js
@@ -0,0 +1,22 @@
+import { v4 as uuidv4 } from "uuid";
+
+const REQUEST_ID_HEADER = "X-Request-ID";
+
+const attachRequestIdInterceptor = (axiosInstance) => {
+ return axiosInstance.interceptors.request.use((config) => {
+ if (!config.headers[REQUEST_ID_HEADER]) {
+ config.headers[REQUEST_ID_HEADER] = uuidv4();
+ }
+ return config;
+ });
+};
+
+const getRequestIdFromError = (err) => {
+ return (
+ err?.response?.headers?.[REQUEST_ID_HEADER.toLowerCase()] ||
+ err?.response?.headers?.[REQUEST_ID_HEADER] ||
+ err?.config?.headers?.[REQUEST_ID_HEADER]
+ );
+};
+
+export { REQUEST_ID_HEADER, attachRequestIdInterceptor, getRequestIdFromError };
diff --git a/frontend/src/hooks/useAxiosPrivate.js b/frontend/src/hooks/useAxiosPrivate.js
index 9d86b2f653..283ce55e24 100644
--- a/frontend/src/hooks/useAxiosPrivate.js
+++ b/frontend/src/hooks/useAxiosPrivate.js
@@ -1,6 +1,7 @@
import axios from "axios";
import { useEffect, useMemo } from "react";
+import { attachRequestIdInterceptor } from "../helpers/requestId";
import useLogout from "./useLogout";
function useAxiosPrivate() {
@@ -8,6 +9,7 @@ function useAxiosPrivate() {
const axiosPrivate = useMemo(() => axios.create(), []);
useEffect(() => {
+ const requestInterceptor = attachRequestIdInterceptor(axiosPrivate);
const responseInterceptor = axiosPrivate.interceptors.response.use(
(response) => {
return response;
@@ -22,6 +24,7 @@ function useAxiosPrivate() {
);
return () => {
+ axiosPrivate.interceptors.request.eject(requestInterceptor);
axiosPrivate.interceptors.response.eject(responseInterceptor);
};
}, []);
diff --git a/frontend/src/hooks/useExceptionHandler.jsx b/frontend/src/hooks/useExceptionHandler.jsx
index 1a43811d34..16500322d3 100644
--- a/frontend/src/hooks/useExceptionHandler.jsx
+++ b/frontend/src/hooks/useExceptionHandler.jsx
@@ -1,14 +1,17 @@
import PropTypes from "prop-types";
import { useNavigate } from "react-router-dom";
+import { getRequestIdFromError } from "../helpers/requestId";
+
const useExceptionHandler = () => {
const navigate = useNavigate();
- const buildAlert = (content, title, duration) => ({
+ const buildAlert = (content, title, duration, requestId) => ({
type: "error",
content,
title,
duration,
+ requestId,
});
const handleException = (
@@ -18,17 +21,24 @@ const useExceptionHandler = () => {
title = "Failed",
duration = 0,
) => {
+ const requestId = getRequestIdFromError(err);
if (!err) {
- return buildAlert(errMessage, title, duration);
+ return buildAlert(errMessage, title, duration, requestId);
}
if (err.code === "ERR_NETWORK" && !navigator.onLine) {
return buildAlert(
"Please check your internet connection.",
title,
duration,
+ requestId,
);
} else if (err.code === "ERR_CANCELED") {
- return buildAlert("Request has been canceled.", title, duration);
+ return buildAlert(
+ "Request has been canceled.",
+ title,
+ duration,
+ requestId,
+ );
}
if (err?.response?.data) {
@@ -40,7 +50,7 @@ const useExceptionHandler = () => {
responseData.error || responseData.detail || responseData.message;
if (commonErrorMessage) {
- return buildAlert(commonErrorMessage, title, duration);
+ return buildAlert(commonErrorMessage, title, duration, requestId);
}
// Then handle specific error types
@@ -73,24 +83,25 @@ const useExceptionHandler = () => {
.join("\n");
}
}
- return buildAlert(errorMessage, title, duration);
+ return buildAlert(errorMessage, title, duration, requestId);
}
break;
case "subscription_error":
navigate("/subscription-expired");
- return buildAlert(errors, title, duration);
+ return buildAlert(errors, title, duration, requestId);
case "client_error":
case "server_error":
return buildAlert(
errors?.[0]?.detail ? errors[0].detail : errMessage,
title,
duration,
+ requestId,
);
default:
- return buildAlert(errMessage, title, duration);
+ return buildAlert(errMessage, title, duration, requestId);
}
} else {
- return buildAlert(errMessage, title, duration);
+ return buildAlert(errMessage, title, duration, requestId);
}
};
diff --git a/frontend/src/index.css b/frontend/src/index.css
index 244be08cfe..2af318dbb9 100644
--- a/frontend/src/index.css
+++ b/frontend/src/index.css
@@ -251,3 +251,21 @@ body {
background-color: #ffffff;
z-index: 2000;
}
+
+.notification-request-id {
+ margin-top: 8px;
+ font-size: 12px;
+ display: flex;
+ align-items: center;
+ flex-wrap: wrap;
+ gap: 4px;
+}
+
+.notification-request-id__label {
+ font-size: 12px;
+}
+
+.notification-request-id__value {
+ font-size: 12px;
+ word-break: break-all;
+}
diff --git a/frontend/src/store/alert-store.js b/frontend/src/store/alert-store.js
index 24549e09ce..25827813ab 100644
--- a/frontend/src/store/alert-store.js
+++ b/frontend/src/store/alert-store.js
@@ -13,6 +13,7 @@ const STORE_VARIABLES = {
title: "",
duration: DEFAULT_DURATION,
key: null,
+ requestId: null,
},
};
From 0fa515b8e950a84ee859e538d2ea7f8751a19d27 Mon Sep 17 00:00:00 2001
From: Ghost Jake <89829542+Deepak-Kesavan@users.noreply.github.com>
Date: Tue, 12 May 2026 09:40:27 +0530
Subject: [PATCH 2/5] UN-3444 [FIX] Address PR review on request-ID helpers
- Use nullish coalescing in `getRequestIdFromError` so an empty-string
backend-echoed header isn't silently shadowed by the request-side header.
- Drop the redundant mixed-case response-header lookup; browser/axios
response headers are always lower-cased.
- Export the global axios interceptor ID so HMR-friendly cleanup is
possible (and the chain doesn't leak across module re-evals).
---
frontend/src/App.jsx | 2 +-
frontend/src/helpers/requestId.js | 3 +--
2 files changed, 2 insertions(+), 3 deletions(-)
diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx
index 1e6f72a4f7..9534dae9e9 100644
--- a/frontend/src/App.jsx
+++ b/frontend/src/App.jsx
@@ -14,7 +14,7 @@ import { useAlertStore } from "./store/alert-store.js";
import { useSessionStore } from "./store/session-store.js";
import { useSocketLogsStore } from "./store/socket-logs-store.js";
-attachRequestIdInterceptor(axios);
+export const globalRequestIdInterceptor = attachRequestIdInterceptor(axios);
let GoogleTagManagerHelper;
try {
diff --git a/frontend/src/helpers/requestId.js b/frontend/src/helpers/requestId.js
index 1ca57febae..c483993745 100644
--- a/frontend/src/helpers/requestId.js
+++ b/frontend/src/helpers/requestId.js
@@ -13,8 +13,7 @@ const attachRequestIdInterceptor = (axiosInstance) => {
const getRequestIdFromError = (err) => {
return (
- err?.response?.headers?.[REQUEST_ID_HEADER.toLowerCase()] ||
- err?.response?.headers?.[REQUEST_ID_HEADER] ||
+ err?.response?.headers?.[REQUEST_ID_HEADER.toLowerCase()] ??
err?.config?.headers?.[REQUEST_ID_HEADER]
);
};
From 381a1d91c7a8b17dac8c4b55c5f1b3a9f076efb6 Mon Sep 17 00:00:00 2001
From: Ghost Jake <89829542+Deepak-Kesavan@users.noreply.github.com>
Date: Tue, 19 May 2026 10:01:01 +0530
Subject: [PATCH 3/5] UN-3444 [FEAT] Append request ID to error log rows in the
bottom Logs panel
Notification rows in the Logs & Notifications table now include the
request ID on a second line (markdown inline-code) so users can reference
it without opening the toast.
---
frontend/src/App.jsx | 6 +++++-
1 file changed, 5 insertions(+), 1 deletion(-)
diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx
index 9534dae9e9..99204c129d 100644
--- a/frontend/src/App.jsx
+++ b/frontend/src/App.jsx
@@ -91,11 +91,15 @@ function App() {
key: alertDetails?.key,
});
+ const logMessage = showRequestId
+ ? `${alertDetails.content}\nRequest ID: \`${alertDetails.requestId}\``
+ : alertDetails.content;
+
pushLogMessages([
{
timestamp: Math.floor(Date.now() / 1000),
level: alertDetails?.type ? alertDetails?.type.toUpperCase() : "",
- message: alertDetails.content,
+ message: logMessage,
type: "NOTIFICATION",
},
]);
From a07d5487cd24f8ba513a3b186cf178f7b415629e Mon Sep 17 00:00:00 2001
From: Ghost Jake <89829542+Deepak-Kesavan@users.noreply.github.com>
Date: Tue, 19 May 2026 11:29:49 +0530
Subject: [PATCH 4/5] UN-3444 [FIX] Address review on request-ID helpers
- Use AxiosHeaders.set(name, value, false) when available so the
case-insensitive normalization is respected and caller-supplied IDs
are preserved; fall back to bracket access for plain-object headers.
- Guard against undefined config.headers on hand-built request configs.
- Collapse buildAlert into a local alert(content) closure inside
handleException so each return site stops repeating title/duration/
requestId. Default requestId to null to match the alert-store shape.
- Drop the stale, function-shaped useExceptionHandler.propTypes block.
- Make the global axios attach HMR-safe with a Symbol-based idempotency
guard; remove the unused exported handle.
- Consolidate the .notification-request-id font-size rules and remove
the now-empty __label class.
- Add vitest coverage for the helper (injection, no-overwrite, distinct
IDs per request, fallback chain, null/empty inputs).
---
frontend/src/App.jsx | 13 ++-
frontend/src/helpers/requestId.js | 15 +++-
frontend/src/helpers/requestId.test.js | 95 ++++++++++++++++++++++
frontend/src/hooks/useExceptionHandler.jsx | 57 ++++---------
frontend/src/index.css | 11 ++-
5 files changed, 136 insertions(+), 55 deletions(-)
create mode 100644 frontend/src/helpers/requestId.test.js
diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx
index 99204c129d..149347f18f 100644
--- a/frontend/src/App.jsx
+++ b/frontend/src/App.jsx
@@ -14,7 +14,11 @@ import { useAlertStore } from "./store/alert-store.js";
import { useSessionStore } from "./store/session-store.js";
import { useSocketLogsStore } from "./store/socket-logs-store.js";
-export const globalRequestIdInterceptor = attachRequestIdInterceptor(axios);
+const GLOBAL_INTERCEPTOR_FLAG = Symbol.for("unstract.requestIdInterceptor");
+if (!axios[GLOBAL_INTERCEPTOR_FLAG]) {
+ attachRequestIdInterceptor(axios);
+ axios[GLOBAL_INTERCEPTOR_FLAG] = true;
+}
let GoogleTagManagerHelper;
try {
@@ -64,12 +68,7 @@ function App() {
{showRequestId && (
-
- Request ID:
- {" "}
+ Request ID:{" "}
{
+ if (typeof headers.set === "function") {
+ headers.set(REQUEST_ID_HEADER, value, false);
+ return;
+ }
+ if (!headers[REQUEST_ID_HEADER]) {
+ headers[REQUEST_ID_HEADER] = value;
+ }
+};
+
const attachRequestIdInterceptor = (axiosInstance) => {
return axiosInstance.interceptors.request.use((config) => {
- if (!config.headers[REQUEST_ID_HEADER]) {
- config.headers[REQUEST_ID_HEADER] = uuidv4();
- }
+ config.headers ??= {};
+ setHeaderIfMissing(config.headers, uuidv4());
return config;
});
};
diff --git a/frontend/src/helpers/requestId.test.js b/frontend/src/helpers/requestId.test.js
new file mode 100644
index 0000000000..b38837bafc
--- /dev/null
+++ b/frontend/src/helpers/requestId.test.js
@@ -0,0 +1,95 @@
+import axios from "axios";
+import { describe, expect, it } from "vitest";
+
+import {
+ attachRequestIdInterceptor,
+ getRequestIdFromError,
+ REQUEST_ID_HEADER,
+} from "./requestId";
+
+const UUID_V4_REGEX =
+ /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
+
+const runRequestInterceptors = async (instance, config = {}) => {
+ let current = { ...config, headers: { ...(config.headers || {}) } };
+ for (const handler of instance.interceptors.request.handlers) {
+ if (handler && handler.fulfilled) {
+ current = await handler.fulfilled(current);
+ }
+ }
+ return current;
+};
+
+describe("attachRequestIdInterceptor", () => {
+ it("injects a v4 UUID when the header is absent", async () => {
+ const instance = axios.create();
+ attachRequestIdInterceptor(instance);
+
+ const result = await runRequestInterceptors(instance);
+
+ expect(result.headers[REQUEST_ID_HEADER]).toMatch(UUID_V4_REGEX);
+ });
+
+ it("does NOT overwrite a caller-supplied X-Request-ID", async () => {
+ const instance = axios.create();
+ attachRequestIdInterceptor(instance);
+
+ const supplied = "caller-provided-id";
+ const result = await runRequestInterceptors(instance, {
+ headers: { [REQUEST_ID_HEADER]: supplied },
+ });
+
+ expect(result.headers[REQUEST_ID_HEADER]).toBe(supplied);
+ });
+
+ it("generates a distinct ID per request", async () => {
+ const instance = axios.create();
+ attachRequestIdInterceptor(instance);
+
+ const first = await runRequestInterceptors(instance);
+ const second = await runRequestInterceptors(instance);
+
+ expect(first.headers[REQUEST_ID_HEADER]).not.toBe(
+ second.headers[REQUEST_ID_HEADER],
+ );
+ });
+
+ it("creates a headers object when one is missing on the config", async () => {
+ const instance = axios.create();
+ attachRequestIdInterceptor(instance);
+
+ let current = {};
+ for (const handler of instance.interceptors.request.handlers) {
+ if (handler && handler.fulfilled) {
+ current = await handler.fulfilled(current);
+ }
+ }
+
+ expect(current.headers[REQUEST_ID_HEADER]).toMatch(UUID_V4_REGEX);
+ });
+});
+
+describe("getRequestIdFromError", () => {
+ it("reads the lowercased response header", () => {
+ const err = {
+ response: { headers: { "x-request-id": "from-response" } },
+ config: { headers: { [REQUEST_ID_HEADER]: "from-config" } },
+ };
+
+ expect(getRequestIdFromError(err)).toBe("from-response");
+ });
+
+ it("falls back to the request config header when no response header exists", () => {
+ const err = {
+ config: { headers: { [REQUEST_ID_HEADER]: "from-config" } },
+ };
+
+ expect(getRequestIdFromError(err)).toBe("from-config");
+ });
+
+ it("returns undefined for null/empty error inputs without throwing", () => {
+ expect(getRequestIdFromError(null)).toBeUndefined();
+ expect(getRequestIdFromError(undefined)).toBeUndefined();
+ expect(getRequestIdFromError({})).toBeUndefined();
+ });
+});
diff --git a/frontend/src/hooks/useExceptionHandler.jsx b/frontend/src/hooks/useExceptionHandler.jsx
index 16500322d3..62917a8f0a 100644
--- a/frontend/src/hooks/useExceptionHandler.jsx
+++ b/frontend/src/hooks/useExceptionHandler.jsx
@@ -1,4 +1,3 @@
-import PropTypes from "prop-types";
import { useNavigate } from "react-router-dom";
import { getRequestIdFromError } from "../helpers/requestId";
@@ -6,14 +5,6 @@ import { getRequestIdFromError } from "../helpers/requestId";
const useExceptionHandler = () => {
const navigate = useNavigate();
- const buildAlert = (content, title, duration, requestId) => ({
- type: "error",
- content,
- title,
- duration,
- requestId,
- });
-
const handleException = (
err,
errMessage = "Something went wrong",
@@ -21,24 +12,22 @@ const useExceptionHandler = () => {
title = "Failed",
duration = 0,
) => {
- const requestId = getRequestIdFromError(err);
+ const requestId = getRequestIdFromError(err) ?? null;
+ const alert = (content) => ({
+ type: "error",
+ content,
+ title,
+ duration,
+ requestId,
+ });
+
if (!err) {
- return buildAlert(errMessage, title, duration, requestId);
+ return alert(errMessage);
}
if (err.code === "ERR_NETWORK" && !navigator.onLine) {
- return buildAlert(
- "Please check your internet connection.",
- title,
- duration,
- requestId,
- );
+ return alert("Please check your internet connection.");
} else if (err.code === "ERR_CANCELED") {
- return buildAlert(
- "Request has been canceled.",
- title,
- duration,
- requestId,
- );
+ return alert("Request has been canceled.");
}
if (err?.response?.data) {
@@ -50,7 +39,7 @@ const useExceptionHandler = () => {
responseData.error || responseData.detail || responseData.message;
if (commonErrorMessage) {
- return buildAlert(commonErrorMessage, title, duration, requestId);
+ return alert(commonErrorMessage);
}
// Then handle specific error types
@@ -83,34 +72,24 @@ const useExceptionHandler = () => {
.join("\n");
}
}
- return buildAlert(errorMessage, title, duration, requestId);
+ return alert(errorMessage);
}
break;
case "subscription_error":
navigate("/subscription-expired");
- return buildAlert(errors, title, duration, requestId);
+ return alert(errors);
case "client_error":
case "server_error":
- return buildAlert(
- errors?.[0]?.detail ? errors[0].detail : errMessage,
- title,
- duration,
- requestId,
- );
+ return alert(errors?.[0]?.detail ? errors[0].detail : errMessage);
default:
- return buildAlert(errMessage, title, duration, requestId);
+ return alert(errMessage);
}
} else {
- return buildAlert(errMessage, title, duration, requestId);
+ return alert(errMessage);
}
};
return handleException;
};
-useExceptionHandler.propTypes = {
- err: PropTypes.object, // Assuming err is an object
- errMessage: PropTypes.string,
-};
-
export { useExceptionHandler };
diff --git a/frontend/src/index.css b/frontend/src/index.css
index 2af318dbb9..aa670e91d8 100644
--- a/frontend/src/index.css
+++ b/frontend/src/index.css
@@ -252,20 +252,19 @@ body {
z-index: 2000;
}
+.notification-request-id,
+.notification-request-id__value {
+ font-size: 12px;
+}
+
.notification-request-id {
margin-top: 8px;
- font-size: 12px;
display: flex;
align-items: center;
flex-wrap: wrap;
gap: 4px;
}
-.notification-request-id__label {
- font-size: 12px;
-}
-
.notification-request-id__value {
- font-size: 12px;
word-break: break-all;
}
From adc2a47cd561cd77661f4476db960db91a558922 Mon Sep 17 00:00:00 2001
From: Ghost Jake <89829542+Deepak-Kesavan@users.noreply.github.com>
Date: Tue, 19 May 2026 11:53:07 +0530
Subject: [PATCH 5/5] UN-3444 [FIX] Sonar cleanup on requestId.test.js
- Drop the useless `|| {}` fallback when spreading config.headers.
- Use optional chaining `handler?.fulfilled` instead of `handler && handler.fulfilled`.
---
frontend/src/helpers/requestId.test.js | 6 +++---
1 file changed, 3 insertions(+), 3 deletions(-)
diff --git a/frontend/src/helpers/requestId.test.js b/frontend/src/helpers/requestId.test.js
index b38837bafc..fc637e489e 100644
--- a/frontend/src/helpers/requestId.test.js
+++ b/frontend/src/helpers/requestId.test.js
@@ -11,9 +11,9 @@ const UUID_V4_REGEX =
/^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
const runRequestInterceptors = async (instance, config = {}) => {
- let current = { ...config, headers: { ...(config.headers || {}) } };
+ let current = { ...config, headers: { ...config.headers } };
for (const handler of instance.interceptors.request.handlers) {
- if (handler && handler.fulfilled) {
+ if (handler?.fulfilled) {
current = await handler.fulfilled(current);
}
}
@@ -60,7 +60,7 @@ describe("attachRequestIdInterceptor", () => {
let current = {};
for (const handler of instance.interceptors.request.handlers) {
- if (handler && handler.fulfilled) {
+ if (handler?.fulfilled) {
current = await handler.fulfilled(current);
}
}