@fedify/debugger 2.5.0-dev.2287 → 2.5.0-dev.2304

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/dist/mod.cjs +205 -19
  2. package/dist/mod.js +206 -20
  3. package/package.json +2 -2
package/dist/mod.cjs CHANGED
@@ -361,6 +361,13 @@ const Layout = ({ title, pathPrefix, children }) => {
361
361
  pre { background: #f6f8fa; padding: 1rem; overflow-x: auto; border-radius: 6px; font-size: 0.8125rem; }
362
362
  .empty { color: #888; font-style: italic; }
363
363
  nav a { margin-right: 0.5rem; }
364
+ .filter-form { border: 1px solid #ddd; border-radius: 6px; padding: 0.75rem 1rem; margin-bottom: 1rem; }
365
+ .filter-form fieldset { border: none; padding: 0; margin: 0; }
366
+ .filter-form legend { font-size: 0.8125rem; color: #666; padding: 0; margin-bottom: 0.5rem; }
367
+ .filter-form label { display: inline-flex; align-items: center; gap: 0.3rem; margin: 0 1rem 0.5rem 0; font-size: 0.875rem; }
368
+ .filter-form select, .filter-form input[type="text"] { font-size: 0.875rem; padding: 0.25rem 0.4rem; }
369
+ .filter-actions { display: flex; align-items: center; gap: 1rem; margin-top: 0.25rem; }
370
+ .filter-actions button { font-size: 0.8125rem; padding: 0.3rem 0.75rem; cursor: pointer; }
364
371
  .log-table td { font-size: 0.8125rem; vertical-align: top; }
365
372
  .log-table time { font-family: monospace; white-space: nowrap; }
366
373
  .badge-debug { background: #e8e8e8; color: #666; }
@@ -384,6 +391,8 @@ const Layout = ({ title, pathPrefix, children }) => {
384
391
  .detail-section h2 { border-bottom-color: #21262d; }
385
392
  pre { background: #161b22; }
386
393
  .empty { color: #9198a1; }
394
+ .filter-form { border-color: #30363d; }
395
+ .filter-form legend { color: #9198a1; }
387
396
  .badge-debug { background: #21262d; color: #9198a1; }
388
397
  .badge-info { background: #122d42; color: #58a6ff; }
389
398
  .badge-warning { background: #2e2a1f; color: #d29922; }
@@ -541,7 +550,8 @@ function safeISOString(timestamp) {
541
550
  /**
542
551
  * The trace detail page of the debug dashboard.
543
552
  */
544
- const TraceDetailPage = ({ traceId, activities, logs, pathPrefix }) => {
553
+ const TraceDetailPage = ({ traceId, activities, logs, totalLogCount, availableCategories, selectedCategory, selectedLevel, selectedQuery, pathPrefix }) => {
554
+ const filtered = Boolean(selectedCategory) || Boolean(selectedLevel) || Boolean(selectedQuery);
545
555
  return /* @__PURE__ */ (0, hono_jsx_jsx_runtime.jsxs)(Layout, {
546
556
  pathPrefix,
547
557
  title: `Trace ${traceId.slice(0, 8)}`,
@@ -560,11 +570,20 @@ const TraceDetailPage = ({ traceId, activities, logs, pathPrefix }) => {
560
570
  " ",
561
571
  "activit",
562
572
  activities.length !== 1 ? "ies" : "y",
563
- ",",
564
- " ",
565
- /* @__PURE__ */ (0, hono_jsx_jsx_runtime.jsx)("strong", { children: logs.length }),
566
- " log record",
567
- logs.length !== 1 ? "s" : ""
573
+ ", ",
574
+ filtered ? /* @__PURE__ */ (0, hono_jsx_jsx_runtime.jsxs)("span", { children: [
575
+ /* @__PURE__ */ (0, hono_jsx_jsx_runtime.jsx)("strong", { children: logs.length }),
576
+ " of ",
577
+ /* @__PURE__ */ (0, hono_jsx_jsx_runtime.jsx)("strong", { children: totalLogCount }),
578
+ " ",
579
+ "log record",
580
+ totalLogCount !== 1 ? "s" : ""
581
+ ] }) : /* @__PURE__ */ (0, hono_jsx_jsx_runtime.jsxs)("span", { children: [
582
+ /* @__PURE__ */ (0, hono_jsx_jsx_runtime.jsx)("strong", { children: logs.length }),
583
+ " ",
584
+ "log record",
585
+ logs.length !== 1 ? "s" : ""
586
+ ] })
568
587
  ] }),
569
588
  activities.length === 0 ? /* @__PURE__ */ (0, hono_jsx_jsx_runtime.jsx)("p", {
570
589
  class: "empty",
@@ -610,9 +629,54 @@ const TraceDetailPage = ({ traceId, activities, logs, pathPrefix }) => {
610
629
  ]
611
630
  }, activity.spanId)),
612
631
  /* @__PURE__ */ (0, hono_jsx_jsx_runtime.jsx)("h2", { children: "Logs" }),
632
+ availableCategories.length > 0 && /* @__PURE__ */ (0, hono_jsx_jsx_runtime.jsxs)("form", {
633
+ class: "filter-form",
634
+ method: "get",
635
+ action: `${pathPrefix}/traces/${traceId}`,
636
+ children: [
637
+ /* @__PURE__ */ (0, hono_jsx_jsx_runtime.jsxs)("label", { children: ["Category", /* @__PURE__ */ (0, hono_jsx_jsx_runtime.jsxs)("select", {
638
+ name: "category",
639
+ children: [/* @__PURE__ */ (0, hono_jsx_jsx_runtime.jsx)("option", {
640
+ value: "",
641
+ children: "All categories"
642
+ }), availableCategories.map((category) => /* @__PURE__ */ (0, hono_jsx_jsx_runtime.jsx)("option", {
643
+ value: category,
644
+ selected: category === selectedCategory,
645
+ children: category
646
+ }, category))]
647
+ })] }),
648
+ /* @__PURE__ */ (0, hono_jsx_jsx_runtime.jsxs)("label", { children: ["Level", /* @__PURE__ */ (0, hono_jsx_jsx_runtime.jsxs)("select", {
649
+ name: "level",
650
+ children: [/* @__PURE__ */ (0, hono_jsx_jsx_runtime.jsx)("option", {
651
+ value: "",
652
+ children: "All levels"
653
+ }), (0, _logtape_logtape.getLogLevels)().map((level) => /* @__PURE__ */ (0, hono_jsx_jsx_runtime.jsx)("option", {
654
+ value: level,
655
+ selected: level === selectedLevel,
656
+ children: level
657
+ }, level))]
658
+ })] }),
659
+ /* @__PURE__ */ (0, hono_jsx_jsx_runtime.jsxs)("label", { children: ["Search", /* @__PURE__ */ (0, hono_jsx_jsx_runtime.jsx)("input", {
660
+ type: "text",
661
+ name: "q",
662
+ value: selectedQuery ?? "",
663
+ placeholder: "Search message…"
664
+ })] }),
665
+ /* @__PURE__ */ (0, hono_jsx_jsx_runtime.jsxs)("div", {
666
+ class: "filter-actions",
667
+ children: [/* @__PURE__ */ (0, hono_jsx_jsx_runtime.jsx)("button", {
668
+ type: "submit",
669
+ children: "Filter"
670
+ }), filtered && /* @__PURE__ */ (0, hono_jsx_jsx_runtime.jsx)("a", {
671
+ href: `${pathPrefix}/traces/${traceId}`,
672
+ children: "Clear filters"
673
+ })]
674
+ })
675
+ ]
676
+ }),
613
677
  logs.length === 0 ? /* @__PURE__ */ (0, hono_jsx_jsx_runtime.jsx)("p", {
614
678
  class: "empty",
615
- children: "No logs captured for this trace."
679
+ children: filtered ? "No logs match the selected filters." : "No logs captured for this trace."
616
680
  }) : /* @__PURE__ */ (0, hono_jsx_jsx_runtime.jsxs)("table", {
617
681
  class: "log-table",
618
682
  children: [/* @__PURE__ */ (0, hono_jsx_jsx_runtime.jsx)("thead", { children: /* @__PURE__ */ (0, hono_jsx_jsx_runtime.jsxs)("tr", { children: [
@@ -654,10 +718,31 @@ function formatJson(json) {
654
718
  /**
655
719
  * The traces list page of the debug dashboard.
656
720
  */
657
- const TracesListPage = ({ traces, pathPrefix }) => {
721
+ const TracesListPage = ({ traces, availableTypes, selectedTypes, initialSnapshot, pathPrefix }) => {
722
+ const filtered = selectedTypes.length > 0;
658
723
  return /* @__PURE__ */ (0, hono_jsx_jsx_runtime.jsxs)(Layout, {
659
724
  pathPrefix,
660
725
  children: [
726
+ availableTypes.length > 0 && /* @__PURE__ */ (0, hono_jsx_jsx_runtime.jsxs)("form", {
727
+ class: "filter-form",
728
+ method: "get",
729
+ action: `${pathPrefix}/`,
730
+ children: [/* @__PURE__ */ (0, hono_jsx_jsx_runtime.jsxs)("fieldset", { children: [/* @__PURE__ */ (0, hono_jsx_jsx_runtime.jsx)("legend", { children: "Filter by activity type" }), availableTypes.map((type) => /* @__PURE__ */ (0, hono_jsx_jsx_runtime.jsxs)("label", { children: [/* @__PURE__ */ (0, hono_jsx_jsx_runtime.jsx)("input", {
731
+ type: "checkbox",
732
+ name: "type",
733
+ value: type,
734
+ checked: selectedTypes.includes(type)
735
+ }), type] }, type))] }), /* @__PURE__ */ (0, hono_jsx_jsx_runtime.jsxs)("div", {
736
+ class: "filter-actions",
737
+ children: [/* @__PURE__ */ (0, hono_jsx_jsx_runtime.jsx)("button", {
738
+ type: "submit",
739
+ children: "Filter"
740
+ }), filtered && /* @__PURE__ */ (0, hono_jsx_jsx_runtime.jsx)("a", {
741
+ href: `${pathPrefix}/`,
742
+ children: "Clear filters"
743
+ })]
744
+ })]
745
+ }),
661
746
  /* @__PURE__ */ (0, hono_jsx_jsx_runtime.jsxs)("p", { children: [
662
747
  "Showing ",
663
748
  /* @__PURE__ */ (0, hono_jsx_jsx_runtime.jsx)("strong", { children: traces.length }),
@@ -668,7 +753,7 @@ const TracesListPage = ({ traces, pathPrefix }) => {
668
753
  ] }),
669
754
  traces.length === 0 ? /* @__PURE__ */ (0, hono_jsx_jsx_runtime.jsx)("p", {
670
755
  class: "empty",
671
- children: "No traces captured yet."
756
+ children: filtered ? "No traces match the selected filters." : "No traces captured yet."
672
757
  }) : /* @__PURE__ */ (0, hono_jsx_jsx_runtime.jsxs)("table", { children: [/* @__PURE__ */ (0, hono_jsx_jsx_runtime.jsx)("thead", { children: /* @__PURE__ */ (0, hono_jsx_jsx_runtime.jsxs)("tr", { children: [
673
758
  /* @__PURE__ */ (0, hono_jsx_jsx_runtime.jsx)("th", { children: "Trace ID" }),
674
759
  /* @__PURE__ */ (0, hono_jsx_jsx_runtime.jsx)("th", { children: "Activity Types" }),
@@ -694,17 +779,40 @@ const TracesListPage = ({ traces, pathPrefix }) => {
694
779
  ] }, trace.traceId)) })] }),
695
780
  /* @__PURE__ */ (0, hono_jsx_jsx_runtime.jsx)("script", { dangerouslySetInnerHTML: { __html: `
696
781
  (function() {
782
+ var selectedTypes = new URLSearchParams(location.search).getAll("type");
783
+ function snapshotOf(data) {
784
+ var types = [];
785
+ var rows = [];
786
+ for (var i = 0; i < data.length; i++) {
787
+ var activityTypes = (data[i].activityTypes || []).slice().sort();
788
+ var matchesFilter = selectedTypes.length === 0;
789
+ for (var j = 0; j < activityTypes.length; j++) {
790
+ if (types.indexOf(activityTypes[j]) === -1) types.push(activityTypes[j]);
791
+ if (!matchesFilter && selectedTypes.indexOf(activityTypes[j]) !== -1) {
792
+ matchesFilter = true;
793
+ }
794
+ }
795
+ if (matchesFilter) {
796
+ rows.push(
797
+ data[i].traceId + ":" + activityTypes.join("+") + ":" +
798
+ data[i].activityCount,
799
+ );
800
+ }
801
+ }
802
+ types.sort();
803
+ rows.sort();
804
+ return types.join(",") + "|" + rows.join(";");
805
+ }
806
+ var prevSnapshot = ${JSON.stringify(initialSnapshot).replace(/</g, "\\u003c")};
697
807
  var interval = setInterval(function() {
698
808
  fetch(${JSON.stringify(pathPrefix).replace(/</g, "\\u003c")} + "/api/traces")
699
809
  .then(function(r) { return r.json(); })
700
810
  .then(function(data) {
701
- var countEl = document.querySelector("strong");
702
- if (countEl) {
703
- var current = parseInt(countEl.textContent, 10);
704
- if (data.length !== current) {
705
- location.reload();
706
- }
811
+ var snapshot = snapshotOf(data);
812
+ if (snapshot !== prevSnapshot) {
813
+ location.reload();
707
814
  }
815
+ prevSnapshot = snapshot;
708
816
  })
709
817
  .catch(function() {});
710
818
  }, 3000);
@@ -718,6 +826,64 @@ const TracesListPage = ({ traces, pathPrefix }) => {
718
826
  };
719
827
  //#endregion
720
828
  //#region src/routes.tsx
829
+ /** Collects the distinct activity types across all traces, sorted. */
830
+ function distinctActivityTypes(traces) {
831
+ const types = /* @__PURE__ */ new Set();
832
+ for (const trace of traces) for (const type of trace.activityTypes) types.add(type);
833
+ return [...types].sort();
834
+ }
835
+ /**
836
+ * Keeps only the traces whose `activityTypes` include at least one of the
837
+ * given `types`. An empty `types` list means "no filter"—all traces pass
838
+ * through unchanged.
839
+ */
840
+ function filterTracesByTypes(traces, types) {
841
+ if (types.length === 0) return [...traces];
842
+ return traces.filter((trace) => trace.activityTypes.some((type) => types.includes(type)));
843
+ }
844
+ /**
845
+ * Computes a string that changes whenever the trace corpus changes in a
846
+ * way the traces list page's live-poll script needs to react to. Rather
847
+ * than comparing a handful of hand-picked summary fields—which has missed
848
+ * a real change each time a new one turned up—this serializes every trace
849
+ * that currently matches `selectedTypes` as `id:types:activityCount`, so
850
+ * a trace being replaced, an existing trace's activity count going up, or
851
+ * its activity types growing are all covered by the same comparison. The
852
+ * distinct activity types across *all* traces are tracked separately,
853
+ * since a type that does not match the active filter still needs to make
854
+ * its checkbox appear. The page embeds this as the poll script's
855
+ * starting point, so the very first poll tick is compared against the
856
+ * data the page was actually rendered with, rather than treating
857
+ * whatever that first tick happens to see as the baseline.
858
+ */
859
+ function snapshotOf(traces, selectedTypes) {
860
+ const types = distinctActivityTypes(traces);
861
+ const rows = filterTracesByTypes(traces, selectedTypes).map((trace) => `${trace.traceId}:${[...trace.activityTypes].sort().join("+")}:${trace.activityCount}`).sort();
862
+ return `${types.join(",")}|${rows.join(";")}`;
863
+ }
864
+ /** Collects the distinct dot-joined log categories, sorted. */
865
+ function distinctLogCategories(logs) {
866
+ const categories = /* @__PURE__ */ new Set();
867
+ for (const log of logs) categories.add(log.category.join("."));
868
+ return [...categories].sort();
869
+ }
870
+ /** Applies a {@link LogFilter} to a list of log records. */
871
+ function filterLogs(logs, filter) {
872
+ let filtered = logs;
873
+ if (filter.category) {
874
+ const category = filter.category;
875
+ filtered = filtered.filter((log) => log.category.join(".") === category);
876
+ }
877
+ if (filter.level) {
878
+ const level = filter.level;
879
+ filtered = filtered.filter((log) => log.level === level);
880
+ }
881
+ if (filter.q) {
882
+ const needle = filter.q.toLowerCase();
883
+ filtered = filtered.filter((log) => log.message.toLowerCase().includes(needle));
884
+ }
885
+ return filtered;
886
+ }
721
887
  function createDebugApp(pathPrefix, exporter, logStore, auth) {
722
888
  const app = new hono.Hono({ strict: false }).basePath(pathPrefix);
723
889
  let hmacKeyPromise;
@@ -782,30 +948,50 @@ function createDebugApp(pathPrefix, exporter, logStore, auth) {
782
948
  }
783
949
  app.get("/api/traces", async (c) => {
784
950
  const traces = await exporter.getRecentTraces();
785
- return c.json(traces);
951
+ const types = c.req.queries("type") ?? [];
952
+ return c.json(filterTracesByTypes(traces, types));
786
953
  });
787
954
  app.get("/api/logs/:traceId", async (c) => {
788
955
  const traceId = c.req.param("traceId");
789
956
  await logStore.flush();
790
957
  const logs = await logStore.get(traceId);
791
- return c.json(logs);
958
+ const logFilter = {
959
+ category: c.req.query("category"),
960
+ level: c.req.query("level"),
961
+ q: c.req.query("q")
962
+ };
963
+ return c.json(filterLogs(logs, logFilter));
792
964
  });
793
965
  app.get("/traces/:traceId", async (c) => {
794
966
  const traceId = c.req.param("traceId");
795
967
  await logStore.flush();
796
968
  const activities = await exporter.getActivitiesByTraceId(traceId);
797
969
  const logs = await logStore.get(traceId);
970
+ const logFilter = {
971
+ category: c.req.query("category"),
972
+ level: c.req.query("level"),
973
+ q: c.req.query("q")
974
+ };
798
975
  return c.html(/* @__PURE__ */ (0, hono_jsx_jsx_runtime.jsx)(TraceDetailPage, {
799
976
  traceId,
800
977
  activities,
801
- logs,
978
+ logs: filterLogs(logs, logFilter),
979
+ totalLogCount: logs.length,
980
+ availableCategories: distinctLogCategories(logs),
981
+ selectedCategory: logFilter.category,
982
+ selectedLevel: logFilter.level,
983
+ selectedQuery: logFilter.q,
802
984
  pathPrefix
803
985
  }));
804
986
  });
805
987
  app.get("/", async (c) => {
806
988
  const traces = await exporter.getRecentTraces();
989
+ const selectedTypes = c.req.queries("type") ?? [];
807
990
  return c.html(/* @__PURE__ */ (0, hono_jsx_jsx_runtime.jsx)(TracesListPage, {
808
- traces,
991
+ traces: filterTracesByTypes(traces, selectedTypes),
992
+ availableTypes: distinctActivityTypes(traces),
993
+ selectedTypes,
994
+ initialSnapshot: snapshotOf(traces, selectedTypes),
809
995
  pathPrefix
810
996
  }));
811
997
  });
package/dist/mod.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import { Temporal } from "temporal-polyfill";
2
2
  import { MemoryKvStore } from "@fedify/fedify/federation";
3
3
  import { FedifySpanExporter } from "@fedify/fedify/otel";
4
- import { configureSync, getConfig } from "@logtape/logtape";
4
+ import { configureSync, getConfig, getLogLevels } from "@logtape/logtape";
5
5
  import { context, propagation, trace } from "@opentelemetry/api";
6
6
  import { AsyncLocalStorageContextManager } from "@opentelemetry/context-async-hooks";
7
7
  import { W3CTraceContextPropagator } from "@opentelemetry/core";
@@ -360,6 +360,13 @@ const Layout = ({ title, pathPrefix, children }) => {
360
360
  pre { background: #f6f8fa; padding: 1rem; overflow-x: auto; border-radius: 6px; font-size: 0.8125rem; }
361
361
  .empty { color: #888; font-style: italic; }
362
362
  nav a { margin-right: 0.5rem; }
363
+ .filter-form { border: 1px solid #ddd; border-radius: 6px; padding: 0.75rem 1rem; margin-bottom: 1rem; }
364
+ .filter-form fieldset { border: none; padding: 0; margin: 0; }
365
+ .filter-form legend { font-size: 0.8125rem; color: #666; padding: 0; margin-bottom: 0.5rem; }
366
+ .filter-form label { display: inline-flex; align-items: center; gap: 0.3rem; margin: 0 1rem 0.5rem 0; font-size: 0.875rem; }
367
+ .filter-form select, .filter-form input[type="text"] { font-size: 0.875rem; padding: 0.25rem 0.4rem; }
368
+ .filter-actions { display: flex; align-items: center; gap: 1rem; margin-top: 0.25rem; }
369
+ .filter-actions button { font-size: 0.8125rem; padding: 0.3rem 0.75rem; cursor: pointer; }
363
370
  .log-table td { font-size: 0.8125rem; vertical-align: top; }
364
371
  .log-table time { font-family: monospace; white-space: nowrap; }
365
372
  .badge-debug { background: #e8e8e8; color: #666; }
@@ -383,6 +390,8 @@ const Layout = ({ title, pathPrefix, children }) => {
383
390
  .detail-section h2 { border-bottom-color: #21262d; }
384
391
  pre { background: #161b22; }
385
392
  .empty { color: #9198a1; }
393
+ .filter-form { border-color: #30363d; }
394
+ .filter-form legend { color: #9198a1; }
386
395
  .badge-debug { background: #21262d; color: #9198a1; }
387
396
  .badge-info { background: #122d42; color: #58a6ff; }
388
397
  .badge-warning { background: #2e2a1f; color: #d29922; }
@@ -540,7 +549,8 @@ function safeISOString(timestamp) {
540
549
  /**
541
550
  * The trace detail page of the debug dashboard.
542
551
  */
543
- const TraceDetailPage = ({ traceId, activities, logs, pathPrefix }) => {
552
+ const TraceDetailPage = ({ traceId, activities, logs, totalLogCount, availableCategories, selectedCategory, selectedLevel, selectedQuery, pathPrefix }) => {
553
+ const filtered = Boolean(selectedCategory) || Boolean(selectedLevel) || Boolean(selectedQuery);
544
554
  return /* @__PURE__ */ jsxs(Layout, {
545
555
  pathPrefix,
546
556
  title: `Trace ${traceId.slice(0, 8)}`,
@@ -559,11 +569,20 @@ const TraceDetailPage = ({ traceId, activities, logs, pathPrefix }) => {
559
569
  " ",
560
570
  "activit",
561
571
  activities.length !== 1 ? "ies" : "y",
562
- ",",
563
- " ",
564
- /* @__PURE__ */ jsx("strong", { children: logs.length }),
565
- " log record",
566
- logs.length !== 1 ? "s" : ""
572
+ ", ",
573
+ filtered ? /* @__PURE__ */ jsxs("span", { children: [
574
+ /* @__PURE__ */ jsx("strong", { children: logs.length }),
575
+ " of ",
576
+ /* @__PURE__ */ jsx("strong", { children: totalLogCount }),
577
+ " ",
578
+ "log record",
579
+ totalLogCount !== 1 ? "s" : ""
580
+ ] }) : /* @__PURE__ */ jsxs("span", { children: [
581
+ /* @__PURE__ */ jsx("strong", { children: logs.length }),
582
+ " ",
583
+ "log record",
584
+ logs.length !== 1 ? "s" : ""
585
+ ] })
567
586
  ] }),
568
587
  activities.length === 0 ? /* @__PURE__ */ jsx("p", {
569
588
  class: "empty",
@@ -609,9 +628,54 @@ const TraceDetailPage = ({ traceId, activities, logs, pathPrefix }) => {
609
628
  ]
610
629
  }, activity.spanId)),
611
630
  /* @__PURE__ */ jsx("h2", { children: "Logs" }),
631
+ availableCategories.length > 0 && /* @__PURE__ */ jsxs("form", {
632
+ class: "filter-form",
633
+ method: "get",
634
+ action: `${pathPrefix}/traces/${traceId}`,
635
+ children: [
636
+ /* @__PURE__ */ jsxs("label", { children: ["Category", /* @__PURE__ */ jsxs("select", {
637
+ name: "category",
638
+ children: [/* @__PURE__ */ jsx("option", {
639
+ value: "",
640
+ children: "All categories"
641
+ }), availableCategories.map((category) => /* @__PURE__ */ jsx("option", {
642
+ value: category,
643
+ selected: category === selectedCategory,
644
+ children: category
645
+ }, category))]
646
+ })] }),
647
+ /* @__PURE__ */ jsxs("label", { children: ["Level", /* @__PURE__ */ jsxs("select", {
648
+ name: "level",
649
+ children: [/* @__PURE__ */ jsx("option", {
650
+ value: "",
651
+ children: "All levels"
652
+ }), getLogLevels().map((level) => /* @__PURE__ */ jsx("option", {
653
+ value: level,
654
+ selected: level === selectedLevel,
655
+ children: level
656
+ }, level))]
657
+ })] }),
658
+ /* @__PURE__ */ jsxs("label", { children: ["Search", /* @__PURE__ */ jsx("input", {
659
+ type: "text",
660
+ name: "q",
661
+ value: selectedQuery ?? "",
662
+ placeholder: "Search message…"
663
+ })] }),
664
+ /* @__PURE__ */ jsxs("div", {
665
+ class: "filter-actions",
666
+ children: [/* @__PURE__ */ jsx("button", {
667
+ type: "submit",
668
+ children: "Filter"
669
+ }), filtered && /* @__PURE__ */ jsx("a", {
670
+ href: `${pathPrefix}/traces/${traceId}`,
671
+ children: "Clear filters"
672
+ })]
673
+ })
674
+ ]
675
+ }),
612
676
  logs.length === 0 ? /* @__PURE__ */ jsx("p", {
613
677
  class: "empty",
614
- children: "No logs captured for this trace."
678
+ children: filtered ? "No logs match the selected filters." : "No logs captured for this trace."
615
679
  }) : /* @__PURE__ */ jsxs("table", {
616
680
  class: "log-table",
617
681
  children: [/* @__PURE__ */ jsx("thead", { children: /* @__PURE__ */ jsxs("tr", { children: [
@@ -653,10 +717,31 @@ function formatJson(json) {
653
717
  /**
654
718
  * The traces list page of the debug dashboard.
655
719
  */
656
- const TracesListPage = ({ traces, pathPrefix }) => {
720
+ const TracesListPage = ({ traces, availableTypes, selectedTypes, initialSnapshot, pathPrefix }) => {
721
+ const filtered = selectedTypes.length > 0;
657
722
  return /* @__PURE__ */ jsxs(Layout, {
658
723
  pathPrefix,
659
724
  children: [
725
+ availableTypes.length > 0 && /* @__PURE__ */ jsxs("form", {
726
+ class: "filter-form",
727
+ method: "get",
728
+ action: `${pathPrefix}/`,
729
+ children: [/* @__PURE__ */ jsxs("fieldset", { children: [/* @__PURE__ */ jsx("legend", { children: "Filter by activity type" }), availableTypes.map((type) => /* @__PURE__ */ jsxs("label", { children: [/* @__PURE__ */ jsx("input", {
730
+ type: "checkbox",
731
+ name: "type",
732
+ value: type,
733
+ checked: selectedTypes.includes(type)
734
+ }), type] }, type))] }), /* @__PURE__ */ jsxs("div", {
735
+ class: "filter-actions",
736
+ children: [/* @__PURE__ */ jsx("button", {
737
+ type: "submit",
738
+ children: "Filter"
739
+ }), filtered && /* @__PURE__ */ jsx("a", {
740
+ href: `${pathPrefix}/`,
741
+ children: "Clear filters"
742
+ })]
743
+ })]
744
+ }),
660
745
  /* @__PURE__ */ jsxs("p", { children: [
661
746
  "Showing ",
662
747
  /* @__PURE__ */ jsx("strong", { children: traces.length }),
@@ -667,7 +752,7 @@ const TracesListPage = ({ traces, pathPrefix }) => {
667
752
  ] }),
668
753
  traces.length === 0 ? /* @__PURE__ */ jsx("p", {
669
754
  class: "empty",
670
- children: "No traces captured yet."
755
+ children: filtered ? "No traces match the selected filters." : "No traces captured yet."
671
756
  }) : /* @__PURE__ */ jsxs("table", { children: [/* @__PURE__ */ jsx("thead", { children: /* @__PURE__ */ jsxs("tr", { children: [
672
757
  /* @__PURE__ */ jsx("th", { children: "Trace ID" }),
673
758
  /* @__PURE__ */ jsx("th", { children: "Activity Types" }),
@@ -693,17 +778,40 @@ const TracesListPage = ({ traces, pathPrefix }) => {
693
778
  ] }, trace.traceId)) })] }),
694
779
  /* @__PURE__ */ jsx("script", { dangerouslySetInnerHTML: { __html: `
695
780
  (function() {
781
+ var selectedTypes = new URLSearchParams(location.search).getAll("type");
782
+ function snapshotOf(data) {
783
+ var types = [];
784
+ var rows = [];
785
+ for (var i = 0; i < data.length; i++) {
786
+ var activityTypes = (data[i].activityTypes || []).slice().sort();
787
+ var matchesFilter = selectedTypes.length === 0;
788
+ for (var j = 0; j < activityTypes.length; j++) {
789
+ if (types.indexOf(activityTypes[j]) === -1) types.push(activityTypes[j]);
790
+ if (!matchesFilter && selectedTypes.indexOf(activityTypes[j]) !== -1) {
791
+ matchesFilter = true;
792
+ }
793
+ }
794
+ if (matchesFilter) {
795
+ rows.push(
796
+ data[i].traceId + ":" + activityTypes.join("+") + ":" +
797
+ data[i].activityCount,
798
+ );
799
+ }
800
+ }
801
+ types.sort();
802
+ rows.sort();
803
+ return types.join(",") + "|" + rows.join(";");
804
+ }
805
+ var prevSnapshot = ${JSON.stringify(initialSnapshot).replace(/</g, "\\u003c")};
696
806
  var interval = setInterval(function() {
697
807
  fetch(${JSON.stringify(pathPrefix).replace(/</g, "\\u003c")} + "/api/traces")
698
808
  .then(function(r) { return r.json(); })
699
809
  .then(function(data) {
700
- var countEl = document.querySelector("strong");
701
- if (countEl) {
702
- var current = parseInt(countEl.textContent, 10);
703
- if (data.length !== current) {
704
- location.reload();
705
- }
810
+ var snapshot = snapshotOf(data);
811
+ if (snapshot !== prevSnapshot) {
812
+ location.reload();
706
813
  }
814
+ prevSnapshot = snapshot;
707
815
  })
708
816
  .catch(function() {});
709
817
  }, 3000);
@@ -717,6 +825,64 @@ const TracesListPage = ({ traces, pathPrefix }) => {
717
825
  };
718
826
  //#endregion
719
827
  //#region src/routes.tsx
828
+ /** Collects the distinct activity types across all traces, sorted. */
829
+ function distinctActivityTypes(traces) {
830
+ const types = /* @__PURE__ */ new Set();
831
+ for (const trace of traces) for (const type of trace.activityTypes) types.add(type);
832
+ return [...types].sort();
833
+ }
834
+ /**
835
+ * Keeps only the traces whose `activityTypes` include at least one of the
836
+ * given `types`. An empty `types` list means "no filter"—all traces pass
837
+ * through unchanged.
838
+ */
839
+ function filterTracesByTypes(traces, types) {
840
+ if (types.length === 0) return [...traces];
841
+ return traces.filter((trace) => trace.activityTypes.some((type) => types.includes(type)));
842
+ }
843
+ /**
844
+ * Computes a string that changes whenever the trace corpus changes in a
845
+ * way the traces list page's live-poll script needs to react to. Rather
846
+ * than comparing a handful of hand-picked summary fields—which has missed
847
+ * a real change each time a new one turned up—this serializes every trace
848
+ * that currently matches `selectedTypes` as `id:types:activityCount`, so
849
+ * a trace being replaced, an existing trace's activity count going up, or
850
+ * its activity types growing are all covered by the same comparison. The
851
+ * distinct activity types across *all* traces are tracked separately,
852
+ * since a type that does not match the active filter still needs to make
853
+ * its checkbox appear. The page embeds this as the poll script's
854
+ * starting point, so the very first poll tick is compared against the
855
+ * data the page was actually rendered with, rather than treating
856
+ * whatever that first tick happens to see as the baseline.
857
+ */
858
+ function snapshotOf(traces, selectedTypes) {
859
+ const types = distinctActivityTypes(traces);
860
+ const rows = filterTracesByTypes(traces, selectedTypes).map((trace) => `${trace.traceId}:${[...trace.activityTypes].sort().join("+")}:${trace.activityCount}`).sort();
861
+ return `${types.join(",")}|${rows.join(";")}`;
862
+ }
863
+ /** Collects the distinct dot-joined log categories, sorted. */
864
+ function distinctLogCategories(logs) {
865
+ const categories = /* @__PURE__ */ new Set();
866
+ for (const log of logs) categories.add(log.category.join("."));
867
+ return [...categories].sort();
868
+ }
869
+ /** Applies a {@link LogFilter} to a list of log records. */
870
+ function filterLogs(logs, filter) {
871
+ let filtered = logs;
872
+ if (filter.category) {
873
+ const category = filter.category;
874
+ filtered = filtered.filter((log) => log.category.join(".") === category);
875
+ }
876
+ if (filter.level) {
877
+ const level = filter.level;
878
+ filtered = filtered.filter((log) => log.level === level);
879
+ }
880
+ if (filter.q) {
881
+ const needle = filter.q.toLowerCase();
882
+ filtered = filtered.filter((log) => log.message.toLowerCase().includes(needle));
883
+ }
884
+ return filtered;
885
+ }
720
886
  function createDebugApp(pathPrefix, exporter, logStore, auth) {
721
887
  const app = new Hono({ strict: false }).basePath(pathPrefix);
722
888
  let hmacKeyPromise;
@@ -781,30 +947,50 @@ function createDebugApp(pathPrefix, exporter, logStore, auth) {
781
947
  }
782
948
  app.get("/api/traces", async (c) => {
783
949
  const traces = await exporter.getRecentTraces();
784
- return c.json(traces);
950
+ const types = c.req.queries("type") ?? [];
951
+ return c.json(filterTracesByTypes(traces, types));
785
952
  });
786
953
  app.get("/api/logs/:traceId", async (c) => {
787
954
  const traceId = c.req.param("traceId");
788
955
  await logStore.flush();
789
956
  const logs = await logStore.get(traceId);
790
- return c.json(logs);
957
+ const logFilter = {
958
+ category: c.req.query("category"),
959
+ level: c.req.query("level"),
960
+ q: c.req.query("q")
961
+ };
962
+ return c.json(filterLogs(logs, logFilter));
791
963
  });
792
964
  app.get("/traces/:traceId", async (c) => {
793
965
  const traceId = c.req.param("traceId");
794
966
  await logStore.flush();
795
967
  const activities = await exporter.getActivitiesByTraceId(traceId);
796
968
  const logs = await logStore.get(traceId);
969
+ const logFilter = {
970
+ category: c.req.query("category"),
971
+ level: c.req.query("level"),
972
+ q: c.req.query("q")
973
+ };
797
974
  return c.html(/* @__PURE__ */ jsx(TraceDetailPage, {
798
975
  traceId,
799
976
  activities,
800
- logs,
977
+ logs: filterLogs(logs, logFilter),
978
+ totalLogCount: logs.length,
979
+ availableCategories: distinctLogCategories(logs),
980
+ selectedCategory: logFilter.category,
981
+ selectedLevel: logFilter.level,
982
+ selectedQuery: logFilter.q,
801
983
  pathPrefix
802
984
  }));
803
985
  });
804
986
  app.get("/", async (c) => {
805
987
  const traces = await exporter.getRecentTraces();
988
+ const selectedTypes = c.req.queries("type") ?? [];
806
989
  return c.html(/* @__PURE__ */ jsx(TracesListPage, {
807
- traces,
990
+ traces: filterTracesByTypes(traces, selectedTypes),
991
+ availableTypes: distinctActivityTypes(traces),
992
+ selectedTypes,
993
+ initialSnapshot: snapshotOf(traces, selectedTypes),
808
994
  pathPrefix
809
995
  }));
810
996
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fedify/debugger",
3
- "version": "2.5.0-dev.2287+821dfa44",
3
+ "version": "2.5.0-dev.2304+3a599380",
4
4
  "description": "Embedded ActivityPub debug dashboard for Fedify",
5
5
  "type": "module",
6
6
  "main": "./dist/mod.cjs",
@@ -29,7 +29,7 @@
29
29
  "directory": "packages/debugger"
30
30
  },
31
31
  "peerDependencies": {
32
- "@fedify/fedify": "^2.5.0-dev.2287+821dfa44"
32
+ "@fedify/fedify": "^2.5.0-dev.2304+3a599380"
33
33
  },
34
34
  "dependencies": {
35
35
  "@logtape/logtape": "^2.2.0",