Skip to content

Commit 247b209

Browse files
authored
Merge pull request #152 from notea-org/feature/debugging
Debugging utilities + more user-friendly error listing
2 parents 2431c96 + 102a573 commit 247b209

23 files changed

+1225
-63
lines changed

.gitignore

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ node_modules
2323
npm-debug.log*
2424
yarn-debug.log*
2525
yarn-error.log*
26+
/logs
2627

2728
# local env files
2829
.env
@@ -43,4 +44,4 @@ tsconfig.tsbuildinfo
4344
# Editor files
4445
# - IntelliJ IDEA
4546
.idea/
46-
*.iml
47+
*.iml

Dockerfile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ COPY --from=builder /app/node_modules ./node_modules
2626
VOLUME /app/config
2727
# VOLUME /app/data
2828

29-
ENV CONFIG_FILE=/app/config/notea.yml
29+
ENV CONFIG_FILE=/app/config/notea.yml LOG_DIRECTORY=/app/logs
3030

3131
EXPOSE 3000
3232

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
import { Button } from '@material-ui/core';
2+
import useI18n from 'libs/web/hooks/use-i18n';
3+
import { FC } from 'react';
4+
import { logLevelToString, DebugInformation } from 'libs/shared/debugging';
5+
6+
export const DebugInfoCopyButton: FC<{
7+
debugInfo: DebugInformation;
8+
}> = ({ debugInfo }) => {
9+
const { t } = useI18n();
10+
11+
function generateDebugInfo(): string {
12+
let data =
13+
'Notea debug information' +
14+
'\nTime ' +
15+
new Date(Date.now()).toISOString() +
16+
'\n\n';
17+
function ensureNewline() {
18+
if (!data.endsWith('\n') && data.length >= 1) {
19+
data += '\n';
20+
}
21+
}
22+
23+
if (debugInfo.issues.length > 0) {
24+
ensureNewline();
25+
data += 'Configuration errors: ';
26+
let i = 1;
27+
const prefixLength = debugInfo.issues.length.toString().length;
28+
for (const issue of debugInfo.issues) {
29+
const prefix = i.toString().padStart(prefixLength, ' ') + ': ';
30+
const empty = ' '.repeat(prefixLength + 2);
31+
32+
data += prefix + issue.name;
33+
data += empty + '// ' + issue.cause;
34+
data += empty + issue.description;
35+
i++;
36+
}
37+
} else {
38+
data += 'No detected configuration errors.';
39+
}
40+
41+
if (debugInfo.logs.length > 0) {
42+
ensureNewline();
43+
for (const log of debugInfo.logs) {
44+
data += `[${new Date(log.time).toISOString()} ${log.name}] ${logLevelToString(log.level)}: ${log.msg}`;
45+
}
46+
}
47+
48+
49+
50+
return data;
51+
}
52+
53+
function copyDebugInfo() {
54+
const text = generateDebugInfo();
55+
56+
navigator.clipboard
57+
.writeText(text)
58+
.then(() => {
59+
// nothing
60+
})
61+
.catch((e) => {
62+
console.error(
63+
'Error when trying to copy debugging information to clipboard: %O',
64+
e
65+
);
66+
});
67+
}
68+
69+
return (
70+
<Button variant="contained" onClick={copyDebugInfo}>
71+
{t('Copy debugging information')}
72+
</Button>
73+
);
74+
};

components/debug/issue-list.tsx

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
import { FC } from 'react';
2+
import { Issue } from 'libs/shared/debugging';
3+
import { Issue as IssueDisplay } from './issue';
4+
5+
export const IssueList: FC<{
6+
issues: Array<Issue>;
7+
}> = ({ issues }) => {
8+
return (
9+
<div className="flex flex-col">
10+
{issues.map((v, i) => {
11+
return (
12+
<IssueDisplay key={i} issue={v} id={`issue-${i}`}/>
13+
);
14+
})}
15+
</div>
16+
);
17+
};

components/debug/issue.tsx

