diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx
index 2a21fd639c..149347f18f 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,19 @@ 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";
+const GLOBAL_INTERCEPTOR_FLAG = Symbol.for("unstract.requestIdInterceptor");
+if (!axios[GLOBAL_INTERCEPTOR_FLAG]) {
+ attachRequestIdInterceptor(axios);
+ axios[GLOBAL_INTERCEPTOR_FLAG] = true;
+}
+
let GoogleTagManagerHelper;
try {
const mod = await import(
@@ -53,20 +61,44 @@ 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,
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",
},
]);
diff --git a/frontend/src/helpers/requestId.js b/frontend/src/helpers/requestId.js
new file mode 100644
index 0000000000..da4e80f097
--- /dev/null
+++ b/frontend/src/helpers/requestId.js
@@ -0,0 +1,30 @@
+import { v4 as uuidv4 } from "uuid";
+
+const REQUEST_ID_HEADER = "X-Request-ID";
+
+const setHeaderIfMissing = (headers, value) => {
+ 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) => {
+ config.headers ??= {};
+ setHeaderIfMissing(config.headers, uuidv4());
+ return config;
+ });
+};
+
+const getRequestIdFromError = (err) => {
+ return (
+ err?.response?.headers?.[REQUEST_ID_HEADER.toLowerCase()] ??
+ err?.config?.headers?.[REQUEST_ID_HEADER]
+ );
+};
+
+export { REQUEST_ID_HEADER, attachRequestIdInterceptor, getRequestIdFromError };
diff --git a/frontend/src/helpers/requestId.test.js b/frontend/src/helpers/requestId.test.js
new file mode 100644
index 0000000000..fc637e489e
--- /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?.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?.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/useAxiosPrivate.js b/frontend/src/hooks/useAxiosPrivate.js
index 7abc500038..ac57911d2d 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;
@@ -34,6 +36,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..62917a8f0a 100644
--- a/frontend/src/hooks/useExceptionHandler.jsx
+++ b/frontend/src/hooks/useExceptionHandler.jsx
@@ -1,16 +1,10 @@
-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) => ({
- type: "error",
- content,
- title,
- duration,
- });
-
const handleException = (
err,
errMessage = "Something went wrong",
@@ -18,17 +12,22 @@ const useExceptionHandler = () => {
title = "Failed",
duration = 0,
) => {
+ const requestId = getRequestIdFromError(err) ?? null;
+ const alert = (content) => ({
+ type: "error",
+ content,
+ title,
+ duration,
+ requestId,
+ });
+
if (!err) {
- return buildAlert(errMessage, title, duration);
+ return alert(errMessage);
}
if (err.code === "ERR_NETWORK" && !navigator.onLine) {
- return buildAlert(
- "Please check your internet connection.",
- title,
- duration,
- );
+ return alert("Please check your internet connection.");
} else if (err.code === "ERR_CANCELED") {
- return buildAlert("Request has been canceled.", title, duration);
+ return alert("Request has been canceled.");
}
if (err?.response?.data) {
@@ -40,7 +39,7 @@ const useExceptionHandler = () => {
responseData.error || responseData.detail || responseData.message;
if (commonErrorMessage) {
- return buildAlert(commonErrorMessage, title, duration);
+ return alert(commonErrorMessage);
}
// Then handle specific error types
@@ -73,33 +72,24 @@ const useExceptionHandler = () => {
.join("\n");
}
}
- return buildAlert(errorMessage, title, duration);
+ return alert(errorMessage);
}
break;
case "subscription_error":
navigate("/subscription-expired");
- return buildAlert(errors, title, duration);
+ return alert(errors);
case "client_error":
case "server_error":
- return buildAlert(
- errors?.[0]?.detail ? errors[0].detail : errMessage,
- title,
- duration,
- );
+ return alert(errors?.[0]?.detail ? errors[0].detail : errMessage);
default:
- return buildAlert(errMessage, title, duration);
+ return alert(errMessage);
}
} else {
- return buildAlert(errMessage, title, duration);
+ 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 244be08cfe..aa670e91d8 100644
--- a/frontend/src/index.css
+++ b/frontend/src/index.css
@@ -251,3 +251,20 @@ body {
background-color: #ffffff;
z-index: 2000;
}
+
+.notification-request-id,
+.notification-request-id__value {
+ font-size: 12px;
+}
+
+.notification-request-id {
+ margin-top: 8px;
+ display: flex;
+ align-items: center;
+ flex-wrap: wrap;
+ gap: 4px;
+}
+
+.notification-request-id__value {
+ 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,
},
};