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"] => {