Skip to content

Commit

Permalink
feat: capture logs in browser storage (#576)
Browse files Browse the repository at this point in the history
* feat: add error reporting

feat: add error reporting

- notify user when claiming failed
- i18n error messages
- add errors table to indexed db
- add ui into settings with download / copy errors

exacter scss

* smaller log

i18n

* log error responses

* fixup!

* fixup spacing

* feat: capture all logs (#583)

* refactor: cleanup logging code

* test: add tests for logs capturing

* chore: add missing strings

* fix: prettier formatting

---------

Co-authored-by: michael1011 <[email protected]>
Co-authored-by: Kilian <[email protected]>
  • Loading branch information
3 people authored May 16, 2024
1 parent cd9a76f commit e8a9689
Show file tree
Hide file tree
Showing 19 changed files with 431 additions and 56 deletions.
20 changes: 15 additions & 5 deletions src/components/CopyButton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,16 +5,26 @@ import { Show, createSignal } from "solid-js";
import { useGlobalContext } from "../context/Global";
import { clipboard } from "../utils/helper";

const CopyButton = ({ data, label }) => {
const CopyButton = ({
data,
label,
btnClass = "btn",
}: {
data: string;
label: string;
btnClass?: string;
}) => {
const { t } = useGlobalContext();
const [buttonClass, setButtonClass] = createSignal<string>("btn");

const [buttonClass, setButtonClass] = createSignal<string>(btnClass);
const [buttonActive, setButtonActive] = createSignal<boolean>(false);

const onClick = () => {
clipboard(data.replaceAll(" ", ""));
setButtonClass("btn btn-active");
setButtonClass(`${btnClass} btn-active`);
setButtonActive(true);
setTimeout(() => {
setButtonClass("btn");
setButtonClass(btnClass);
setButtonActive(false);
}, 600);
};
Expand All @@ -23,7 +33,7 @@ const CopyButton = ({ data, label }) => {
<span class={buttonClass()} onClick={onClick}>
<Show
when={buttonActive() === true}
fallback=<BiRegularCopy size={21} />>
fallback={<BiRegularCopy size={21} />}>
<IoCheckmark size={21} />
</Show>
{t(label)}
Expand Down
6 changes: 4 additions & 2 deletions src/components/DownloadRefund.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ const DownloadRefund = () => {
const downloadRefundQr = (swap: any) => {
QRCode.toDataURL(JSON.stringify(createRefundData(swap)), { width: 400 })
.then((url: string) => {
if (isIos) {
if (isIos()) {
// Compatibility with third party iOS browsers
const newTab = window.open();
newTab.document.body.innerHTML = `
Expand All @@ -57,7 +57,9 @@ const DownloadRefund = () => {
<button
class="btn btn-success"
onclick={() =>
isMobile ? downloadRefundQr(swap()) : downloadRefundJson(swap())
isMobile()
? downloadRefundQr(swap())
: downloadRefundJson(swap())
}>
{t("download_refund_file")}
</button>
Expand Down
53 changes: 53 additions & 0 deletions src/components/Logs.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import {
BiRegularCopy,
BiRegularDownload,
BiRegularTrash,
} from "solid-icons/bi";
import { Show } from "solid-js/web";

import { useGlobalContext } from "../context/Global";
import { downloadJson } from "../utils/download";
import { clipboard, isIos } from "../utils/helper";

const Logs = () => {
const { getLogs, clearLogs } = useGlobalContext();

const clear = async (evt: MouseEvent) => {
evt.stopPropagation();
await clearLogs();
};

const copy = async (evt: MouseEvent) => {
evt.stopPropagation();
const logs = await getLogs();
clipboard(JSON.stringify(logs));
};

const download = async (evt: MouseEvent) => {
evt.stopPropagation();
downloadJson("boltz-logs", await getLogs());
};

return (
<div>
<span onClick={copy} class="btn-small">
<BiRegularCopy size={14} />
</span>
&nbsp;
<Show when={!isIos()}>
<span
onClick={download}
class="btn-small"
data-testid="logs-download">
<BiRegularDownload size={14} />
</span>
&nbsp;
</Show>
<span onClick={clear} class="btn-small btn-danger">
<BiRegularTrash size={14} />
</span>
</div>
);
};

export default Logs;
12 changes: 5 additions & 7 deletions src/components/RefundButton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,7 @@ const RefundButton = ({
setRefundTxId?: Setter<string>;
}) => {
const {
setNotificationType,
setNotification,
notify,
getSwap,
setSwapStorage,
setRefundAddress,
Expand Down Expand Up @@ -150,8 +149,7 @@ const RefundButton = ({
}
}
} catch (error) {
log.debug("refund failed", error);
setNotificationType("error");
log.warn("refund failed", error);
if (typeof error.json === "function") {
error
.json()
Expand All @@ -169,15 +167,15 @@ const RefundButton = ({
msg = t("locktime_not_satisfied");
}
log.error(msg);
setNotification(msg);
notify("error", msg);
})
.catch((genericError: any) => {
log.error(genericError);
setNotification(genericError);
notify("error", genericError);
});
} else {
log.error(error.message);
setNotification(error.message);
notify("error", error.message);
}
}

Expand Down
1 change: 1 addition & 0 deletions src/components/SettingsCog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { useGlobalContext } from "../context/Global";

const SettingsCog = () => {
const { setSettingsMenu } = useGlobalContext();

return (
<span id="settings-cog" onClick={() => setSettingsMenu(true)}>
<ImCog />
Expand Down
11 changes: 9 additions & 2 deletions src/components/SettingsMenu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@ import { IoClose } from "solid-icons/io";

import { useGlobalContext } from "../context/Global";
import "../style/settings.scss";
import Denominaton from "./Denomination";
import Denomination from "./Denomination";
import Logs from "./Logs";
import Separator from "./Separator";
import Tooltip from "./Tooltip";

Expand All @@ -25,14 +26,20 @@ const SettingsMenu = () => {
<label>{t("denomination")}: </label>
<Tooltip label="denomination_tooltip" />
<div class="spacer"></div>
<Denominaton />
<Denomination />
</span>
<span class="setting">
<label>{t("decimal_separator")}: </label>
<Tooltip label="decimal_tooltip" />
<div class="spacer"></div>
<Separator />
</span>
<span class="setting">
<label>{t("logs")}: </label>
<Tooltip label="logs_tooltip" />
<div class="spacer"></div>
<Logs />
</span>
</div>
</div>
);
Expand Down
11 changes: 6 additions & 5 deletions src/components/SwapChecker.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -125,7 +125,7 @@ export const SwapChecker = () => {
setTimeoutEta,
setTimeoutBlockheight,
} = usePayContext();
const { notify, updateSwapStatus, getSwap, getSwaps, setSwapStorage } =
const { notify, updateSwapStatus, getSwap, getSwaps, setSwapStorage, t } =
useGlobalContext();

const assetWebsocket = new Map<string, BoltzWebSocket>();
Expand Down Expand Up @@ -211,17 +211,18 @@ export const SwapChecker = () => {
if (claimedSwap.id === swap().id) {
setSwap(claimedSwap);
}

notify("success", `swap ${res.id} claimed`);
notify("success", t("claim_success", { id: res.id }));
} catch (e) {
log.warn("swapchecker failed to claim swap", e);
const msg = t("claim_fail", { id: currentSwap.id });
log.warn(msg, e);
notify("error", msg);
}
} else if (data.status === swapStatusPending.TransactionClaimPending) {
try {
await createSubmarineSignature(currentSwap);
} catch (e) {
log.warn(
"swapchecker failed to sign cooperative submarine claim",
`creating cooperative signature for submarine swap claim failed`,
e,
);
}
Expand Down
45 changes: 38 additions & 7 deletions src/context/Global.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import dict from "../i18n/i18n";
import { Pairs, getPairs } from "../utils/boltzClient";
import { detectEmbedded } from "../utils/embed";
import { isMobile } from "../utils/helper";
import { deleteOldLogs, injectLogWriter } from "../utils/logs";
import { swapStatusFinal } from "../utils/swapStatus";
import { checkWasmSupported } from "../utils/wasmSupport";
import { detectWebLNProvider } from "../utils/webln";
Expand Down Expand Up @@ -65,6 +66,9 @@ export type GlobalContextType = {
notify: (type: string, message: string) => void;
fetchPairs: (asset?: string) => void;

getLogs: () => Promise<Record<string, string[]>>;
clearLogs: () => Promise<void>;

setSwapStorage: (swap: SwapWithId) => Promise<any>;
getSwap: <T = any>(id: string) => Promise<T>;
getSwaps: <T = any>() => Promise<T[]>;
Expand Down Expand Up @@ -127,7 +131,9 @@ const GlobalProvider = (props: { children: any }) => {
const [hideHero, setHideHero] = createSignal<boolean>(false);

const [ref, setRef] = makePersisted(
createSignal(isMobile ? "boltz_webapp_mobile" : "boltz_webapp_desktop"),
createSignal(
isMobile() ? "boltz_webapp_mobile" : "boltz_webapp_desktop",
),
{
name: "ref",
...stringSerializer,
Expand Down Expand Up @@ -175,10 +181,33 @@ const GlobalProvider = (props: { children: any }) => {

// Use IndexedDB if available; fallback to LocalStorage
localforage.config({
name: "swaps",
driver: [localforage.INDEXEDDB, localforage.LOCALSTORAGE],
});

const logsForage = localforage.createInstance({
name: "logs",
});

injectLogWriter(logsForage);

createMemo(() => deleteOldLogs(logsForage));

const getLogs = async () => {
const logs: Record<string, string[]> = {};

await logsForage.iterate<string[], any>((logArray, date) => {
logs[date] = logArray;
});

return logs;
};

const clearLogs = () => logsForage.clear();

const swapsForage = localforage.createInstance({
name: "swaps",
});

migrateSwapsFromLocalStorage()
.then((migratedSwapCount) => {
if (migratedSwapCount === 0) {
Expand All @@ -197,16 +226,16 @@ const GlobalProvider = (props: { children: any }) => {
});

const setSwapStorage = (swap: SwapWithId) =>
localforage.setItem(swap.id, swap);
swapsForage.setItem(swap.id, swap);

const deleteSwap = (id: string) => localforage.removeItem(id);
const deleteSwap = (id: string) => swapsForage.removeItem(id);

const getSwap = <T = SwapWithId,>(id: string) => localforage.getItem<T>(id);
const getSwap = <T = SwapWithId,>(id: string) => swapsForage.getItem<T>(id);

const getSwaps = async <T = SwapWithId,>(): Promise<T[]> => {
const swaps: T[] = [];

await localforage.iterate<T, any>((swap) => {
await swapsForage.iterate<T, any>((swap) => {
swaps.push(swap);
});

Expand All @@ -227,7 +256,7 @@ const GlobalProvider = (props: { children: any }) => {
return false;
};

const clearSwaps = () => localforage.clear();
const clearSwaps = () => swapsForage.clear();

setI18n(detectLanguage(i18nConfigured()));
detectWebLNProvider().then((state: boolean) => setWebln(state));
Expand Down Expand Up @@ -301,6 +330,8 @@ const GlobalProvider = (props: { children: any }) => {
t,
notify,
fetchPairs,
getLogs,
clearLogs,
updateSwapStatus,
setSwapStorage,
getSwap,
Expand Down
16 changes: 16 additions & 0 deletions src/i18n/i18n.ts
Original file line number Diff line number Diff line change
Expand Up @@ -181,6 +181,10 @@ const dict = {
denomination_tooltip: "Choose your preferred denomination: BTC or sats",
decimal_tooltip:
"Choose your preferred decimal separator: dot or comma",
claim_success: "Swap {{ id }} claimed successfully!",
claim_fail: "Failed to claim swap: {{ id }}",
logs: "Logs",
logs_tooltip: "Logs of the web app, useful for debugging.",
},
de: {
language: "Deutsch",
Expand Down Expand Up @@ -371,6 +375,10 @@ const dict = {
"Wähle deine bevorzugte Denomination: BTC oder sats",
decimal_tooltip:
"Wähle dein bevorzugtes Dezimaltrennzeichen: Punkt oder Komma",
claim_success: "Swap {{ id }} erfolgreich geclaimed!",
claim_fail: "Swap {{ id }} konnte nicht geclaimed werden!",
logs: "Logs",
logs_tooltip: "Logs der Web App, nützlich für Debugging.",
},
es: {
language: "Español",
Expand Down Expand Up @@ -561,6 +569,10 @@ const dict = {
decimal_separator: "Separador decimal",
denomination_tooltip: "Elige tu denominación preferida: BTC o sats",
decimal_tooltip: "Elige tu separador decimal preferido: punto o coma",
claim_success: "¡Intercambio {{ id }} reclamado con éxito!",
claim_fail: "¡Error en reclamar el intercambio {{ id }}!",
logs: "Logs",
logs_tooltip: "Registros de la aplicación web, para depuración.",
},
zh: {
language: "中文",
Expand Down Expand Up @@ -731,6 +743,10 @@ const dict = {
decimal_separator: "小数分隔符",
denomination_tooltip: "选择您的首选面额:BTC 或 sats",
decimal_tooltip: "选择您的首选小数分隔符:点或逗号",
claim_success: "交换{{ id }}成功索赔!",
claim_fail: "交换{{ id }}索赔失败!",
logs: "日志",
logs_tooltip: "网络应用程序的日志,用于调试。",
},
};

Expand Down
2 changes: 1 addition & 1 deletion src/pages/Create.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -383,7 +383,7 @@ const Create = () => {
</Show>
<InvoiceInput />
</Show>
<Show when={isMobile}>
<Show when={isMobile()}>
<QrScan />
</Show>
<CreateButton />
Expand Down
2 changes: 1 addition & 1 deletion src/pages/History.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,7 @@ const History = () => {
/>
<hr />
<Show when={swaps().length > 0}>
<Show when={!isIos}>
<Show when={!isIos()}>
<button
class="btn btn-success"
onClick={backupLocalStorage}>
Expand Down
2 changes: 1 addition & 1 deletion src/status/InvoiceSet.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ const InvoiceSet = () => {
<hr class="spacer" />
<h3>{t("warning_expiry")}</h3>
</Show>
<Show when={isMobile}>
<Show when={isMobile()}>
<hr />
<a href={swap().bip21} class="btn btn-light">
{t("open_in_wallet")}
Expand Down
Loading

0 comments on commit e8a9689

Please sign in to comment.