Skip to content

Commit ba57b85

Browse files
SuaYootw4lemma-sgikreymer
authored
feat: Display behavior logs (#2531)
- Displays behavior logs wherever error logs are shown - Makes page URL in detail dialog clickable rather than in row column to prevent accidental navigation - Rename "Download Logs" -> "Download All Logs" and add tooltip with additional context --------- Co-authored-by: Tessa Walsh <tessa@bitarchivist.net> Co-authored-by: Emma Segal-Grossman <hi@emma.cafe> Co-authored-by: Ilya Kreymer <ikreymer@gmail.com>
1 parent 785fd85 commit ba57b85

File tree

10 files changed

+691
-362
lines changed

10 files changed

+691
-362
lines changed

frontend/docs/docs/user-guide/archived-items.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -64,11 +64,11 @@ To download an entire archived item as a single WACZ file, click the _Download I
6464

6565
To combine multiple archived items and download them all as a single WACZ file, add them to a collection and [download the collection](collection.md#download-a-collection).
6666

67-
### Error Logs
67+
### Logs
6868

69-
View a list of errors that may have been encountered during crawling. Clicking an error in the list will reveal additional information.
69+
View a list of errors and behavior logs that were generated during crawling. Clicking a log entry in the list will reveal additional information.
7070

71-
All log entries with that were recorded in the creation of the archived item can be downloaded in JSONL format by pressing the _Download Logs_ button.
71+
Only a subset of the logs generated by the crawler are visible in this tab. All log entries that were recorded in the creation of the archived item can be downloaded in JSONL format by pressing the _Download All Logs_ button.
7272

7373
### Crawl Settings
7474

frontend/src/components/ui/badge.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ export type BadgeVariant =
1010
| "danger"
1111
| "neutral"
1212
| "primary"
13+
| "blue"
1314
| "high-contrast";
1415

1516
/**
@@ -44,6 +45,7 @@ export class Badge extends TailwindElement {
4445
neutral: tw`bg-neutral-100 text-neutral-600`,
4546
"high-contrast": tw`bg-neutral-600 text-neutral-0`,
4647
primary: tw`bg-primary text-neutral-0`,
48+
blue: tw`bg-cyan-50 text-neutral-600`,
4749
}[
4850
this.variant
4951
]} inline-flex items-center justify-center rounded-sm px-2 align-[1px] text-xs"

frontend/src/components/ui/pagination.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ export type PageChangeEvent = CustomEvent<PageChangeDetail>;
2525
* </btrix-pagination>
2626
* ```
2727
*
28-
* @event page-change {PageChangeEvent}
28+
* @fires page-change {PageChangeEvent}
2929
*/
3030
@customElement("btrix-pagination")
3131
@localized()
Lines changed: 309 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,309 @@
1+
import { localized, msg } from "@lit/localize";
2+
import clsx from "clsx";
3+
import { css, html, nothing } from "lit";
4+
import { customElement, property, state } from "lit/decorators.js";
5+
6+
import { TailwindElement } from "@/classes/TailwindElement";
7+
import { noData } from "@/strings/ui";
8+
import { CrawlLogContext, CrawlLogLevel, type CrawlLog } from "@/types/crawler";
9+
import { truncate } from "@/utils/css";
10+
import { stopProp } from "@/utils/events";
11+
import { tw } from "@/utils/tailwind";
12+
13+
const labelFor: Record<CrawlLogContext, string> = {
14+
[CrawlLogContext.General]: msg("General", {
15+
desc: "'General' crawl log context type",
16+
}),
17+
[CrawlLogContext.Behavior]: msg("Page Behavior"),
18+
[CrawlLogContext.BehaviorScript]: msg("Built-in Behavior"),
19+
[CrawlLogContext.BehaviorScriptCustom]: msg("Custom Behavior Script"),
20+
};
21+
22+
const contextLevelFor: Record<CrawlLogContext, number> = {
23+
[CrawlLogContext.Behavior]: 1,
24+
[CrawlLogContext.BehaviorScript]: 2,
25+
[CrawlLogContext.General]: 3,
26+
[CrawlLogContext.BehaviorScriptCustom]: 4,
27+
};
28+
// Minimum context level to highlight
29+
const MIN_CONTEXT_LEVEL = 3 as const;
30+
31+
/**
32+
* Tabular list of logs
33+
*/
34+
@customElement("btrix-crawl-log-table")
35+
@localized()
36+
export class CrawlLogTable extends TailwindElement {
37+
static styles = [
38+
truncate,
39+
css`
40+
pre {
41+
white-space: pre-wrap;
42+
font-family: var(--sl-font-mono);
43+
font-size: var(--sl-font-size-x-small);
44+
margin: 0;
45+
padding: var(--sl-spacing-small);
46+
border: 1px solid var(--sl-panel-border-color);
47+
border-radius: var(--sl-border-radius-medium);
48+
}
49+
`,
50+
];
51+
52+
@property({ type: Array })
53+
logs?: CrawlLog[];
54+
55+
/**
56+
* Number to offset index by, e.g. for pagination
57+
*/
58+
@property({ type: Number })
59+
offset = 1;
60+
61+
@state()
62+
private selectedLog:
63+
| (CrawlLog & {
64+
index: number;
65+
})
66+
| null = null;
67+
68+
render() {
69+
if (!this.logs) return;
70+
71+
const rowClasses = tw`grid grid-cols-[5rem_2.5rem_20rem_1fr] leading-[1.3]`;
72+
73+
return html`<btrix-numbered-list class="text-xs">
74+
<btrix-numbered-list-header slot="header">
75+
<div class=${rowClasses}>
76+
<div class="px-2">${msg("Timestamp")}</div>
77+
<div class="text-center">${msg("Level")}</div>
78+
<div class="px-2">${msg("Message")}</div>
79+
<div class="px-2">${msg("Page URL")}</div>
80+
</div>
81+
</btrix-numbered-list-header>
82+
${this.logs.map((log: CrawlLog, idx) => {
83+
const selected = this.selectedLog?.index === idx;
84+
return html`
85+
<btrix-numbered-list-item
86+
class="group"
87+
hoverable
88+
?selected=${selected}
89+
aria-selected="${selected}"
90+
@click=${() => {
91+
this.selectedLog = {
92+
index: idx,
93+
...log,
94+
};
95+
}}
96+
>
97+
<div slot="marker" class="min-w-[3ch]">
98+
${idx + 1 + this.offset}.
99+
</div>
100+
<div
101+
class=${clsx(
102+
rowClasses,
103+
(contextLevelFor[log.context as unknown as CrawlLogContext] ||
104+
0) < MIN_CONTEXT_LEVEL
105+
? tw`text-stone-400`
106+
: tw`text-stone-800`,
107+
tw`group-hover:text-inherit`,
108+
)}
109+
>
110+
<div>
111+
<sl-tooltip
112+
placement="bottom"
113+
@sl-hide=${stopProp}
114+
@sl-after-hide=${stopProp}
115+
>
116+
<btrix-format-date
117+
date=${log.timestamp}
118+
hour="2-digit"
119+
minute="2-digit"
120+
second="2-digit"
121+
hour-format="24"
122+
>
123+
</btrix-format-date>
124+
<btrix-format-date
125+
slot="content"
126+
date=${log.timestamp}
127+
month="long"
128+
day="numeric"
129+
year="numeric"
130+
hour="numeric"
131+
minute="numeric"
132+
second="numeric"
133+
time-zone-name="short"
134+
>
135+
</btrix-format-date>
136+
</sl-tooltip>
137+
</div>
138+
<div class="pr-4 text-center">
139+
<sl-tooltip
140+
class="capitalize"
141+
content=${log.logLevel}
142+
placement="bottom"
143+
@sl-hide=${stopProp}
144+
@sl-after-hide=${stopProp}
145+
>
146+
${this.renderLevel(log)}
147+
</sl-tooltip>
148+
</div>
149+
<div class="whitespace-pre-wrap">${log.message}</div>
150+
${log.details.page
151+
? html`
152+
<div class="truncate" title="${log.details.page}">
153+
${log.details.page}
154+
</div>
155+
`
156+
: html`<div class="text-neutral-400 group-hover:text-inherit">
157+
${noData}
158+
</div>`}
159+
</div>
160+
</btrix-numbered-list-item>
161+
`;
162+
})}
163+
</btrix-numbered-list>
164+
165+
<btrix-dialog
166+
.label=${msg("Log Details")}
167+
.open=${!!this.selectedLog}
168+
class="[--width:40rem]"
169+
@sl-show=${stopProp}
170+
@sl-after-show=${stopProp}
171+
@sl-hide=${stopProp}
172+
@sl-after-hide=${(e: CustomEvent) => {
173+
stopProp(e);
174+
this.selectedLog = null;
175+
}}
176+
>${this.renderLogDetails()}</btrix-dialog
177+
> `;
178+
}
179+
180+
private renderLevel(log: CrawlLog) {
181+
const logLevel = log.logLevel;
182+
const contextLevel =
183+
contextLevelFor[log.context as unknown as CrawlLogContext] || 0;
184+
const baseClasses = tw`size-4 group-hover:text-inherit`;
185+
186+
switch (logLevel) {
187+
case CrawlLogLevel.Fatal:
188+
return html`
189+
<sl-icon
190+
name="exclamation-octagon-fill"
191+
class=${clsx(tw`text-danger-500`, baseClasses)}
192+
></sl-icon>
193+
`;
194+
case CrawlLogLevel.Error:
195+
return html`
196+
<sl-icon
197+
name="exclamation-triangle-fill"
198+
class=${clsx(tw`text-danger-500`, baseClasses)}
199+
></sl-icon>
200+
`;
201+
case CrawlLogLevel.Warning:
202+
return html`
203+
<sl-icon
204+
name="exclamation-diamond-fill"
205+
class=${clsx(tw`text-warning-500`, baseClasses)}
206+
></sl-icon>
207+
`;
208+
case CrawlLogLevel.Info:
209+
return html`
210+
<sl-icon
211+
name="info-circle-fill"
212+
class=${clsx(
213+
tw`text-neutral-400`,
214+
contextLevel < MIN_CONTEXT_LEVEL && tw`opacity-30`,
215+
baseClasses,
216+
)}
217+
></sl-icon>
218+
`;
219+
case CrawlLogLevel.Debug:
220+
return html`
221+
<sl-icon
222+
name="bug"
223+
class=${clsx(tw`text-neutral-400`, baseClasses)}
224+
></sl-icon>
225+
`;
226+
default:
227+
return html`
228+
<sl-icon
229+
name="question-lg"
230+
class=${clsx(tw`text-neutral-300`, baseClasses)}
231+
></sl-icon>
232+
`;
233+
break;
234+
}
235+
}
236+
237+
private renderLogDetails() {
238+
if (!this.selectedLog) return;
239+
const { context, details } = this.selectedLog;
240+
const { page, stack, ...unknownDetails } = details;
241+
242+
return html`
243+
<btrix-desc-list>
244+
<btrix-desc-list-item label=${msg("TIMESTAMP")}>
245+
${this.selectedLog.timestamp}
246+
</btrix-desc-list-item>
247+
<btrix-desc-list-item label=${msg("CONTEXT")}>
248+
${Object.values(CrawlLogContext).includes(
249+
context as unknown as CrawlLogContext,
250+
)
251+
? labelFor[context as CrawlLogContext]
252+
: html`<span class="capitalize">${context}</span>`}
253+
</btrix-desc-list-item>
254+
<btrix-desc-list-item label=${msg("MESSAGE")}>
255+
${this.selectedLog.message}
256+
</btrix-desc-list-item>
257+
${page
258+
? html`<btrix-desc-list-item label=${msg("PAGE")}>
259+
${this.renderPage(page)}
260+
</btrix-desc-list-item>`
261+
: nothing}
262+
${stack
263+
? html`<btrix-desc-list-item label=${msg("STACK")}>
264+
${this.renderPre(stack)}
265+
</btrix-desc-list-item>`
266+
: nothing}
267+
${Object.entries(unknownDetails).map(
268+
([key, value]) => html`
269+
<btrix-desc-list-item label=${key.toUpperCase()}>
270+
${typeof value !== "string" && typeof value !== "number"
271+
? this.renderPre(value)
272+
: value
273+
? html`<span class="break-all">${value}</span>`
274+
: noData}
275+
</btrix-desc-list-item>
276+
`,
277+
)}
278+
</btrix-desc-list>
279+
`;
280+
}
281+
282+
private renderPage(page: string) {
283+
return html`
284+
<sl-tooltip
285+
content=${msg("Open live page in new tab")}
286+
@sl-hide=${stopProp}
287+
@sl-after-hide=${stopProp}
288+
>
289+
<a
290+
class="break-all text-blue-500 hover:text-blue-400"
291+
href=${page}
292+
target="_blank"
293+
rel="noopener noreferrer nofollow"
294+
>${page}</a
295+
>
296+
</sl-tooltip>
297+
`;
298+
}
299+
300+
private renderPre(value: unknown) {
301+
let str = value;
302+
if (typeof value !== "string") {
303+
str = JSON.stringify(value, null, 2);
304+
}
305+
return html`<pre
306+
class="overflow-auto whitespace-pre"
307+
><code>${str}</code></pre>`;
308+
}
309+
}

0 commit comments

Comments
 (0)