Lines changed: 193 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,193 @@
1+
import { FC } from 'react';
2+
import {
3+
getNameFromRecommendation,
4+
getNameFromSeverity,
5+
Issue as IssueInfo,
6+
IssueFix,
7+
IssueSeverity
8+
} from 'libs/shared/debugging';
9+
import {
10+
Accordion as MuiAccordion,
11+
AccordionDetails as MuiAccordionDetails,
12+
AccordionSummary as MuiAccordionSummary,
13+
withStyles
14+
} from '@material-ui/core';
15+
import { ChevronDownIcon } from '@heroicons/react/outline';
16+
import useI18n from 'libs/web/hooks/use-i18n';
17+
import { errorToString, isProbablyError } from 'libs/shared/util';
18+
19+
const Accordion = withStyles({
20+
root: {
21+
boxShadow: 'none',
22+
'&:not(:last-child)': {
23+
borderBottom: 0,
24+
},
25+
'&:before': {
26+
display: 'none',
27+
},
28+
'&$expanded': {
29+
margin: 'auto 0',
30+
},
31+
},
32+
expanded: {
33+
},
34+
})(MuiAccordion);
35+
36+
const AccordionSummary = withStyles({
37+
root: {
38+
backgroundColor: 'rgba(0, 0, 0, .03)',
39+
borderBottom: '1px solid rgba(0, 0, 0, .125)',
40+
borderTopRightRadius: 'inherit',
41+
borderBottomRightRadius: 'inherit',
42+
marginBottom: -1,
43+
minHeight: 56,
44+
'&$expanded': {
45+
minHeight: 56,
46+
borderBottomRightRadius: '0'
47+
},
48+
},
49+
content: {
50+
'&$expanded': {
51+
margin: '12px 0',
52+
}
53+
},
54+
expanded: {},
55+
})(MuiAccordionSummary);
56+
57+
const AccordionDetails = withStyles((theme) => ({
58+
root: {
59+
padding: theme.spacing(2),
60+
},
61+
}))(MuiAccordionDetails);
62+
63+
interface FixProps {
64+
id: string;
65+
fix: IssueFix;
66+
}
67+
const Fix: FC<FixProps> = ({ id, fix }) => {
68+
const i18n = useI18n();
69+
const { t } = i18n;
70+
const steps = fix.steps ?? [];
71+
return (
72+
<Accordion key={id} className={"bg-gray-300"}>
73+
<AccordionSummary
74+
expandIcon={<ChevronDownIcon width=".8em"/>}
75+
aria-controls={`${id}-details`}
76+
id={`${id}-summary`}
77+
>
78+
<div className={"flex flex-col"}>
79+
{fix.recommendation !== 0 && (
80+
<span className={"text-xs uppercase"}>{getNameFromRecommendation(fix.recommendation, i18n)}</span>
81+
)}
82+
<span className={"font-bold"}>{fix.description}</span>
83+
</div>
84+
</AccordionSummary>
85+
<AccordionDetails className={"rounded-[inherit]"}>
86+
{steps.length > 0 ? (
87+
<ol className="list-decimal list-inside">
88+
{steps.map((step, i) => {
89+
const stepId = `${id}-step-${i}`;
90+
return (
91+
<li key={stepId}>{step}</li>
92+
);
93+
})}
94+
</ol>
95+
) : (
96+
<span>{t('No steps were provided by Notea to perform this fix.')}</span>
97+
)}
98+
</AccordionDetails>
99+
</Accordion>
100+
);
101+
};
102+
103+
interface IssueProps {
104+
issue: IssueInfo;
105+
id: string;
106+
}
107+
108+
export const Issue: FC<IssueProps> = function (props) {
109+
const { issue, id } = props;
110+
const i18n = useI18n();
111+
const { t } = i18n;
112+
113+
let borderColour: string;
114+
switch (issue.severity) {
115+
case IssueSeverity.SUGGESTION:
116+
borderColour = "border-gray-500";
117+
break;
118+
case IssueSeverity.WARNING:
119+
borderColour = "border-yellow-100";
120+
break;
121+
case IssueSeverity.ERROR:
122+
borderColour = "border-red-500";
123+
break;
124+
case IssueSeverity.FATAL_ERROR:
125+
borderColour = "border-red-300";
126+
break;
127+
}
128+
129+
const Cause: FC<{ value: IssueInfo['cause'] }> = ({ value }) => {
130+
if (typeof value === 'string') {
131+
return (
132+
<div className={"flex flex-row my-1"}>
133+
<span className={"font-bold"}>{t('Cause')}</span>
134+
<span className={"font-mono ml-1"}>{value}</span>
135+
</div>
136+
);
137+
}
138+
139+
if (isProbablyError(value)) {
140+
return (
141+
<div className={"flex flex-col my-1"}>
142+
<span className={"font-bold"}>{t('Cause')}</span>
143+
<pre className={"font-mono whitespace-pre"}>{errorToString(value)}</pre>
144+
</div>
145+
);
146+
}
147+
148+
throw new Error("Invalid value type");
149+
};
150+
151+
return (
152+
<Accordion className={`border-l-4 ${borderColour} bg-gray-200`}>
153+
<AccordionSummary
154+
className={"bg-gray-100"}
155+
expandIcon={<ChevronDownIcon width=".8em"/>}
156+
aria-controls={`${id}-details`}
157+
id={`${id}-summary`}
158+
>
159+
<div className={"flex flex-col bg-transparent"}>
160+
<span className={"text-xs uppercase"}>
161+
{issue.isRuntime === true ? 'Runtime ' : ''}
162+
{getNameFromSeverity(issue.severity, i18n)}
163+
</span>
164+
<span className={"font-bold"}>{issue.name}</span>
165+
</div>
166+
</AccordionSummary>
167+
<AccordionDetails className={"flex flex-col"}>
168+
<span>{issue.description ?? t('No description was provided for this issue.')}</span>
169+
{issue.cause && <Cause value={issue.cause}/>}
170+
171+
{issue.fixes.length > 0 ? (
172+
<div className={"mt-1 flex flex-col"}>
173+
<span className={"font-bold"}>{t('Potential fixes')}</span>
174+
<div>
175+
{issue.fixes.map((fix, i) => {
176+
const fixId = `${id}-fix-${i}`;
177+
return (
178+
<Fix
179+
key={fixId}
180+
id={fixId}
181+
fix={fix}
182+
/>
183+
);
184+
})}
185+
</div>
186+
</div>
187+
) : (
188+
<span>{t('No fixes are known by Notea for this issue.')}</span>
189+
)}
190+
</AccordionDetails>
191+
</Accordion>
192+
);
193+
};

