diff --git a/airflow-core/src/airflow/ui/src/components/renderStructuredLog.test.tsx b/airflow-core/src/airflow/ui/src/components/renderStructuredLog.test.tsx
index 871568049c959..655522ef45e73 100644
--- a/airflow-core/src/airflow/ui/src/components/renderStructuredLog.test.tsx
+++ b/airflow-core/src/airflow/ui/src/components/renderStructuredLog.test.tsx
@@ -35,6 +35,47 @@ describe("tiContextFields", () => {
});
});
+describe("renderStructuredLog — traceback frame highlighting", () => {
+ const sitepkgFile = "/usr/local/lib/python3.14/site-packages/airflow/sdk/bases/operator.py";
+ const dagBundleFile = "/tmp/airflow/dag_bundles/astro/main/dags/non_alert/enrich_ticket.py";
+
+ const renderWithError = () => {
+ const result = renderStructuredLog({
+ index: 0,
+ logLink: "",
+ logMessage: {
+ error_detail: [
+ {
+ exc_notes: [],
+ exc_type: "TypeError",
+ exc_value: "fromisoformat: argument must be str",
+ frames: [
+ { filename: sitepkgFile, lineno: 445, name: "wrapper" },
+ { filename: dagBundleFile, lineno: 226, name: "retrieve_cluster" },
+ ],
+ is_cause: false,
+ syntax_error: null,
+ },
+ ],
+ event: "Task failed with exception",
+ level: "error",
+ timestamp: "2026-07-22T09:15:20Z",
+ },
+ renderingMode: "jsx",
+ translate: translate as never,
+ });
+
+ return render({result});
+ };
+
+ it("marks a DAG-bundle frame as user code and a site-packages frame as library", () => {
+ renderWithError();
+
+ expect(screen.getByText(JSON.stringify(dagBundleFile))).toHaveAttribute("data-frame-source", "user");
+ expect(screen.getByText(JSON.stringify(sitepkgFile))).toHaveAttribute("data-frame-source", "library");
+ });
+});
+
describe("renderStructuredLog — TI context field stripping", () => {
it("does not render TI context fields as per-line structured attributes", () => {
const result = renderStructuredLog({
diff --git a/airflow-core/src/airflow/ui/src/components/renderStructuredLog.tsx b/airflow-core/src/airflow/ui/src/components/renderStructuredLog.tsx
index 153f9f21a2495..a8389611f2429 100644
--- a/airflow-core/src/airflow/ui/src/components/renderStructuredLog.tsx
+++ b/airflow-core/src/airflow/ui/src/components/renderStructuredLog.tsx
@@ -26,7 +26,7 @@ import type { StructuredLogMessage, TaskInstancesLogResponse } from "openapi/req
import AnsiRenderer from "src/components/AnsiRenderer";
import Time from "src/components/Time";
import { urlRegex } from "src/constants/urlRegex";
-import { LogLevel, logLevelColorMapping } from "src/utils/logs";
+import { isUserCodeFrame, LogLevel, logLevelColorMapping } from "src/utils/logs";
type Frame = {
filename: string;
@@ -255,11 +255,21 @@ const renderStructuredLogImpl = ({
return ` ${translate("components:logs.file")} ${frame.filename}, ${translate("components:logs.location", { line: frame.lineno, name: frame.name })}\n`;
}
+ // Highlight user-code frames (DAG bundle, plugins, local files) in the info blue,
+ // and leave installed-package frames in the normal text color, so the frame the
+ // error actually came from stands out from the framework frames around it.
+ const userCode = isUserCodeFrame(frame.filename);
+
return (
{translate("components:logs.file")}{" "}
- {JSON.stringify(frame.filename)},{" "}
- {translate("components:logs.location", { line: frame.lineno, name: frame.name })}
+
+ {JSON.stringify(frame.filename)}
+
+ , {translate("components:logs.location", { line: frame.lineno, name: frame.name })}
);
});
diff --git a/airflow-core/src/airflow/ui/src/utils/logs.test.ts b/airflow-core/src/airflow/ui/src/utils/logs.test.ts
index 0d7db12ecd8d8..fc6b2dee4dee4 100644
--- a/airflow-core/src/airflow/ui/src/utils/logs.test.ts
+++ b/airflow-core/src/airflow/ui/src/utils/logs.test.ts
@@ -20,7 +20,26 @@ import { describe, it, expect } from "vitest";
import type { TaskInstancesLogResponse } from "openapi/requests/types.gen";
-import { parseStreamingLogContent } from "./logs";
+import { isUserCodeFrame, parseStreamingLogContent } from "./logs";
+
+describe("isUserCodeFrame", () => {
+ it.each([
+ "/tmp/airflow/dag_bundles/astro/main/dags/non_alert/enrich_ticket.py",
+ "/opt/airflow/dags/my_dag.py",
+ "/home/user/plugins/my_plugin.py",
+ "C:\\airflow\\dags\\my_dag.py",
+ ])("treats %s as user code", (filename) => {
+ expect(isUserCodeFrame(filename)).toBe(true);
+ });
+
+ it.each([
+ "/usr/local/lib/python3.14/site-packages/airflow/sdk/execution_time/task_runner.py",
+ "/usr/lib/python3/dist-packages/airflow/models/baseoperator.py",
+ "C:\\Python314\\Lib\\site-packages\\airflow\\sdk\\bases\\operator.py",
+ ])("treats %s as library code", (filename) => {
+ expect(isUserCodeFrame(filename)).toBe(false);
+ });
+});
describe("parseStreamingLogContent", () => {
it("returns content when data has content property", () => {
diff --git a/airflow-core/src/airflow/ui/src/utils/logs.ts b/airflow-core/src/airflow/ui/src/utils/logs.ts
index 21f12c660b049..a454fd5fbb4af 100644
--- a/airflow-core/src/airflow/ui/src/utils/logs.ts
+++ b/airflow-core/src/airflow/ui/src/utils/logs.ts
@@ -54,6 +54,11 @@ export const logLevelOptions = createListCollection<{
],
});
+// A traceback frame is "user code" unless it lives in an installed-package directory
+// (site-packages / dist-packages). DAG bundle code, plugins, and local files are all user code.
+export const isUserCodeFrame = (filename: string): boolean =>
+ !/[/\\](?:site|dist)-packages[/\\]/u.test(filename);
+
export const parseStreamingLogContent = (
data: TaskInstancesLogResponse | undefined,
): TaskInstancesLogResponse["content"] => {