= (
Full ID: {traceId} —{" "}
{activities.length}{" "}
- activit{activities.length !== 1 ? "ies" : "y"},{" "}
- {logs.length} log record{logs.length !== 1 ? "s" : ""}
+ activit{activities.length !== 1 ? "ies" : "y"}, {filtered
+ ? (
+
+ {logs.length} of {totalLogCount}
+ {" "}
+ log record{totalLogCount !== 1 ? "s" : ""}
+
+ )
+ : (
+
+ {logs.length}{" "}
+ log record{logs.length !== 1 ? "s" : ""}
+
+ )}
{activities.length === 0
@@ -179,8 +232,67 @@ export const TraceDetailPage: FC = (
)}
Logs
+ {availableCategories.length > 0 && (
+
+ )}
{logs.length === 0
- ? No logs captured for this trace.
+ ? (
+
+ {filtered
+ ? "No logs match the selected filters."
+ : "No logs captured for this trace."}
+
+ )
: (
diff --git a/packages/debugger/src/views/traces-list.tsx b/packages/debugger/src/views/traces-list.tsx
index 2bf6eb53d..4db9ed53c 100644
--- a/packages/debugger/src/views/traces-list.tsx
+++ b/packages/debugger/src/views/traces-list.tsx
@@ -9,10 +9,31 @@ import { Layout } from "./layout.tsx";
*/
export interface TracesListPageProps {
/**
- * The list of trace summaries to display.
+ * The list of trace summaries to display, already filtered by
+ * {@link selectedTypes} when it is non-empty.
*/
traces: TraceSummary[];
+ /**
+ * The distinct activity types available to filter by, derived from the
+ * unfiltered trace set.
+ */
+ availableTypes: readonly string[];
+
+ /**
+ * The activity types currently selected in the filter form.
+ */
+ selectedTypes: readonly string[];
+
+ /**
+ * The same snapshot string the live-poll script computes from a fresh
+ * `/api/traces` fetch, computed here from the data this page was
+ * actually rendered with. The script starts comparing from this value
+ * instead of from whatever its first poll happens to see, so a change
+ * that landed between the render and the first poll is still caught.
+ */
+ initialSnapshot: string;
+
/**
* The path prefix for the debug dashboard.
*/
@@ -23,16 +44,45 @@ export interface TracesListPageProps {
* The traces list page of the debug dashboard.
*/
export const TracesListPage: FC = (
- { traces, pathPrefix },
+ { traces, availableTypes, selectedTypes, initialSnapshot, pathPrefix },
) => {
+ const filtered = selectedTypes.length > 0;
return (
+ {availableTypes.length > 0 && (
+
+ )}
Showing {traces.length}{" "}
trace{traces.length !== 1 ? "s" : ""}.
{traces.length === 0
- ? No traces captured yet.
+ ? (
+
+ {filtered
+ ? "No traces match the selected filters."
+ : "No traces captured yet."}
+
+ )
: (
@@ -76,19 +126,44 @@ export const TracesListPage: FC = (
dangerouslySetInnerHTML={{
__html: `
(function() {
+ var selectedTypes = new URLSearchParams(location.search).getAll("type");
+ function snapshotOf(data) {
+ var types = [];
+ var rows = [];
+ for (var i = 0; i < data.length; i++) {
+ var activityTypes = (data[i].activityTypes || []).slice().sort();
+ var matchesFilter = selectedTypes.length === 0;
+ for (var j = 0; j < activityTypes.length; j++) {
+ if (types.indexOf(activityTypes[j]) === -1) types.push(activityTypes[j]);
+ if (!matchesFilter && selectedTypes.indexOf(activityTypes[j]) !== -1) {
+ matchesFilter = true;
+ }
+ }
+ if (matchesFilter) {
+ rows.push(
+ data[i].traceId + ":" + activityTypes.join("+") + ":" +
+ data[i].activityCount,
+ );
+ }
+ }
+ types.sort();
+ rows.sort();
+ return types.join(",") + "|" + rows.join(";");
+ }
+ var prevSnapshot = ${
+ JSON.stringify(initialSnapshot).replace(/