This is an automated email from the ASF dual-hosted git repository.

pierrejeambrun pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/airflow.git


The following commit(s) were added to refs/heads/main by this push:
     new 4e652109591 Highlight user-code frames in task log tracebacks (#70294)
4e652109591 is described below

commit 4e65210959198a97ce955019776fb3abb7bca526
Author: Pierre Jeambrun <[email protected]>
AuthorDate: Fri Jul 24 11:29:07 2026 +0200

    Highlight user-code frames in task log tracebacks (#70294)
    
    In an error traceback the frame that actually failed is usually in the 
user's DAG code, but it sits visually indistinguishable among the framework 
frames from installed packages. Emphasising user-code frames and muting 
installed-package frames lets users find where the error originates at a glance.
---
 .../ui/src/components/renderStructuredLog.test.tsx | 41 ++++++++++++++++++++++
 .../ui/src/components/renderStructuredLog.tsx      | 16 +++++++--
 airflow-core/src/airflow/ui/src/utils/logs.test.ts | 21 ++++++++++-
 airflow-core/src/airflow/ui/src/utils/logs.ts      |  5 +++
 4 files changed, 79 insertions(+), 4 deletions(-)

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 871568049c9..655522ef45e 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(<Wrapper>{result}</Wrapper>);
+  };
+
+  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 153f9f21a24..a8389611f24 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 (
           <chakra.p 
key={`frame-${frame.name}-${frame.filename}-${frame.lineno}`}>
             {translate("components:logs.file")}{" "}
-            <chakra.span 
color="fg.info">{JSON.stringify(frame.filename)}</chakra.span>,{" "}
-            {translate("components:logs.location", { line: frame.lineno, name: 
frame.name })}
+            <chakra.span
+              color={userCode ? "fg.info" : undefined}
+              data-frame-source={userCode ? "user" : "library"}
+            >
+              {JSON.stringify(frame.filename)}
+            </chakra.span>
+            , {translate("components:logs.location", { line: frame.lineno, 
name: frame.name })}
           </chakra.p>
         );
       });
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 0d7db12ecd8..fc6b2dee4de 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 21f12c660b0..a454fd5fbb4 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"] => {

Reply via email to