feat: add an option to opt-out telemetry (#1236)
This commit is contained in:
@@ -68,7 +68,7 @@
|
||||
"js-md5": "^0.8.3",
|
||||
"jwt-decode": "^4.0.0",
|
||||
"next": "15.2.4",
|
||||
"posthog-js": "^1.205.0",
|
||||
"posthog-js": "^1.246.0",
|
||||
"react": "19.0.0",
|
||||
"react-color": "^2.19.3",
|
||||
"react-dom": "19.0.0",
|
||||
|
||||
@@ -325,5 +325,7 @@
|
||||
"DeepL": "ديب إل",
|
||||
"Google Translate": "ترجمة جوجل",
|
||||
"Azure Translator": "مترجم أزور",
|
||||
"Invert Image In Dark Mode": "عكس الصورة في الوضع المظلم"
|
||||
"Invert Image In Dark Mode": "عكس الصورة في الوضع المظلم",
|
||||
"Help improve Readest": "ساعد في تحسين ريديست",
|
||||
"Sharing anonymized statistics": "مشاركة إحصائيات مجهولة الهوية"
|
||||
}
|
||||
|
||||
@@ -321,5 +321,7 @@
|
||||
"DeepL": "DeepL",
|
||||
"Google Translate": "Google Übersetzer",
|
||||
"Azure Translator": "Azure Übersetzer",
|
||||
"Invert Image In Dark Mode": "Invertiere Bild im Dunkeln"
|
||||
"Invert Image In Dark Mode": "Invertiere Bild im Dunkeln",
|
||||
"Help improve Readest": "Readest verbessern helfen",
|
||||
"Sharing anonymized statistics": "Anonymisierte Statistiken teilen"
|
||||
}
|
||||
|
||||
@@ -321,5 +321,7 @@
|
||||
"DeepL": "DeepL",
|
||||
"Google Translate": "Μετάφραση Google",
|
||||
"Azure Translator": "Μεταφραστής Azure",
|
||||
"Invert Image In Dark Mode": "Αντιστροφή σε σκούρο"
|
||||
"Invert Image In Dark Mode": "Αντιστροφή σε σκούρο",
|
||||
"Help improve Readest": "Βοηθήστε στη βελτίωση του Readest",
|
||||
"Sharing anonymized statistics": "Μοιράζομαι ανώνυμες στατιστικές"
|
||||
}
|
||||
|
||||
@@ -322,5 +322,7 @@
|
||||
"DeepL": "DeepL",
|
||||
"Google Translate": "Google Translate",
|
||||
"Azure Translator": "Azure Translator",
|
||||
"Invert Image In Dark Mode": "Invertir imagen en oscuro"
|
||||
"Invert Image In Dark Mode": "Invertir imagen en oscuro",
|
||||
"Help improve Readest": "Ayuda a mejorar Readest",
|
||||
"Sharing anonymized statistics": "Compartiendo estadísticas anónimas"
|
||||
}
|
||||
|
||||
@@ -322,5 +322,7 @@
|
||||
"DeepL": "DeepL",
|
||||
"Google Translate": "Google Traduction",
|
||||
"Azure Translator": "Traducteur Azure",
|
||||
"Invert Image In Dark Mode": "Inverser l'image en mode sombre"
|
||||
"Invert Image In Dark Mode": "Inverser l'image en mode sombre",
|
||||
"Help improve Readest": "Contribuer à améliorer Readest",
|
||||
"Sharing anonymized statistics": "Partage de statistiques anonymisées"
|
||||
}
|
||||
|
||||
@@ -321,5 +321,7 @@
|
||||
"DeepL": "डीपएल",
|
||||
"Google Translate": "गूगल अनुवाद",
|
||||
"Azure Translator": "एज़्योर अनुवादक",
|
||||
"Invert Image In Dark Mode": "अंधेरे में छवि को उलटें"
|
||||
"Invert Image In Dark Mode": "अंधेरे में छवि को उलटें",
|
||||
"Help improve Readest": "Readest में सुधार करने में मदद करें",
|
||||
"Sharing anonymized statistics": "सांख्यिकी साझा करना"
|
||||
}
|
||||
|
||||
@@ -320,5 +320,7 @@
|
||||
"DeepL": "DeepL",
|
||||
"Google Translate": "Google Translate",
|
||||
"Azure Translator": "Azure Translator",
|
||||
"Invert Image In Dark Mode": "Inversi Gambar di Gelap"
|
||||
"Invert Image In Dark Mode": "Inversi Gambar di Gelap",
|
||||
"Help improve Readest": "Bantu tingkatkan Readest",
|
||||
"Sharing anonymized statistics": "Berbagi statistik anonim"
|
||||
}
|
||||
|
||||
@@ -322,5 +322,7 @@
|
||||
"DeepL": "DeepL",
|
||||
"Google Translate": "Google Translate",
|
||||
"Azure Translator": "Traduttore Azure",
|
||||
"Invert Image In Dark Mode": "Inverti immagine in scuro"
|
||||
"Invert Image In Dark Mode": "Inverti immagine in scuro",
|
||||
"Help improve Readest": "Aiuta a migliorare Readest",
|
||||
"Sharing anonymized statistics": "Condivisione di statistiche anonime"
|
||||
}
|
||||
|
||||
@@ -320,5 +320,7 @@
|
||||
"DeepL": "DeepL",
|
||||
"Google Translate": "Google翻訳",
|
||||
"Azure Translator": "Azure翻訳",
|
||||
"Invert Image In Dark Mode": "ダークモードで画像を反転"
|
||||
"Invert Image In Dark Mode": "ダークモードで画像を反転",
|
||||
"Help improve Readest": "Readestを改善する手助け",
|
||||
"Sharing anonymized statistics": "匿名化された統計情報を共有"
|
||||
}
|
||||
|
||||
@@ -320,5 +320,7 @@
|
||||
"DeepL": "DeepL",
|
||||
"Google Translate": "구글 번역",
|
||||
"Azure Translator": "Azure 번역기",
|
||||
"Invert Image In Dark Mode": "어두운 곳에서 이미지 반전"
|
||||
"Invert Image In Dark Mode": "어두운 곳에서 이미지 반전",
|
||||
"Help improve Readest": "Readest 개선에 도움을 주세요",
|
||||
"Sharing anonymized statistics": "익명화된 통계 공유"
|
||||
}
|
||||
|
||||
@@ -321,5 +321,7 @@
|
||||
"DeepL": "DeepL",
|
||||
"Google Translate": "Google Vertalen",
|
||||
"Azure Translator": "Azure Vertaler",
|
||||
"Invert Image In Dark Mode": "Inverteer afbeelding in het donker"
|
||||
"Invert Image In Dark Mode": "Inverteer afbeelding in het donker",
|
||||
"Help improve Readest": "Help Readest verbeteren",
|
||||
"Sharing anonymized statistics": "Anonieme statistieken delen"
|
||||
}
|
||||
|
||||
@@ -323,5 +323,7 @@
|
||||
"DeepL": "DeepL",
|
||||
"Google Translate": "Tłumacz Google",
|
||||
"Azure Translator": "Azure Translator",
|
||||
"Invert Image In Dark Mode": "Odwróć obraz w ciemności"
|
||||
"Invert Image In Dark Mode": "Odwróć obraz w ciemności",
|
||||
"Help improve Readest": "Pomóż ulepszyć Readest",
|
||||
"Sharing anonymized statistics": "Udostępnianie zanonimizowanych statystyk"
|
||||
}
|
||||
|
||||
@@ -322,5 +322,7 @@
|
||||
"DeepL": "DeepL",
|
||||
"Google Translate": "Google Tradutor",
|
||||
"Azure Translator": "Tradutor Azure",
|
||||
"Invert Image In Dark Mode": "Inverter Imagem no Escuro"
|
||||
"Invert Image In Dark Mode": "Inverter Imagem no Escuro",
|
||||
"Help improve Readest": "Ajude a melhorar o Readest",
|
||||
"Sharing anonymized statistics": "Compartilhando estatísticas anonimizadas"
|
||||
}
|
||||
|
||||
@@ -323,5 +323,7 @@
|
||||
"DeepL": "DeepL",
|
||||
"Google Translate": "Google Translate",
|
||||
"Azure Translator": "Azure Translator",
|
||||
"Invert Image In Dark Mode": "Инвертировать в тёмной теме"
|
||||
"Invert Image In Dark Mode": "Инвертировать в тёмной теме",
|
||||
"Help improve Readest": "Помогите улучшить Readest",
|
||||
"Sharing anonymized statistics": "Отправка анонимной статистики"
|
||||
}
|
||||
|
||||
@@ -321,5 +321,7 @@
|
||||
"DeepL": "DeepL",
|
||||
"Google Translate": "Google Çeviri",
|
||||
"Azure Translator": "Azure Çevirmen",
|
||||
"Invert Image In Dark Mode": "Karartılmış Görüntüyü Ters Çevir"
|
||||
"Invert Image In Dark Mode": "Karartılmış Görüntüyü Ters Çevir",
|
||||
"Help improve Readest": "Readest’i geliştirin",
|
||||
"Sharing anonymized statistics": "Anonim verileri paylaş"
|
||||
}
|
||||
|
||||
@@ -323,5 +323,7 @@
|
||||
"DeepL": "DeepL",
|
||||
"Google Translate": "Google Translate",
|
||||
"Azure Translator": "Azure Translator",
|
||||
"Invert Image In Dark Mode": "Інвертувати зображення в темряві"
|
||||
"Invert Image In Dark Mode": "Інвертувати зображення в темряві",
|
||||
"Help improve Readest": "Допоможіть покращити Readest",
|
||||
"Sharing anonymized statistics": "Поділ анонімною статистикою"
|
||||
}
|
||||
|
||||
@@ -320,5 +320,7 @@
|
||||
"DeepL": "DeepL",
|
||||
"Google Translate": "Google Dịch",
|
||||
"Azure Translator": "Azure Translator",
|
||||
"Invert Image In Dark Mode": "Đảo ảnh chế độ tối"
|
||||
"Invert Image In Dark Mode": "Đảo ảnh chế độ tối",
|
||||
"Help improve Readest": "Giúp cải thiện Readest",
|
||||
"Sharing anonymized statistics": "Chia sẻ thống kê ẩn danh"
|
||||
}
|
||||
|
||||
@@ -320,5 +320,7 @@
|
||||
"DeepL": "DeepL",
|
||||
"Google Translate": "Google Translate",
|
||||
"Azure Translator": "Azure Translator",
|
||||
"Invert Image In Dark Mode": "深色主题下图片反色"
|
||||
"Invert Image In Dark Mode": "深色主题下图片反色",
|
||||
"Help improve Readest": "帮助改进 Readest",
|
||||
"Sharing anonymized statistics": "分享匿名统计数据"
|
||||
}
|
||||
|
||||
@@ -320,5 +320,7 @@
|
||||
"DeepL": "DeepL",
|
||||
"Google Translate": "Google Translate",
|
||||
"Azure Translator": "Azure Translator",
|
||||
"Invert Image In Dark Mode": "深色主題下圖片反相"
|
||||
"Invert Image In Dark Mode": "深色主題下圖片反相",
|
||||
"Help improve Readest": "幫助改進 Readest",
|
||||
"Sharing anonymized statistics": "分享匿名統計數據"
|
||||
}
|
||||
|
||||
@@ -16,6 +16,7 @@ import { useResponsiveSize } from '@/hooks/useResponsiveSize';
|
||||
import { getStoragePlanData } from '@/utils/access';
|
||||
import { navigateToLogin, navigateToProfile } from '@/utils/nav';
|
||||
import { tauriHandleSetAlwaysOnTop, tauriHandleToggleFullScreen } from '@/utils/window';
|
||||
import { optInTelemetry, optOutTelemetry } from '@/utils/telemetry';
|
||||
import { QuotaType } from '@/types/user';
|
||||
import UserAvatar from '@/components/UserAvatar';
|
||||
import MenuItem from '@/components/MenuItem';
|
||||
@@ -40,6 +41,7 @@ const SettingsMenu: React.FC<SettingsMenuProps> = ({ setIsDropdownOpen }) => {
|
||||
const [isAutoImportBooksOnOpen, setIsAutoImportBooksOnOpen] = useState(
|
||||
settings.autoImportBooksOnOpen,
|
||||
);
|
||||
const [isTelemetryEnabled, setIsTelemetryEnabled] = useState(settings.telemetryEnabled);
|
||||
const iconSize = useResponsiveSize(16);
|
||||
|
||||
const showAboutReadest = () => {
|
||||
@@ -119,6 +121,18 @@ const SettingsMenu: React.FC<SettingsMenuProps> = ({ setIsDropdownOpen }) => {
|
||||
setIsOpenLastBooks(settings.openLastBooks);
|
||||
};
|
||||
|
||||
const toggleTelemetry = () => {
|
||||
settings.telemetryEnabled = !settings.telemetryEnabled;
|
||||
if (settings.telemetryEnabled) {
|
||||
optInTelemetry();
|
||||
} else {
|
||||
optOutTelemetry();
|
||||
}
|
||||
setSettings(settings);
|
||||
saveSettings(envConfig, settings);
|
||||
setIsTelemetryEnabled(settings.telemetryEnabled);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (!user || !token) return;
|
||||
const storagPlan = getStoragePlanData(token);
|
||||
@@ -215,6 +229,12 @@ const SettingsMenu: React.FC<SettingsMenuProps> = ({ setIsDropdownOpen }) => {
|
||||
<hr className='border-base-200 my-1' />
|
||||
{isWebAppPlatform() && <MenuItem label={_('Download Readest')} onClick={downloadReadest} />}
|
||||
<MenuItem label={_('About Readest')} onClick={showAboutReadest} />
|
||||
<MenuItem
|
||||
label={_('Help improve Readest')}
|
||||
description={isTelemetryEnabled ? _('Sharing anonymized statistics') : ''}
|
||||
Icon={isTelemetryEnabled ? MdCheck : undefined}
|
||||
onClick={toggleTelemetry}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -5,6 +5,7 @@ import { useResponsiveSize } from '@/hooks/useResponsiveSize';
|
||||
|
||||
interface MenuItemProps {
|
||||
label: string;
|
||||
description?: string;
|
||||
buttonClass?: string;
|
||||
labelClass?: string;
|
||||
shortcut?: string;
|
||||
@@ -17,6 +18,7 @@ interface MenuItemProps {
|
||||
|
||||
const MenuItem: React.FC<MenuItemProps> = ({
|
||||
label,
|
||||
description,
|
||||
buttonClass,
|
||||
labelClass,
|
||||
shortcut,
|
||||
@@ -30,40 +32,52 @@ const MenuItem: React.FC<MenuItemProps> = ({
|
||||
const menuButton = (
|
||||
<button
|
||||
className={clsx(
|
||||
'hover:bg-base-300 text-base-content flex h-10 w-full items-center justify-between rounded-md p-1',
|
||||
'hover:bg-base-300 text-base-content flex w-full flex-col items-center justify-center rounded-md p-1 py-[10px]',
|
||||
disabled && 'btn-disabled text-gray-400',
|
||||
buttonClass,
|
||||
)}
|
||||
onClick={onClick}
|
||||
disabled={disabled}
|
||||
>
|
||||
<div className='flex min-w-0 items-center'>
|
||||
{!noIcon && (
|
||||
<span style={{ minWidth: `${iconSize}px` }}>
|
||||
{typeof Icon === 'function' ? (
|
||||
<Icon className='text-base-content' size={iconSize} />
|
||||
) : (
|
||||
Icon
|
||||
<div className='flex w-full items-center justify-between'>
|
||||
<div className='flex min-w-0 items-center'>
|
||||
{!noIcon && (
|
||||
<span style={{ minWidth: `${iconSize}px` }}>
|
||||
{typeof Icon === 'function' ? (
|
||||
<Icon className='text-base-content' size={iconSize} />
|
||||
) : (
|
||||
Icon
|
||||
)}
|
||||
</span>
|
||||
)}
|
||||
<span
|
||||
className={clsx('mx-2 flex-1 truncate text-base sm:text-sm', labelClass)}
|
||||
style={{ minWidth: 0 }}
|
||||
>
|
||||
{label}
|
||||
</span>
|
||||
</div>
|
||||
{shortcut && (
|
||||
<kbd
|
||||
className={clsx(
|
||||
'border-base-300/40 bg-base-300/75 text-neutral-content hidden rounded-md border shadow-sm sm:flex',
|
||||
'shrink-0 px-1.5 py-0.5 text-xs font-medium',
|
||||
)}
|
||||
>
|
||||
{shortcut}
|
||||
</kbd>
|
||||
)}
|
||||
</div>
|
||||
<div className='flex w-full'>
|
||||
{description && (
|
||||
<span
|
||||
className='mt-1 truncate text-start text-xs text-gray-500'
|
||||
style={{ minWidth: 0, paddingInlineStart: noIcon ? '0' : `${iconSize + 8}px` }}
|
||||
>
|
||||
{description}
|
||||
</span>
|
||||
)}
|
||||
<span
|
||||
className={clsx('mx-2 flex-1 truncate text-base sm:text-sm', labelClass)}
|
||||
style={{ minWidth: 0 }}
|
||||
>
|
||||
{label}
|
||||
</span>
|
||||
</div>
|
||||
{shortcut && (
|
||||
<kbd
|
||||
className={clsx(
|
||||
'border-base-300/40 bg-base-300/75 text-neutral-content hidden rounded-md border shadow-sm sm:flex',
|
||||
'shrink-0 px-1.5 py-0.5 text-xs font-medium',
|
||||
)}
|
||||
>
|
||||
{shortcut}
|
||||
</kbd>
|
||||
)}
|
||||
</button>
|
||||
);
|
||||
|
||||
|
||||
@@ -1,7 +1,13 @@
|
||||
'use client';
|
||||
import { ReactNode } from 'react';
|
||||
import posthog from 'posthog-js';
|
||||
import { ReactNode } from 'react';
|
||||
import { PostHogProvider } from 'posthog-js/react';
|
||||
import { TELEMETRY_OPT_OUT_KEY } from '@/utils/telemetry';
|
||||
|
||||
const shouldDisablePostHog = () => {
|
||||
if (typeof window === 'undefined') return false;
|
||||
return localStorage.getItem(TELEMETRY_OPT_OUT_KEY) === 'true';
|
||||
};
|
||||
|
||||
if (
|
||||
typeof window !== 'undefined' &&
|
||||
@@ -12,6 +18,9 @@ if (
|
||||
api_host: process.env['NEXT_PUBLIC_POSTHOG_HOST'],
|
||||
person_profiles: 'always',
|
||||
});
|
||||
if (shouldDisablePostHog()) {
|
||||
posthog.opt_out_capturing();
|
||||
}
|
||||
}
|
||||
export const CSPostHogProvider = ({ children }: { children: ReactNode }) => {
|
||||
return <PostHogProvider client={posthog}>{children}</PostHogProvider>;
|
||||
|
||||
@@ -41,6 +41,7 @@ export const DEFAULT_SYSTEM_SETTINGS: Partial<SystemSettings> = {
|
||||
openLastBooks: false,
|
||||
lastOpenBooks: [],
|
||||
autoImportBooksOnOpen: false,
|
||||
telemetryEnabled: true,
|
||||
libraryViewMode: 'grid',
|
||||
librarySortBy: 'updated',
|
||||
librarySortAscending: false,
|
||||
|
||||
@@ -31,6 +31,7 @@ export interface SystemSettings {
|
||||
openLastBooks: boolean;
|
||||
lastOpenBooks: string[];
|
||||
autoImportBooksOnOpen: boolean;
|
||||
telemetryEnabled: boolean;
|
||||
libraryViewMode: LibraryViewModeType;
|
||||
librarySortBy: LibrarySortByType;
|
||||
librarySortAscending: boolean;
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
import posthog from 'posthog-js';
|
||||
|
||||
export const TELEMETRY_OPT_OUT_KEY = 'readest-telemetry-opt-out';
|
||||
|
||||
export const hasOptedOutTelemetry = () => {
|
||||
return localStorage.getItem(TELEMETRY_OPT_OUT_KEY) === 'true';
|
||||
};
|
||||
|
||||
export const captureEvent = (event: string, properties?: Record<string, unknown>) => {
|
||||
if (!hasOptedOutTelemetry()) {
|
||||
posthog.capture(event, properties);
|
||||
}
|
||||
};
|
||||
|
||||
export const optInTelemetry = () => {
|
||||
localStorage.setItem(TELEMETRY_OPT_OUT_KEY, 'false');
|
||||
posthog.opt_in_capturing();
|
||||
};
|
||||
export const optOutTelemetry = () => {
|
||||
localStorage.setItem(TELEMETRY_OPT_OUT_KEY, 'true');
|
||||
posthog.opt_out_capturing();
|
||||
};
|
||||
Generated
+13
-5
@@ -135,8 +135,8 @@ importers:
|
||||
specifier: 15.2.4
|
||||
version: 15.2.4(@babel/core@7.26.7)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
|
||||
posthog-js:
|
||||
specifier: ^1.205.0
|
||||
version: 1.205.0
|
||||
specifier: ^1.246.0
|
||||
version: 1.246.0
|
||||
react:
|
||||
specifier: 19.0.0
|
||||
version: 19.0.0
|
||||
@@ -4877,8 +4877,16 @@ packages:
|
||||
resolution: {integrity: sha512-OCVPnIObs4N29kxTjzLfUryOkvZEq+pf8jTF0lg8E7uETuWHA+v7j3c/xJmiqpX450191LlmZfUKkXxkTry7nA==}
|
||||
engines: {node: ^10 || ^12 || >=14}
|
||||
|
||||
posthog-js@1.205.0:
|
||||
resolution: {integrity: sha512-zP4SQ9Dg9JwqkEteoAOviAAAMdT/nJ4vk1jqfE6fVudziEa3szkQWd7czk5ehlEdrKFUE85MonCKW4L/uwtybA==}
|
||||
posthog-js@1.246.0:
|
||||
resolution: {integrity: sha512-5lN/UMqDfxsLeSnT3LsY4P+eD1H+P9qxgN/iUk473LmhCM7IV8TAfdjJj23nnXk/euGNQtvyINYoD2DG+d4eEw==}
|
||||
peerDependencies:
|
||||
'@rrweb/types': 2.0.0-alpha.17
|
||||
rrweb-snapshot: 2.0.0-alpha.17
|
||||
peerDependenciesMeta:
|
||||
'@rrweb/types':
|
||||
optional: true
|
||||
rrweb-snapshot:
|
||||
optional: true
|
||||
|
||||
preact@10.25.0:
|
||||
resolution: {integrity: sha512-6bYnzlLxXV3OSpUxLdaxBmE7PMOu0aR3pG6lryK/0jmvcDFPlcXGQAt5DpK3RITWiDrfYZRI0druyaK/S9kYLg==}
|
||||
@@ -11851,7 +11859,7 @@ snapshots:
|
||||
picocolors: 1.1.1
|
||||
source-map-js: 1.2.1
|
||||
|
||||
posthog-js@1.205.0:
|
||||
posthog-js@1.246.0:
|
||||
dependencies:
|
||||
core-js: 3.39.0
|
||||
fflate: 0.4.8
|
||||
|
||||
Reference in New Issue
Block a user