Merge pull request #1371 from hydralauncher/feature/adding-sentry

Feature/adding sentry
This commit is contained in:
Zamitto 2025-01-01 19:48:17 -03:00 committed by GitHub
commit 5c2bafcfe8
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
21 changed files with 178 additions and 62 deletions

View File

@ -47,13 +47,13 @@
"auto-launch": "^5.0.6",
"axios": "^1.7.9",
"better-sqlite3": "^11.7.0",
"check-disk-space": "^3.4.0",
"classnames": "^2.5.1",
"color": "^4.2.3",
"color.js": "^1.2.0",
"create-desktop-shortcuts": "^1.11.0",
"date-fns": "^3.6.0",
"dexie": "^4.0.10",
"diskusage": "^1.2.0",
"electron-log": "^5.2.4",
"electron-updater": "^6.3.9",
"file-type": "^19.6.0",

View File

@ -178,7 +178,8 @@
"select_folder": "Select folder",
"backup_from": "Backup from {{date}}",
"custom_backup_location_set": "Custom backup location set",
"no_directory_selected": "No directory selected"
"no_directory_selected": "No directory selected",
"no_write_permission": "Cannot download into this directory. Click here to learn more."
},
"activation": {
"title": "Activate Hydra",

View File

@ -167,7 +167,8 @@
"select_folder": "Selecione a pasta",
"manage_files_description": "Gerencie quais arquivos serão feitos backup",
"clear": "Limpar",
"no_directory_selected": "Nenhum diretório selecionado"
"no_directory_selected": "Nenhum diretório selecionado",
"no_write_permission": "O download não pode ser feito neste diretório. Clique aqui para saber mais."
},
"activation": {
"title": "Ativação",

View File

@ -0,0 +1,15 @@
import fs from "node:fs";
import { registerEvent } from "../register-event";
const checkFolderWritePermission = async (
_event: Electron.IpcMainInvokeEvent,
path: string
) =>
new Promise((resolve) => {
fs.access(path, fs.constants.W_OK, (err) => {
resolve(!err);
});
});
registerEvent("checkFolderWritePermission", checkFolderWritePermission);

View File

@ -1,10 +1,10 @@
import checkDiskSpace from "check-disk-space";
import disk from "diskusage";
import { registerEvent } from "../register-event";
const getDiskFreeSpace = async (
_event: Electron.IpcMainInvokeEvent,
path: string
) => checkDiskSpace(path);
) => disk.check(path);
registerEvent("getDiskFreeSpace", getDiskFreeSpace);

View File

@ -11,6 +11,7 @@ import "./catalogue/get-trending-games";
import "./catalogue/get-publishers";
import "./catalogue/get-developers";
import "./hardware/get-disk-free-space";
import "./hardware/check-folder-write-permission";
import "./library/add-game-to-library";
import "./library/create-game-shortcut";
import "./library/close-game";
@ -30,6 +31,8 @@ import "./library/select-game-wine-prefix";
import "./misc/open-checkout";
import "./misc/open-external";
import "./misc/show-open-dialog";
import "./misc/get-features";
import "./misc/show-item-in-folder";
import "./torrenting/cancel-game-download";
import "./torrenting/pause-game-download";
import "./torrenting/resume-game-download";
@ -71,7 +74,6 @@ import "./cloud-save/delete-game-artifact";
import "./cloud-save/select-game-backup-path";
import "./notifications/publish-new-repacks-notification";
import { isPortableVersion } from "@main/helpers";
import "./misc/show-item-in-folder";
ipcMain.handle("ping", () => "pong");
ipcMain.handle("getVersion", () => appVersion);

View File

@ -0,0 +1,8 @@
import { registerEvent } from "../register-event";
import { HydraApi } from "@main/services";
const getFeatures = async (_event: Electron.IpcMainInvokeEvent) => {
return HydraApi.get<string[]>("/features", null, { needsAuth: false });
};
registerEvent("getFeatures", getFeatures);

View File