components/debug/logs.tsx

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import { FC } from 'react';
2+
import { logLevelToString, LogLike } from 'libs/shared/debugging';
3+
4+
interface LogsProps {
5+
logs: Array<LogLike>
6+
}
7+
8+
export const Logs: FC<LogsProps> = (props) => {
9+
return (
10+
<div className={"flex flex-col space-y-1"}>
11+
{props.logs.length > 0 ? props.logs.map((log, i) => {
12+
return (
13+
<div className={"flex flex-col border-l pl-2"} key={i}>
14+
<span className={"text-sm uppercase"}>
15+
{logLevelToString(log.level)} at {new Date(log.time ?? 0).toLocaleString()} from <b>{log.name}</b>
16+
</span>
17+
<span className={"font-mono"}>
18+
{log.msg}
19+
</span>
20+
</div>
21+
);
22+
}) : (
23+
<span>No logs.</span>
24+
)}
25+
</div>
26+
);
27+
};

components/settings/debugging.tsx

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import { IssueList } from 'components/debug/issue-list';
2+
import type { FC } from 'react';
3+
import { DebugInfoCopyButton } from 'components/debug/debug-info-copy-button';
4+
import { DebugInformation, Issue } from 'libs/shared/debugging';
5+
6+
export const Debugging: FC<{
7+
debugInfo: DebugInformation;
8+
}> = (props) => {
9+
const issues: Array<Issue> = [...props.debugInfo.issues].sort((a, b) => b.severity - a.severity);
10+
return (
11+
<div className="my-2">
12+
<IssueList
13+
issues={issues}
14+
/>
15+
<div className={'flex flex-row my-2'}>
16+
<DebugInfoCopyButton debugInfo={props.debugInfo} />
17+
</div>
18+
</div>
19+
);
20+
};

0 commit comments

Comments
 (0)