@ -64,7 +64,10 @@ export class WindowManager {
this.mainWindow.webContents.session.webRequest.onBeforeSendHeaders(
(details, callback) => {
if (details.webContentsId !== this.mainWindow?.webContents.id) {
if (
details.webContentsId !== this.mainWindow?.webContents.id ||
details.url.includes("chatwoot")
) {
return callback(details);
}
@ -81,15 +84,11 @@ export class WindowManager {
this.mainWindow.webContents.session.webRequest.onHeadersReceived(
(details, callback) => {
if (details.webContentsId !== this.mainWindow?.webContents.id) {
return callback(details);
}
if (details.url.includes("featurebase")) {
return callback(details);
}
if (details.url.includes("chatwoot")) {
if (
details.webContentsId !== this.mainWindow?.webContents.id ||
details.url.includes("featurebase") ||
details.url.includes("chatwoot")
) {
return callback(details);
}

View File

@ -150,6 +150,8 @@ contextBridge.exposeInMainWorld("electron", {
/* Hardware */
getDiskFreeSpace: (path: string) =>
ipcRenderer.invoke("getDiskFreeSpace", path),
checkFolderWritePermission: (path: string) =>
ipcRenderer.invoke("checkFolderWritePermission", path),
/* Cloud save */
uploadSaveGame: (
@ -226,6 +228,7 @@ contextBridge.exposeInMainWorld("electron", {
ipcRenderer.invoke("showOpenDialog", options),
showItemInFolder: (path: string) =>
ipcRenderer.invoke("showItemInFolder", path),
getFeatures: () => ipcRenderer.invoke("getFeatures"),
platform: process.platform,
/* Auto update */

View File

@ -46,6 +46,12 @@ export function Modal({
}, [onClose]);
const isTopMostModal = () => {
if (
document.querySelector(
".featurebase-widget-overlay.featurebase-display-block"
)
)
return false;
const openModals = document.querySelectorAll("[role=dialog]");
return (

View File

@ -1,6 +1,5 @@
import React, { useId, useMemo, useState } from "react";
import type { RecipeVariants } from "@vanilla-extract/recipes";
import type { FieldError, FieldErrorsImpl, Merge } from "react-hook-form";
import { EyeClosedIcon, EyeIcon } from "@primer/octicons-react";
import { useTranslation } from "react-i18next";
@ -23,7 +22,7 @@ export interface TextFieldProps
HTMLDivElement
>;
rightContent?: React.ReactNode | null;
error?: FieldError | Merge<FieldError, FieldErrorsImpl<any>> | undefined;
error?: string | React.ReactNode;
}
export const TextField = React.forwardRef<HTMLInputElement, TextFieldProps>(
@ -55,10 +54,7 @@ export const TextField = React.forwardRef<HTMLInputElement, TextFieldProps>(
}, [props.type, isPasswordVisible]);
const hintContent = useMemo(() => {
if (error && error.message)
return (
<small className={styles.errorLabel}>{error.message as string}</small>
);
if (error) return <small className={styles.errorLabel}>{error}</small>;
if (hint) return <small>{hint}</small>;
return null;

View File

@ -31,7 +31,7 @@ import type {
CatalogueSearchPayload,
} from "@types";
import type { AxiosProgressEvent } from "axios";
import type { DiskSpace } from "check-disk-space";
import type disk from "diskusage";
declare global {
declare module "*.svg" {
@ -140,7 +140,8 @@ declare global {
) => Promise<{ fingerprint: string }>;
/* Hardware */
getDiskFreeSpace: (path: string) => Promise<DiskSpace>;
getDiskFreeSpace: (path: string) => Promise<disk.DiskUsage>;
checkFolderWritePermission: (path: string) => Promise<boolean>;
/* Cloud save */
uploadSaveGame: (
@ -195,6 +196,7 @@ declare global {
options: Electron.OpenDialogOptions
) => Promise<Electron.OpenDialogReturnValue>;
showItemInFolder: (path: string) => Promise<void>;
getFeatures: () => Promise<string[]>;
platform: NodeJS.Platform;
/* Auto update */

View File

@ -6,3 +6,4 @@ export * from "./redux";
export * from "./use-user-details";
export * from "./use-format";
export * from "./use-repacks";
export * from "./use-feature";

View File

@ -0,0 +1,23 @@
import { useEffect } from "react";
enum Feature {
CheckDownloadWritePermission = "CHECK_DOWNLOAD_WRITE_PERMISSION",
}
export function useFeature() {
useEffect(() => {
window.electron.getFeatures().then((features) => {
localStorage.setItem("features", JSON.stringify(features || []));
});
}, []);
const isFeatureEnabled = (feature: Feature) => {
const features = JSON.parse(localStorage.getItem("features") || "[]");
return features.includes(feature);
};
return {
isFeatureEnabled,
Feature,
};
}

View File

@ -13,6 +13,7 @@ import type {
UpdateProfileRequest,
UserDetails,
} from "@types";
import * as Sentry from "@sentry/react";
import { UserFriendModalTab } from "@renderer/pages/shared-modals/user-friend-modal";
import { isFuture, isToday } from "date-fns";
@ -30,6 +31,8 @@ export function useUserDetails() {
} = useAppSelector((state) => state.userDetails);
const clearUserDetails = useCallback(async () => {
Sentry.setUser(null);
dispatch(setUserDetails(null));
dispatch(setProfileBackground(null));
@ -44,6 +47,12 @@ export function useUserDetails() {
const updateUserDetails = useCallback(
async (userDetails: UserDetails) => {
Sentry.setUser({
id: userDetails.id,
username: userDetails.username,
email: userDetails.email ?? undefined,
});
dispatch(setUserDetails(userDetails));
window.localStorage.setItem("userDetails", JSON.stringify(userDetails));
},

View File

@ -36,3 +36,10 @@ export const downloaderIcon = style({
position: "absolute",
left: `${SPACING_UNIT * 2}px`,
});
export const pathError = style({
cursor: "pointer",
":hover": {
textDecoration: "underline",
},
});

View File

@ -1,7 +1,6 @@
import { useEffect, useMemo, useState } from "react";
import { useCallback, useEffect, useMemo, useState } from "react";
import { Trans, useTranslation } from "react-i18next";
import { DiskSpace } from "check-disk-space";
import * as styles from "./download-settings-modal.css";
import { Button, Link, Modal, TextField } from "@renderer/components";
import { CheckCircleFillIcon, DownloadIcon } from "@primer/octicons-react";
@ -10,7 +9,7 @@ import { Downloader, formatBytes, getDownloadersForUris } from "@shared";
import type { GameRepack } from "@types";
import { SPACING_UNIT } from "@renderer/theme.css";
import { DOWNLOADER_NAME } from "@renderer/constants";
import { useAppSelector, useToast } from "@renderer/hooks";
import { useAppSelector, useFeature, useToast } from "@renderer/hooks";
export interface DownloadSettingsModalProps {
visible: boolean;
@ -33,21 +32,45 @@ export function DownloadSettingsModal({
const { showErrorToast } = useToast();
const [diskFreeSpace, setDiskFreeSpace] = useState<DiskSpace | null>(null);
const [diskFreeSpace, setDiskFreeSpace] = useState<number | null>(null);
const [selectedPath, setSelectedPath] = useState("");
const [downloadStarting, setDownloadStarting] = useState(false);
const [selectedDownloader, setSelectedDownloader] =
useState<Downloader | null>(null);
const [hasWritePermission, setHasWritePermission] = useState<boolean | null>(
null
);
const { isFeatureEnabled, Feature } = useFeature();
const userPreferences = useAppSelector(
(state) => state.userPreferences.value
);
const getDiskFreeSpace = (path: string) => {
window.electron.getDiskFreeSpace(path).then((result) => {
setDiskFreeSpace(result.free);
});
};
const checkFolderWritePermission = useCallback(
async (path: string) => {
if (isFeatureEnabled(Feature.CheckDownloadWritePermission)) {
const result = await window.electron.checkFolderWritePermission(path);
setHasWritePermission(result);
} else {
setHasWritePermission(true);
}
},
[Feature, isFeatureEnabled]
);
useEffect(() => {
if (visible) {
getDiskFreeSpace(selectedPath);
checkFolderWritePermission(selectedPath);
}
}, [visible, selectedPath]);
}, [visible, checkFolderWritePermission, selectedPath]);
const downloaders = useMemo(() => {
return getDownloadersForUris(repack?.uris ?? []);
@ -84,12 +107,6 @@ export function DownloadSettingsModal({
userPreferences?.realDebridApiToken,
]);
const getDiskFreeSpace = (path: string) => {
window.electron.getDiskFreeSpace(path).then((result) => {
setDiskFreeSpace(result);
});
};
const handleChooseDownloadsPath = async () => {
const { filePaths } = await window.electron.showOpenDialog({
defaultPath: selectedPath,
@ -124,7 +141,7 @@ export function DownloadSettingsModal({
visible={visible}
title={t("download_settings")}
description={t("space_left_on_disk", {
space: formatBytes(diskFreeSpace?.free ?? 0),
space: formatBytes(diskFreeSpace ?? 0),
})}
onClose={onClose}
>
@ -168,23 +185,32 @@ export function DownloadSettingsModal({
gap: `${SPACING_UNIT}px`,
}}
>
<div className={styles.downloadsPathField}>
<TextField
value={selectedPath}
readOnly
disabled
label={t("download_path")}
/>
<Button
style={{ alignSelf: "flex-end" }}
theme="outline"
onClick={handleChooseDownloadsPath}
disabled={downloadStarting}
>
{t("change")}
</Button>
</div>
<TextField
value={selectedPath}
readOnly
disabled
label={t("download_path")}
error={
hasWritePermission === false ? (
<span
className={styles.pathError}
data-open-article="cannot-write-directory"
>
{t("no_write_permission")}
</span>
) : undefined
}
rightContent={
<Button
style={{ alignSelf: "flex-end" }}
theme="outline"
onClick={handleChooseDownloadsPath}
disabled={downloadStarting}
>
{t("change")}
</Button>
}
/>
<p className={styles.hintText}>
<Trans i18nKey="select_folder_hint" ns="game_details">
@ -195,7 +221,11 @@ export function DownloadSettingsModal({
<Button
onClick={handleStartClick}
disabled={downloadStarting || selectedDownloader === null}
disabled={
downloadStarting ||
selectedDownloader === null ||
!hasWritePermission
}
>
<DownloadIcon />
{t("download_now")}

View File

@ -163,7 +163,7 @@ export function EditProfileModal(
minLength={3}
maxLength={50}
containerProps={{ style: { width: "100%" } }}
error={errors.displayName}
error={errors.displayName?.message}
/>
</div>

View File

@ -105,7 +105,7 @@ export function ReportProfile() {
{...register("description")}
label={t("report_description")}
placeholder={t("report_description_placeholder")}
error={errors.description}
error={errors.description?.message}
/>
<Button

View File

@ -150,7 +150,7 @@ export function AddDownloadSourceModal({
{...register("url")}
label={t("download_source_url")}
placeholder={t("insert_valid_json_url")}
error={errors.url}
error={errors.url?.message}
rightContent={
<Button
type="button"

View File

@ -4381,11 +4381,6 @@ chalk@^5.3.0:
resolved "https://registry.yarnpkg.com/chalk/-/chalk-5.3.0.tgz#67c20a7ebef70e7f3970a01f90fa210cb6860385"
integrity sha512-dLitG79d+GV1Nb/VYcCDFivJeK1hiukt9QjRNVOsUtTy1rR1YJsmpGGTZ3qJos+uw7WmWF4wUwBd9jxjocFC2w==
check-disk-space@^3.4.0:
version "3.4.0"
resolved "https://registry.yarnpkg.com/check-disk-space/-/check-disk-space-3.4.0.tgz#eb8e69eee7a378fd12e35281b8123a8b4c4a8ff7"
integrity sha512-drVkSqfwA+TvuEhFipiR1OC9boEGZL5RrWvVsOthdcvQNXyCCuKkEiTOTXZ7qxSf/GLwq4GvzfrQD/Wz325hgw==
chokidar@^3.5.3:
version "3.6.0"
resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.6.0.tgz#197c6cc669ef2a8dc5e7b4d97ee4e092c3eb0d5b"
@ -4921,6 +4916,14 @@ dir-glob@^3.0.1:
dependencies:
path-type "^4.0.0"
diskusage@^1.2.0:
version "1.2.0"
resolved "https://registry.yarnpkg.com/diskusage/-/diskusage-1.2.0.tgz#3e8ae42333d5d7e0c7d93e055d7fea9ea841bc88"
integrity sha512-2u3OG3xuf5MFyzc4MctNRUKjjwK+UkovRYdD2ed/NZNZPrt0lqHnLKxGhlFVvAb4/oufIgQG3nWgwmeTbHOvXA==
dependencies:
es6-promise "^4.2.8"
nan "^2.18.0"
dmg-builder@25.1.8:
version "25.1.8"
resolved "https://registry.yarnpkg.com/dmg-builder/-/dmg-builder-25.1.8.tgz#41f3b725edd896156e891016a44129e1bd580430"
@ -5327,6 +5330,11 @@ es6-error@^4.1.1:
resolved "https://registry.yarnpkg.com/es6-error/-/es6-error-4.1.1.tgz#9e3af407459deed47e9a91f9b885a84eb05c561d"
integrity sha512-Um/+FxMr9CISWh0bi5Zv0iOD+4cFh5qLeks1qhAopKVAJw3drgKbKySikp7wGhDL0HPeaja0P5ULZrxLkniUVg==
es6-promise@^4.2.8:
version "4.2.8"
resolved "https://registry.yarnpkg.com/es6-promise/-/es6-promise-4.2.8.tgz#4eb21594c972bc40553d276e510539143db53e0a"
integrity sha512-HJDGx5daxeIvxdBxvG2cb9g4tEvwIk3i8+nhX0yGrYmZUzbkdg8QbDevheDB8gd0//uPj4c1EQua8Q+MViT0/w==
esbuild@^0.21.3, esbuild@^0.21.5:
version "0.21.5"
resolved "https://registry.yarnpkg.com/esbuild/-/esbuild-0.21.5.tgz#9ca301b120922959b766360d8ac830da0d02997d"
@ -7425,6 +7433,11 @@ mz@^2.4.0:
object-assign "^4.0.1"
thenify-all "^1.0.0"
nan@^2.18.0:
version "2.22.0"
resolved "https://registry.yarnpkg.com/nan/-/nan-2.22.0.tgz#31bc433fc33213c97bad36404bb68063de604de3"
integrity sha512-nbajikzWTMwsW+eSsNm3QwlOs7het9gGJU5dDZzRTQGk03vyBOauxgI4VakDzE0PtsGTmXPsXTbbjVhRwR5mpw==
nanoid@^3.3.7:
version "3.3.7"
resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.7.tgz#d0c301a691bc8d54efa0a2226ccf3fe2fd656bd8"