mirror of
https://github.com/tapframe/NuvioStreaming.git
synced 2026-08-07 11:52:34 +00:00
feat: Implement privacy settings and telemetry management
This commit is contained in:
parent
c43e6d879f
commit
454a6f387f
9 changed files with 1009 additions and 32 deletions
38
App.tsx
38
App.tsx
|
|
@ -47,19 +47,45 @@ import { AccountProvider, useAccount } from './src/contexts/AccountContext';
|
|||
import { ToastProvider } from './src/contexts/ToastContext';
|
||||
import { mmkvStorage } from './src/services/mmkvStorage';
|
||||
import { CampaignManager } from './src/components/promotions/CampaignManager';
|
||||
import { isErrorReportingEnabledSync } from './src/services/telemetryService';
|
||||
|
||||
// Initialize Sentry with privacy-first defaults
|
||||
// Settings are loaded from telemetryService and can be controlled by user
|
||||
// Note: Full dynamic control requires app restart as Sentry initializes at startup
|
||||
Sentry.init({
|
||||
dsn: 'https://1a58bf436454d346e5852b7bfd3c95e8@o4509536317276160.ingest.de.sentry.io/4509536317734992',
|
||||
|
||||
// Adds more context data to events (IP address, cookies, user, etc.)
|
||||
// For more information, visit: https://docs.sentry.io/platforms/react-native/data-management/data-collected/
|
||||
sendDefaultPii: true,
|
||||
// Privacy-first: Disable PII by default (IP address, cookies, user data)
|
||||
// Users can opt-in via Privacy Settings if they choose
|
||||
sendDefaultPii: false,
|
||||
|
||||
// Configure Session Replay conservatively to avoid startup overhead in production
|
||||
replaysSessionSampleRate: __DEV__ ? 0.1 : 0,
|
||||
replaysOnErrorSampleRate: __DEV__ ? 1 : 0,
|
||||
// Session Replay completely disabled by default for privacy
|
||||
// This prevents screen recording without explicit user consent
|
||||
replaysSessionSampleRate: 0,
|
||||
replaysOnErrorSampleRate: 0,
|
||||
|
||||
// Only include feedback integration (user-initiated, not automatic)
|
||||
integrations: [Sentry.feedbackIntegration()],
|
||||
|
||||
// beforeSend hook to respect user's telemetry preferences
|
||||
// Uses synchronous MMKV read to check preference immediately
|
||||
beforeSend: (event) => {
|
||||
// Check if error reporting is disabled (synchronous check)
|
||||
if (!isErrorReportingEnabledSync()) {
|
||||
// Drop the event - user has opted out
|
||||
return null;
|
||||
}
|
||||
return event;
|
||||
},
|
||||
|
||||
// beforeSendTransaction hook for performance monitoring
|
||||
beforeSendTransaction: (event) => {
|
||||
if (!isErrorReportingEnabledSync()) {
|
||||
return null;
|
||||
}
|
||||
return event;
|
||||
},
|
||||
|
||||
// uncomment the line below to enable Spotlight (https://spotlightjs.com)
|
||||
// spotlight: __DEV__,
|
||||
});
|
||||
|
|
|
|||
17
index.html
17
index.html
|
|
@ -876,7 +876,7 @@
|
|||
Back
|
||||
</button>
|
||||
<h1>Privacy</h1>
|
||||
<p class="last-updated">Last updated: January 2025</p>
|
||||
<p class="last-updated">Last updated: February 5, 2026</p>
|
||||
|
||||
<div class="privacy-section">
|
||||
<h2>No Account Sync</h2>
|
||||
|
|
@ -901,10 +901,23 @@
|
|||
<ul>
|
||||
<li>TMDB for metadata like posters and cast info</li>
|
||||
<li>Trakt.tv (optional) for watch history sync</li>
|
||||
<li>Sentry for anonymous crash reporting</li>
|
||||
<li>Sentry for crash reporting (privacy-first defaults, no PII by default)</li>
|
||||
<li>PostHog for optional analytics (disabled by default)</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="privacy-section">
|
||||
<h2>Telemetry & Analytics</h2>
|
||||
<p>Nuvio includes optional telemetry to improve stability and user experience:</p>
|
||||
<ul>
|
||||
<li><strong>Sentry</strong> captures crash reports and errors. Personal data (IP, device identifiers)
|
||||
is disabled by default and can be enabled only if you opt in.</li>
|
||||
<li><strong>PostHog</strong> analytics are disabled by default and require explicit opt-in.</li>
|
||||
<li><strong>Session Replay</strong> is disabled by default and can be enabled only if you opt in.</li>
|
||||
</ul>
|
||||
<p>You can disable all telemetry at any time in the app under <strong>Settings → Privacy & Data</strong>.</p>
|
||||
</div>
|
||||
|
||||
<div class="privacy-section">
|
||||
<h2>Content Disclaimer</h2>
|
||||
<p>Nuvio is a media player and aggregator. We do not host any content. All video content is
|
||||
|
|
|
|||
|
|
@ -68,8 +68,6 @@ export const CustomAlert = ({
|
|||
const handleActionPress = useCallback((action: { label: string; onPress: () => void; style?: object }) => {
|
||||
try {
|
||||
action.onPress();
|
||||
// Don't auto-close here if the action handles it, or check if we should
|
||||
// Standard behavior is to close
|
||||
onClose();
|
||||
} catch (error) {
|
||||
console.warn('[CustomAlert] Error in action handler:', error);
|
||||
|
|
|
|||
|
|
@ -22,7 +22,7 @@ const BREAKPOINTS = {
|
|||
tv: 1440,
|
||||
};
|
||||
|
||||
const IMDb_LOGO = 'https://upload.wikimedia.org/wikipedia/commons/6/69/IMDB_Logo_2016.svg';
|
||||
const IMDb_LOGO = 'https://upload.wikimedia.org/wikipedia/commons/thumb/6/69/IMDB_Logo_2016.svg/575px-IMDB_Logo_2016.svg.png';
|
||||
|
||||
export const RATING_PROVIDERS = {
|
||||
imdb: {
|
||||
|
|
@ -95,21 +95,41 @@ export const RatingsSection: React.FC<RatingsSectionProps> = ({ imdbId, type })
|
|||
}, [deviceType]);
|
||||
|
||||
const iconSize = useMemo(() => {
|
||||
switch (deviceType) {
|
||||
case 'tv':
|
||||
return 20;
|
||||
case 'largeTablet':
|
||||
return 18;
|
||||
case 'tablet':
|
||||
return 16;
|
||||
default:
|
||||
return 16;
|
||||
const baseSize = deviceType === 'tv' ? 20 : deviceType === 'largeTablet' ? 18 : deviceType === 'tablet' ? 16 : 16;
|
||||
const numRatings = ratings ? Object.keys(ratings).length : 0;
|
||||
// Reduce size if many ratings to fit in one line
|
||||
if (numRatings > 4) {
|
||||
return Math.max(baseSize - 2, 12);
|
||||
}
|
||||
}, [deviceType]);
|
||||
return baseSize;
|
||||
}, [deviceType, ratings]);
|
||||
|
||||
const textSize = useMemo(() => (isTV ? 16 : isLargeTablet ? 15 : isTablet ? 14 : 14), [isTV, isLargeTablet, isTablet]);
|
||||
const itemSpacing = useMemo(() => (isTV ? 16 : isLargeTablet ? 14 : isTablet ? 12 : 12), [isTV, isLargeTablet, isTablet]);
|
||||
const iconTextGap = useMemo(() => (isTV ? 6 : isLargeTablet ? 5 : isTablet ? 4 : 4), [isTV, isLargeTablet, isTablet]);
|
||||
const textSize = useMemo(() => {
|
||||
const baseSize = isTV ? 16 : isLargeTablet ? 15 : isTablet ? 14 : 14;
|
||||
const numRatings = ratings ? Object.keys(ratings).length : 0;
|
||||
if (numRatings > 4) {
|
||||
return Math.max(baseSize - 1, 12);
|
||||
}
|
||||
return baseSize;
|
||||
}, [isTV, isLargeTablet, isTablet, ratings]);
|
||||
|
||||
const itemSpacing = useMemo(() => {
|
||||
const baseSpacing = isTV ? 16 : isLargeTablet ? 14 : isTablet ? 12 : 12;
|
||||
const numRatings = ratings ? Object.keys(ratings).length : 0;
|
||||
if (numRatings > 4) {
|
||||
return Math.max(baseSpacing - 2, 8);
|
||||
}
|
||||
return baseSpacing;
|
||||
}, [isTV, isLargeTablet, isTablet, ratings]);
|
||||
|
||||
const iconTextGap = useMemo(() => {
|
||||
const baseGap = isTV ? 6 : isLargeTablet ? 5 : isTablet ? 4 : 4;
|
||||
const numRatings = ratings ? Object.keys(ratings).length : 0;
|
||||
if (numRatings > 4) {
|
||||
return Math.max(baseGap - 1, 2);
|
||||
}
|
||||
return baseGap;
|
||||
}, [isTV, isLargeTablet, isTablet, ratings]);
|
||||
|
||||
useEffect(() => {
|
||||
loadProviderSettings();
|
||||
|
|
|
|||
|
|
@ -15,8 +15,9 @@ import { HeaderVisibility } from '../contexts/HeaderVisibility';
|
|||
import { Stream } from '../types/streams';
|
||||
import { SafeAreaProvider, useSafeAreaInsets } from 'react-native-safe-area-context';
|
||||
import { useTheme } from '../contexts/ThemeContext';
|
||||
import { PostHogProvider } from 'posthog-react-native';
|
||||
import { PostHogProvider, usePostHog } from 'posthog-react-native';
|
||||
import { ScrollToTopProvider, useScrollToTopEmitter } from '../contexts/ScrollToTopContext';
|
||||
import { telemetryService, TELEMETRY_EVENTS } from '../services/telemetryService';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
// Optional iOS Glass effect (expo-glass-effect) with safe fallback
|
||||
|
|
@ -82,6 +83,7 @@ import {
|
|||
AboutSettingsScreen,
|
||||
DeveloperSettingsScreen,
|
||||
LegalScreen,
|
||||
PrivacySettingsScreen,
|
||||
} from '../screens/settings';
|
||||
|
||||
|
||||
|
|
@ -222,6 +224,7 @@ export type RootStackParamList = {
|
|||
PlaybackSettings: undefined;
|
||||
AboutSettings: undefined;
|
||||
DeveloperSettings: undefined;
|
||||
PrivacySettings: undefined;
|
||||
Legal: undefined;
|
||||
};
|
||||
|
||||
|
|
@ -1836,6 +1839,21 @@ const InnerNavigator = ({ initialRouteName }: { initialRouteName?: keyof RootSta
|
|||
},
|
||||
}}
|
||||
/>
|
||||
<Stack.Screen
|
||||
name="PrivacySettings"
|
||||
component={PrivacySettingsScreen}
|
||||
options={{
|
||||
animation: Platform.OS === 'android' ? 'default' : 'slide_from_right',
|
||||
animationDuration: Platform.OS === 'android' ? 250 : 300,
|
||||
presentation: 'card',
|
||||
gestureEnabled: true,
|
||||
gestureDirection: 'horizontal',
|
||||
headerShown: false,
|
||||
contentStyle: {
|
||||
backgroundColor: currentTheme.colors.darkBackground,
|
||||
},
|
||||
}}
|
||||
/>
|
||||
</Stack.Navigator>
|
||||
</View>
|
||||
</PaperProvider>
|
||||
|
|
@ -1843,19 +1861,118 @@ const InnerNavigator = ({ initialRouteName }: { initialRouteName?: keyof RootSta
|
|||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Conditional PostHog Provider Wrapper
|
||||
*
|
||||
* Only initializes PostHog analytics if user has opted in via Privacy Settings.
|
||||
* By default, analytics is disabled for privacy.
|
||||
* Uses PostHog's optIn/optOut API for runtime control.
|
||||
*/
|
||||
const ConditionalPostHogProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
|
||||
const [analyticsEnabled, setAnalyticsEnabled] = useState(false);
|
||||
const [isInitialized, setIsInitialized] = useState(false);
|
||||
const posthogRef = useRef<any>(null);
|
||||
|
||||
useEffect(() => {
|
||||
// Initialize telemetry service and check analytics preference
|
||||
const initializeTelemetry = async () => {
|
||||
try {
|
||||
await telemetryService.initialize();
|
||||
setAnalyticsEnabled(telemetryService.isAnalyticsEnabled());
|
||||
} catch (error) {
|
||||
console.error('Failed to initialize telemetry service:', error);
|
||||
setAnalyticsEnabled(false);
|
||||
} finally {
|
||||
setIsInitialized(true);
|
||||
}
|
||||
};
|
||||
|
||||
initializeTelemetry();
|
||||
|
||||
// Listen for telemetry setting changes
|
||||
const subscription = DeviceEventEmitter.addListener(
|
||||
TELEMETRY_EVENTS.SETTINGS_CHANGED,
|
||||
(settings) => {
|
||||
setAnalyticsEnabled(settings.analyticsEnabled);
|
||||
// If PostHog is available, update its opt-in/out state immediately
|
||||
if (posthogRef.current) {
|
||||
if (settings.analyticsEnabled) {
|
||||
posthogRef.current.optIn();
|
||||
console.log('[Telemetry] PostHog opted in');
|
||||
} else {
|
||||
posthogRef.current.optOut();
|
||||
console.log('[Telemetry] PostHog opted out');
|
||||
}
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
return () => {
|
||||
subscription.remove();
|
||||
};
|
||||
}, []);
|
||||
|
||||
// Wait for initialization before rendering
|
||||
if (!isInitialized) {
|
||||
return <>{children}</>;
|
||||
}
|
||||
|
||||
// Always wrap with PostHogProvider but control via optOut
|
||||
// This allows runtime toggling without remounting the tree
|
||||
return (
|
||||
<PostHogProvider
|
||||
apiKey="phc_sk6THCtV3thEAn6cTaA9kL2cHuKDBnlYiSL40ywdS6C"
|
||||
options={{
|
||||
host: "https://us.i.posthog.com",
|
||||
autocapture: analyticsEnabled,
|
||||
// Start opted out if analytics is disabled
|
||||
defaultOptIn: analyticsEnabled,
|
||||
}}
|
||||
autocapture={analyticsEnabled}
|
||||
>
|
||||
<PostHogOptController
|
||||
enabled={analyticsEnabled}
|
||||
onPostHogReady={(posthog) => { posthogRef.current = posthog; }}
|
||||
/>
|
||||
{children}
|
||||
</PostHogProvider>
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Internal component to handle PostHog opt-in/opt-out
|
||||
* Uses the official usePostHog hook for reliable API access
|
||||
*/
|
||||
const PostHogOptController: React.FC<{
|
||||
enabled: boolean;
|
||||
onPostHogReady: (posthog: any) => void;
|
||||
}> = ({ enabled, onPostHogReady }) => {
|
||||
const posthog = usePostHog();
|
||||
|
||||
useEffect(() => {
|
||||
if (posthog) {
|
||||
onPostHogReady(posthog);
|
||||
if (enabled) {
|
||||
posthog.optIn();
|
||||
console.log('[Telemetry] PostHog opted in');
|
||||
} else {
|
||||
posthog.optOut();
|
||||
console.log('[Telemetry] PostHog opted out');
|
||||
}
|
||||
}
|
||||
}, [enabled, posthog, onPostHogReady]);
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
const AppNavigator = ({ initialRouteName }: { initialRouteName?: keyof RootStackParamList }) => (
|
||||
<PostHogProvider
|
||||
apiKey="phc_sk6THCtV3thEAn6cTaA9kL2cHuKDBnlYiSL40ywdS6C"
|
||||
options={{
|
||||
host: "https://us.i.posthog.com",
|
||||
}}
|
||||
>
|
||||
<ConditionalPostHogProvider>
|
||||
<ScrollToTopProvider>
|
||||
<LoadingProvider>
|
||||
<InnerNavigator initialRouteName={initialRouteName} />
|
||||
</LoadingProvider>
|
||||
</ScrollToTopProvider>
|
||||
</PostHogProvider>
|
||||
</ConditionalPostHogProvider>
|
||||
);
|
||||
|
||||
export default AppNavigator;
|
||||
|
|
|
|||
|
|
@ -44,6 +44,7 @@ import { ContentDiscoverySettingsContent } from './settings/ContentDiscoverySett
|
|||
import { AppearanceSettingsContent } from './settings/AppearanceSettingsScreen';
|
||||
import { IntegrationsSettingsContent } from './settings/IntegrationsSettingsScreen';
|
||||
import { AboutSettingsContent, AboutFooter } from './settings/AboutSettingsScreen';
|
||||
import { PrivacySettingsContent } from './settings/PrivacySettingsScreen';
|
||||
import { SettingsCard, SettingItem, ChevronRight, CustomSwitch } from './settings/SettingsComponents';
|
||||
import { useBottomSheetBackHandler } from '../hooks/useBottomSheetBackHandler';
|
||||
import { LOCALES } from '../constants/locales';
|
||||
|
|
@ -148,6 +149,7 @@ const SettingsScreen: React.FC = () => {
|
|||
{ id: 'playback', title: t('settings.playback'), icon: 'play-circle' },
|
||||
{ id: 'backup', title: t('settings.backup_restore'), icon: 'archive' },
|
||||
{ id: 'updates', title: t('settings.updates'), icon: 'refresh-ccw' },
|
||||
{ id: 'privacy', title: t('privacy.title', 'Privacy & Data'), icon: 'shield' },
|
||||
{ id: 'about', title: t('settings.about'), icon: 'info' },
|
||||
{ id: 'developer', title: t('settings.developer'), icon: 'code' },
|
||||
{ id: 'cache', title: t('settings.cache'), icon: 'database' },
|
||||
|
|
@ -429,6 +431,9 @@ const SettingsScreen: React.FC = () => {
|
|||
case 'about':
|
||||
return <AboutSettingsContent isTablet={isTablet} displayDownloads={displayDownloads} />;
|
||||
|
||||
case 'privacy':
|
||||
return <PrivacySettingsContent isTablet={isTablet} />;
|
||||
|
||||
case 'developer':
|
||||
return (__DEV__ || developerModeEnabled) ? (
|
||||
<SettingsCard title={t('settings.sections.testing')} isTablet={isTablet}>
|
||||
|
|
@ -814,6 +819,13 @@ const SettingsScreen: React.FC = () => {
|
|||
renderControl={() => <ChevronRight />}
|
||||
onPress={() => navigation.navigate('Contributors')}
|
||||
/>
|
||||
<SettingItem
|
||||
title={t('privacy.title', 'Privacy & Data')}
|
||||
description={t('privacy.settings_desc', 'Control telemetry and data collection')}
|
||||
icon="shield"
|
||||
renderControl={() => <ChevronRight />}
|
||||
onPress={() => navigation.navigate('PrivacySettings')}
|
||||
/>
|
||||
<SettingItem
|
||||
title={t('settings.about_nuvio')}
|
||||
description={getDisplayedAppVersion()}
|
||||
|
|
|
|||
463
src/screens/settings/PrivacySettingsScreen.tsx
Normal file
463
src/screens/settings/PrivacySettingsScreen.tsx
Normal file
|
|
@ -0,0 +1,463 @@
|
|||
import React, { useState, useEffect, useCallback } from 'react';
|
||||
import {
|
||||
View,
|
||||
Text,
|
||||
StyleSheet,
|
||||
ScrollView,
|
||||
StatusBar,
|
||||
Dimensions,
|
||||
Linking,
|
||||
Alert,
|
||||
} from 'react-native';
|
||||
import { useNavigation } from '@react-navigation/native';
|
||||
import { NavigationProp } from '@react-navigation/native';
|
||||
import { useSafeAreaInsets } from 'react-native-safe-area-context';
|
||||
import { useTheme } from '../../contexts/ThemeContext';
|
||||
import { RootStackParamList } from '../../navigation/AppNavigator';
|
||||
import ScreenHeader from '../../components/common/ScreenHeader';
|
||||
import { SettingsCard, SettingItem, CustomSwitch, ChevronRight } from './SettingsComponents';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { telemetryService, TelemetrySettings } from '../../services/telemetryService';
|
||||
|
||||
const { width } = Dimensions.get('window');
|
||||
|
||||
interface PrivacySettingsContentProps {
|
||||
isTablet?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Privacy Settings Content Component
|
||||
*
|
||||
* Provides user control over telemetry, analytics, and error reporting.
|
||||
*
|
||||
* Data Collection Summary:
|
||||
* - Analytics (PostHog): Usage patterns, screen views, interactions
|
||||
* - Error Reporting (Sentry): Crash reports and errors for app stability
|
||||
* - Session Replay: Screen recordings when errors occur
|
||||
* - PII: Personal identifiable information (IP, device info, etc.)
|
||||
*/
|
||||
export const PrivacySettingsContent: React.FC<PrivacySettingsContentProps> = ({
|
||||
isTablet = false,
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
const navigation = useNavigation<NavigationProp<RootStackParamList>>();
|
||||
const { currentTheme } = useTheme();
|
||||
|
||||
// Telemetry settings state
|
||||
const [settings, setSettings] = useState<TelemetrySettings>({
|
||||
analyticsEnabled: false,
|
||||
errorReportingEnabled: true,
|
||||
sessionReplayEnabled: false,
|
||||
piiEnabled: false,
|
||||
});
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
|
||||
const showAlert = useCallback((
|
||||
title: string,
|
||||
message: string,
|
||||
actions?: Array<{ label: string; onPress: () => void; style?: object }>
|
||||
) => {
|
||||
const alertActions = (actions || [{ label: 'OK', onPress: () => { } }]).map(action => ({
|
||||
text: action.label,
|
||||
onPress: action.onPress,
|
||||
style: undefined as 'default' | 'cancel' | 'destructive' | undefined,
|
||||
}));
|
||||
|
||||
Alert.alert(title, message, alertActions, { cancelable: true });
|
||||
}, []);
|
||||
|
||||
// Load settings on mount
|
||||
useEffect(() => {
|
||||
const loadSettings = async () => {
|
||||
try {
|
||||
await telemetryService.initialize();
|
||||
setSettings(telemetryService.getSettings());
|
||||
} catch (error) {
|
||||
console.error('Failed to load telemetry settings:', error);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
loadSettings();
|
||||
}, []);
|
||||
|
||||
// Handle analytics toggle
|
||||
const handleAnalyticsToggle = async (enabled: boolean) => {
|
||||
try {
|
||||
await telemetryService.setAnalyticsEnabled(enabled);
|
||||
setSettings(prev => ({ ...prev, analyticsEnabled: enabled }));
|
||||
|
||||
if (enabled) {
|
||||
showAlert(
|
||||
t('privacy.analytics_enabled_title', 'Analytics Enabled'),
|
||||
t('privacy.analytics_enabled_message', 'Usage data will be collected to help improve the app. You can disable this at any time.')
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to update analytics setting:', error);
|
||||
}
|
||||
};
|
||||
|
||||
// Handle error reporting toggle
|
||||
const handleErrorReportingToggle = async (enabled: boolean) => {
|
||||
if (!enabled) {
|
||||
showAlert(
|
||||
t('privacy.disable_error_reporting_title', 'Disable Error Reporting?'),
|
||||
t('privacy.disable_error_reporting_message', 'Disabling error reporting means we won\'t be notified of crashes or issues you experience. This may affect our ability to fix bugs.'),
|
||||
[
|
||||
{ label: t('common.cancel', 'Cancel'), onPress: () => { } },
|
||||
{
|
||||
label: t('common.disable', 'Disable'),
|
||||
onPress: async () => {
|
||||
await telemetryService.setErrorReportingEnabled(false);
|
||||
setSettings(prev => ({ ...prev, errorReportingEnabled: false }));
|
||||
}
|
||||
}
|
||||
]
|
||||
);
|
||||
} else {
|
||||
await telemetryService.setErrorReportingEnabled(true);
|
||||
setSettings(prev => ({ ...prev, errorReportingEnabled: true }));
|
||||
}
|
||||
};
|
||||
|
||||
// Handle session replay toggle
|
||||
const handleSessionReplayToggle = async (enabled: boolean) => {
|
||||
if (enabled) {
|
||||
showAlert(
|
||||
t('privacy.enable_session_replay_title', 'Enable Session Replay?'),
|
||||
t('privacy.enable_session_replay_message', 'Session replay records your screen when errors occur to help us understand what happened. This may capture visible content on your screen.'),
|
||||
[
|
||||
{ label: t('common.cancel', 'Cancel'), onPress: () => { } },
|
||||
{
|
||||
label: t('common.enable', 'Enable'),
|
||||
onPress: async () => {
|
||||
await telemetryService.setSessionReplayEnabled(true);
|
||||
setSettings(prev => ({ ...prev, sessionReplayEnabled: true }));
|
||||
}
|
||||
}
|
||||
]
|
||||
);
|
||||
} else {
|
||||
await telemetryService.setSessionReplayEnabled(false);
|
||||
setSettings(prev => ({ ...prev, sessionReplayEnabled: false }));
|
||||
}
|
||||
};
|
||||
|
||||
// Handle PII toggle
|
||||
const handlePiiToggle = async (enabled: boolean) => {
|
||||
if (enabled) {
|
||||
showAlert(
|
||||
t('privacy.enable_pii_title', 'Enable PII Collection?'),
|
||||
t('privacy.enable_pii_message', 'This allows collection of personally identifiable information like IP address and device details. This data helps diagnose issues but increases privacy exposure.'),
|
||||
[
|
||||
{ label: t('common.cancel', 'Cancel'), onPress: () => { } },
|
||||
{
|
||||
label: t('common.enable', 'Enable'),
|
||||
onPress: async () => {
|
||||
await telemetryService.setPiiEnabled(true);
|
||||
setSettings(prev => ({ ...prev, piiEnabled: true }));
|
||||
}
|
||||
}
|
||||
]
|
||||
);
|
||||
} else {
|
||||
await telemetryService.setPiiEnabled(false);
|
||||
setSettings(prev => ({ ...prev, piiEnabled: false }));
|
||||
}
|
||||
};
|
||||
|
||||
// Disable all telemetry
|
||||
const handleDisableAll = () => {
|
||||
showAlert(
|
||||
t('privacy.disable_all_title', 'Disable All Telemetry?'),
|
||||
t('privacy.disable_all_message', 'This will disable all analytics, error reporting, and session replay. We won\'t receive any data about app usage or crashes.'),
|
||||
[
|
||||
{ label: t('common.cancel', 'Cancel'), onPress: () => { } },
|
||||
{
|
||||
label: t('privacy.disable_all_button', 'Disable All'),
|
||||
onPress: async () => {
|
||||
await telemetryService.disableAllTelemetry();
|
||||
setSettings({
|
||||
analyticsEnabled: false,
|
||||
errorReportingEnabled: false,
|
||||
sessionReplayEnabled: false,
|
||||
piiEnabled: false,
|
||||
});
|
||||
// Delay showing the next alert to avoid Reanimated conflicts
|
||||
setTimeout(() => {
|
||||
showAlert(
|
||||
t('privacy.all_disabled_title', 'All Telemetry Disabled'),
|
||||
t('privacy.all_disabled_message', 'All data collection has been disabled. Changes take effect on next app restart.')
|
||||
);
|
||||
}, 300);
|
||||
}
|
||||
}
|
||||
]
|
||||
);
|
||||
};
|
||||
|
||||
// Reset to recommended defaults
|
||||
const handleResetToRecommended = async () => {
|
||||
await telemetryService.enableRecommendedTelemetry();
|
||||
setSettings({
|
||||
analyticsEnabled: false,
|
||||
errorReportingEnabled: true,
|
||||
sessionReplayEnabled: false,
|
||||
piiEnabled: false,
|
||||
});
|
||||
// No chained alert here, this is direct so it's fine
|
||||
showAlert(
|
||||
t('privacy.reset_title', 'Reset to Recommended'),
|
||||
t('privacy.reset_message', 'Privacy settings have been reset to recommended defaults (error reporting enabled, analytics disabled).')
|
||||
);
|
||||
};
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<View style={styles.loadingContainer}>
|
||||
<Text style={[styles.loadingText, { color: currentTheme.colors.mediumEmphasis }]}>
|
||||
{t('common.loading', 'Loading...')}
|
||||
</Text>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{/* Info Card */}
|
||||
<View style={[styles.infoCard, { backgroundColor: currentTheme.colors.elevation1, borderColor: currentTheme.colors.elevation2 }]}>
|
||||
<Text style={[styles.infoTitle, { color: currentTheme.colors.highEmphasis }]}>
|
||||
{t('privacy.info_title', 'Your Privacy Matters')}
|
||||
</Text>
|
||||
<Text style={[styles.infoText, { color: currentTheme.colors.mediumEmphasis }]}>
|
||||
{t('privacy.info_description', 'Control what data is collected and shared. All settings take effect on the next app restart. By default, only anonymous error reporting is enabled to help us fix crashes.')}
|
||||
</Text>
|
||||
</View>
|
||||
|
||||
{/* Analytics Section */}
|
||||
<SettingsCard title={t('privacy.section_analytics', 'ANALYTICS')} isTablet={isTablet}>
|
||||
<SettingItem
|
||||
title={t('privacy.analytics_title', 'Usage Analytics')}
|
||||
description={t('privacy.analytics_description', 'Collect anonymous usage patterns and screen views')}
|
||||
icon="bar-chart-2"
|
||||
renderControl={() => (
|
||||
<CustomSwitch
|
||||
value={settings.analyticsEnabled}
|
||||
onValueChange={handleAnalyticsToggle}
|
||||
/>
|
||||
)}
|
||||
isLast
|
||||
isTablet={isTablet}
|
||||
/>
|
||||
</SettingsCard>
|
||||
|
||||
{/* Error Reporting Section */}
|
||||
<SettingsCard title={t('privacy.section_error_reporting', 'ERROR REPORTING')} isTablet={isTablet}>
|
||||
<SettingItem
|
||||
title={t('privacy.error_reporting_title', 'Crash Reports')}
|
||||
description={t('privacy.error_reporting_description', 'Send anonymous crash reports to improve stability')}
|
||||
icon="alert-circle"
|
||||
renderControl={() => (
|
||||
<CustomSwitch
|
||||
value={settings.errorReportingEnabled}
|
||||
onValueChange={handleErrorReportingToggle}
|
||||
/>
|
||||
)}
|
||||
isTablet={isTablet}
|
||||
/>
|
||||
<SettingItem
|
||||
title={t('privacy.session_replay_title', 'Session Replay')}
|
||||
description={t('privacy.session_replay_description', 'Record screen when errors occur')}
|
||||
icon="video"
|
||||
renderControl={() => (
|
||||
<CustomSwitch
|
||||
value={settings.sessionReplayEnabled}
|
||||
onValueChange={handleSessionReplayToggle}
|
||||
/>
|
||||
)}
|
||||
isTablet={isTablet}
|
||||
/>
|
||||
<SettingItem
|
||||
title={t('privacy.pii_title', 'Include Device Info')}
|
||||
description={t('privacy.pii_description', 'Send IP address and device details with reports')}
|
||||
icon="user"
|
||||
renderControl={() => (
|
||||
<CustomSwitch
|
||||
value={settings.piiEnabled}
|
||||
onValueChange={handlePiiToggle}
|
||||
/>
|
||||
)}
|
||||
isLast
|
||||
isTablet={isTablet}
|
||||
/>
|
||||
</SettingsCard>
|
||||
|
||||
{/* Quick Actions */}
|
||||
<SettingsCard title={t('privacy.section_quick_actions', 'QUICK ACTIONS')} isTablet={isTablet}>
|
||||
<SettingItem
|
||||
title={t('privacy.disable_all', 'Disable All Telemetry')}
|
||||
description={t('privacy.disable_all_desc', 'Turn off all data collection')}
|
||||
icon="shield-off"
|
||||
onPress={handleDisableAll}
|
||||
renderControl={() => <ChevronRight />}
|
||||
isTablet={isTablet}
|
||||
/>
|
||||
<SettingItem
|
||||
title={t('privacy.reset_recommended', 'Reset to Recommended')}
|
||||
description={t('privacy.reset_recommended_desc', 'Privacy-first defaults with error reporting')}
|
||||
icon="refresh-cw"
|
||||
onPress={handleResetToRecommended}
|
||||
renderControl={() => <ChevronRight />}
|
||||
isLast
|
||||
isTablet={isTablet}
|
||||
/>
|
||||
</SettingsCard>
|
||||
|
||||
{/* Learn More */}
|
||||
<SettingsCard title={t('privacy.section_learn_more', 'LEARN MORE')} isTablet={isTablet}>
|
||||
<SettingItem
|
||||
title={t('privacy.privacy_policy', 'Privacy Policy')}
|
||||
icon="file-text"
|
||||
onPress={() => Linking.openURL('https://tapframe.github.io/NuvioStreaming/#privacy-policy')}
|
||||
renderControl={() => <ChevronRight />}
|
||||
isTablet={isTablet}
|
||||
/>
|
||||
</SettingsCard>
|
||||
|
||||
{/* Data Summary */}
|
||||
<View style={[styles.summaryCard, { backgroundColor: currentTheme.colors.elevation1, borderColor: currentTheme.colors.elevation2 }]}>
|
||||
<Text style={[styles.summaryTitle, { color: currentTheme.colors.highEmphasis }]}>
|
||||
{t('privacy.current_settings', 'Current Settings Summary')}
|
||||
</Text>
|
||||
<View style={styles.summaryRow}>
|
||||
<View style={[styles.statusDot, { backgroundColor: settings.analyticsEnabled ? '#4CAF50' : '#9E9E9E' }]} />
|
||||
<Text style={[styles.summaryText, { color: currentTheme.colors.mediumEmphasis }]}>
|
||||
{t('privacy.summary_analytics', 'Analytics')}: {settings.analyticsEnabled ? t('common.on', 'On') : t('common.off', 'Off')}
|
||||
</Text>
|
||||
</View>
|
||||
<View style={styles.summaryRow}>
|
||||
<View style={[styles.statusDot, { backgroundColor: settings.errorReportingEnabled ? '#4CAF50' : '#9E9E9E' }]} />
|
||||
<Text style={[styles.summaryText, { color: currentTheme.colors.mediumEmphasis }]}>
|
||||
{t('privacy.summary_errors', 'Error Reports')}: {settings.errorReportingEnabled ? t('common.on', 'On') : t('common.off', 'Off')}
|
||||
</Text>
|
||||
</View>
|
||||
<View style={styles.summaryRow}>
|
||||
<View style={[styles.statusDot, { backgroundColor: settings.sessionReplayEnabled ? '#FF9800' : '#9E9E9E' }]} />
|
||||
<Text style={[styles.summaryText, { color: currentTheme.colors.mediumEmphasis }]}>
|
||||
{t('privacy.summary_replay', 'Session Replay')}: {settings.sessionReplayEnabled ? t('common.on', 'On') : t('common.off', 'Off')}
|
||||
</Text>
|
||||
</View>
|
||||
<View style={styles.summaryRow}>
|
||||
<View style={[styles.statusDot, { backgroundColor: settings.piiEnabled ? '#FF9800' : '#9E9E9E' }]} />
|
||||
<Text style={[styles.summaryText, { color: currentTheme.colors.mediumEmphasis }]}>
|
||||
{t('privacy.summary_pii', 'Device Info')}: {settings.piiEnabled ? t('common.on', 'On') : t('common.off', 'Off')}
|
||||
</Text>
|
||||
</View>
|
||||
<Text style={[styles.restartNote, { color: currentTheme.colors.mediumEmphasis }]}>
|
||||
{t('privacy.restart_note_detailed', '* Analytics and error reporting changes take effect immediately. Session replay and PII settings require app restart.')}
|
||||
</Text>
|
||||
</View>
|
||||
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* PrivacySettingsScreen - Wrapper for mobile navigation
|
||||
*/
|
||||
const PrivacySettingsScreen: React.FC = () => {
|
||||
const navigation = useNavigation<NavigationProp<RootStackParamList>>();
|
||||
const { currentTheme } = useTheme();
|
||||
const { t } = useTranslation();
|
||||
const insets = useSafeAreaInsets();
|
||||
const screenIsTablet = width >= 768;
|
||||
|
||||
return (
|
||||
<View style={[styles.container, { backgroundColor: currentTheme.colors.darkBackground }]}>
|
||||
<StatusBar barStyle="light-content" />
|
||||
<ScreenHeader
|
||||
title={t('privacy.title', 'Privacy & Data')}
|
||||
showBackButton
|
||||
onBackPress={() => navigation.goBack()}
|
||||
/>
|
||||
|
||||
<ScrollView
|
||||
style={styles.scrollView}
|
||||
showsVerticalScrollIndicator={false}
|
||||
contentContainerStyle={[styles.scrollContent, { paddingBottom: insets.bottom + 40 }]}
|
||||
>
|
||||
<PrivacySettingsContent isTablet={screenIsTablet} />
|
||||
</ScrollView>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
flex: 1,
|
||||
},
|
||||
scrollView: {
|
||||
flex: 1,
|
||||
},
|
||||
scrollContent: {
|
||||
paddingTop: 16,
|
||||
},
|
||||
loadingContainer: {
|
||||
padding: 40,
|
||||
alignItems: 'center',
|
||||
},
|
||||
loadingText: {
|
||||
fontSize: 16,
|
||||
},
|
||||
infoCard: {
|
||||
marginHorizontal: 16,
|
||||
marginBottom: 20,
|
||||
padding: 16,
|
||||
borderRadius: 16,
|
||||
borderWidth: 1,
|
||||
},
|
||||
infoTitle: {
|
||||
fontSize: 17,
|
||||
fontWeight: '600',
|
||||
marginBottom: 8,
|
||||
},
|
||||
infoText: {
|
||||
fontSize: 14,
|
||||
lineHeight: 20,
|
||||
},
|
||||
summaryCard: {
|
||||
marginHorizontal: 16,
|
||||
marginTop: 8,
|
||||
marginBottom: 20,
|
||||
padding: 16,
|
||||
borderRadius: 16,
|
||||
borderWidth: 1,
|
||||
},
|
||||
summaryTitle: {
|
||||
fontSize: 15,
|
||||
fontWeight: '600',
|
||||
marginBottom: 12,
|
||||
},
|
||||
summaryRow: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
marginBottom: 8,
|
||||
},
|
||||
statusDot: {
|
||||
width: 8,
|
||||
height: 8,
|
||||
borderRadius: 4,
|
||||
marginRight: 10,
|
||||
},
|
||||
summaryText: {
|
||||
fontSize: 14,
|
||||
},
|
||||
restartNote: {
|
||||
fontSize: 12,
|
||||
fontStyle: 'italic',
|
||||
marginTop: 8,
|
||||
},
|
||||
});
|
||||
|
||||
export default PrivacySettingsScreen;
|
||||
|
|
@ -6,6 +6,7 @@ export { default as PlaybackSettingsScreen } from './PlaybackSettingsScreen';
|
|||
export { default as AboutSettingsScreen } from './AboutSettingsScreen';
|
||||
export { default as DeveloperSettingsScreen } from './DeveloperSettingsScreen';
|
||||
export { default as LegalScreen } from './LegalScreen';
|
||||
export { default as PrivacySettingsScreen } from './PrivacySettingsScreen';
|
||||
|
||||
// Reusable content component exports (for inline use on tablets)
|
||||
export { ContentDiscoverySettingsContent } from './ContentDiscoverySettingsScreen';
|
||||
|
|
@ -13,6 +14,7 @@ export { AppearanceSettingsContent } from './AppearanceSettingsScreen';
|
|||
export { IntegrationsSettingsContent } from './IntegrationsSettingsScreen';
|
||||
export { PlaybackSettingsContent } from './PlaybackSettingsScreen';
|
||||
export { AboutSettingsContent, AboutFooter } from './AboutSettingsScreen';
|
||||
export { PrivacySettingsContent } from './PrivacySettingsScreen';
|
||||
|
||||
// Shared UI component exports
|
||||
export { SettingsCard, SettingItem, CustomSwitch, ChevronRight } from './SettingsComponents';
|
||||
|
|
|
|||
326
src/services/telemetryService.ts
Normal file
326
src/services/telemetryService.ts
Normal file
|
|
@ -0,0 +1,326 @@
|
|||
/**
|
||||
* Telemetry Service
|
||||
*
|
||||
* Manages user preferences for telemetry, analytics, and error reporting.
|
||||
* Provides a central opt-out mechanism for privacy-conscious users.
|
||||
*
|
||||
* Data Collection Overview:
|
||||
* - Analytics (PostHog): Page views, interactions, session data, device metadata
|
||||
* - Error Reporting (Sentry): Crash reports, errors, breadcrumbs, device info
|
||||
* - Session Replay: Screen recordings on errors (disabled by default)
|
||||
*
|
||||
* Privacy-First Defaults:
|
||||
* - Analytics: Disabled by default
|
||||
* - Error Reporting: Enabled (no PII) by default for stability
|
||||
* - Session Replay: Disabled by default
|
||||
* - PII Collection: Disabled by default
|
||||
*/
|
||||
|
||||
import { mmkvStorage } from './mmkvStorage';
|
||||
import { DeviceEventEmitter } from 'react-native';
|
||||
import { createMMKV } from 'react-native-mmkv';
|
||||
|
||||
// Direct MMKV access for synchronous reads (needed for Sentry beforeSend)
|
||||
const directMMKV = createMMKV();
|
||||
|
||||
// Storage keys for telemetry preferences
|
||||
const TELEMETRY_KEYS = {
|
||||
ANALYTICS_ENABLED: 'telemetry_analytics_enabled',
|
||||
ERROR_REPORTING_ENABLED: 'telemetry_error_reporting_enabled',
|
||||
SESSION_REPLAY_ENABLED: 'telemetry_session_replay_enabled',
|
||||
PII_ENABLED: 'telemetry_pii_enabled',
|
||||
TELEMETRY_INITIALIZED: 'telemetry_initialized',
|
||||
} as const;
|
||||
|
||||
// Event names for telemetry changes
|
||||
export const TELEMETRY_EVENTS = {
|
||||
SETTINGS_CHANGED: 'telemetry_settings_changed',
|
||||
} as const;
|
||||
|
||||
export interface TelemetrySettings {
|
||||
analyticsEnabled: boolean;
|
||||
errorReportingEnabled: boolean;
|
||||
sessionReplayEnabled: boolean;
|
||||
piiEnabled: boolean;
|
||||
}
|
||||
|
||||
// Default settings - Privacy-first approach
|
||||
const DEFAULT_SETTINGS: TelemetrySettings = {
|
||||
analyticsEnabled: false, // Disabled by default - user must opt-in
|
||||
errorReportingEnabled: true, // Enabled for app stability, but without PII
|
||||
sessionReplayEnabled: false, // Disabled by default - high privacy impact
|
||||
piiEnabled: false, // Never send PII by default
|
||||
};
|
||||
|
||||
/**
|
||||
* Synchronously read a setting directly from MMKV
|
||||
* Used by Sentry's beforeSend hook which runs synchronously
|
||||
*/
|
||||
function readSettingSync(key: string): string | undefined {
|
||||
try {
|
||||
return directMMKV.getString(key);
|
||||
} catch {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if error reporting is enabled (synchronous version for Sentry)
|
||||
* This is called from Sentry's beforeSend hook
|
||||
*/
|
||||
export function isErrorReportingEnabledSync(): boolean {
|
||||
const value = readSettingSync(TELEMETRY_KEYS.ERROR_REPORTING_ENABLED);
|
||||
// Default to true if not set (privacy-safe default for stability)
|
||||
return value !== 'false';
|
||||
}
|
||||
|
||||
class TelemetryService {
|
||||
private static instance: TelemetryService;
|
||||
private settings: TelemetrySettings = { ...DEFAULT_SETTINGS };
|
||||
private initialized = false;
|
||||
|
||||
private constructor() {
|
||||
// Synchronously load settings on construction for immediate availability
|
||||
this.loadSettingsSync();
|
||||
}
|
||||
|
||||
public static getInstance(): TelemetryService {
|
||||
if (!TelemetryService.instance) {
|
||||
TelemetryService.instance = new TelemetryService();
|
||||
}
|
||||
return TelemetryService.instance;
|
||||
}
|
||||
|
||||
/**
|
||||
* Synchronously load settings from MMKV (for immediate availability)
|
||||
*/
|
||||
private loadSettingsSync(): void {
|
||||
try {
|
||||
const analytics = readSettingSync(TELEMETRY_KEYS.ANALYTICS_ENABLED);
|
||||
const errorReporting = readSettingSync(TELEMETRY_KEYS.ERROR_REPORTING_ENABLED);
|
||||
const sessionReplay = readSettingSync(TELEMETRY_KEYS.SESSION_REPLAY_ENABLED);
|
||||
const pii = readSettingSync(TELEMETRY_KEYS.PII_ENABLED);
|
||||
|
||||
this.settings = {
|
||||
analyticsEnabled: analytics === 'true',
|
||||
errorReportingEnabled: errorReporting !== 'false', // Default true
|
||||
sessionReplayEnabled: sessionReplay === 'true',
|
||||
piiEnabled: pii === 'true',
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('[TelemetryService] Error loading settings sync:', error);
|
||||
this.settings = { ...DEFAULT_SETTINGS };
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize telemetry service and load saved preferences
|
||||
*/
|
||||
async initialize(): Promise<TelemetrySettings> {
|
||||
if (this.initialized) {
|
||||
return this.settings;
|
||||
}
|
||||
|
||||
try {
|
||||
// Check if this is first run (no telemetry preferences saved yet)
|
||||
const telemetryInitialized = await mmkvStorage.getItem(TELEMETRY_KEYS.TELEMETRY_INITIALIZED);
|
||||
|
||||
if (telemetryInitialized !== 'true') {
|
||||
// First run - use defaults and mark as initialized
|
||||
await this.saveSettings(DEFAULT_SETTINGS);
|
||||
await mmkvStorage.setItem(TELEMETRY_KEYS.TELEMETRY_INITIALIZED, 'true');
|
||||
this.settings = { ...DEFAULT_SETTINGS };
|
||||
} else {
|
||||
// Load saved preferences
|
||||
const [analytics, errorReporting, sessionReplay, pii] = await Promise.all([
|
||||
mmkvStorage.getItem(TELEMETRY_KEYS.ANALYTICS_ENABLED),
|
||||
mmkvStorage.getItem(TELEMETRY_KEYS.ERROR_REPORTING_ENABLED),
|
||||
mmkvStorage.getItem(TELEMETRY_KEYS.SESSION_REPLAY_ENABLED),
|
||||
mmkvStorage.getItem(TELEMETRY_KEYS.PII_ENABLED),
|
||||
]);
|
||||
|
||||
this.settings = {
|
||||
analyticsEnabled: analytics === 'true',
|
||||
errorReportingEnabled: errorReporting !== 'false', // Default true if not explicitly disabled
|
||||
sessionReplayEnabled: sessionReplay === 'true',
|
||||
piiEnabled: pii === 'true',
|
||||
};
|
||||
}
|
||||
|
||||
this.initialized = true;
|
||||
console.log('[TelemetryService] Initialized with settings:', this.settings);
|
||||
} catch (error) {
|
||||
console.error('[TelemetryService] Error initializing:', error);
|
||||
// Use defaults on error
|
||||
this.settings = { ...DEFAULT_SETTINGS };
|
||||
this.initialized = true;
|
||||
}
|
||||
|
||||
return this.settings;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get current telemetry settings
|
||||
*/
|
||||
getSettings(): TelemetrySettings {
|
||||
return { ...this.settings };
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if analytics is enabled
|
||||
*/
|
||||
isAnalyticsEnabled(): boolean {
|
||||
return this.settings.analyticsEnabled;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if error reporting is enabled
|
||||
*/
|
||||
isErrorReportingEnabled(): boolean {
|
||||
return this.settings.errorReportingEnabled;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if session replay is enabled
|
||||
*/
|
||||
isSessionReplayEnabled(): boolean {
|
||||
return this.settings.sessionReplayEnabled;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if PII collection is enabled
|
||||
*/
|
||||
isPiiEnabled(): boolean {
|
||||
return this.settings.piiEnabled;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update analytics setting
|
||||
*/
|
||||
async setAnalyticsEnabled(enabled: boolean): Promise<void> {
|
||||
this.settings.analyticsEnabled = enabled;
|
||||
await mmkvStorage.setItem(TELEMETRY_KEYS.ANALYTICS_ENABLED, enabled.toString());
|
||||
this.emitSettingsChanged();
|
||||
console.log('[TelemetryService] Analytics enabled:', enabled);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update error reporting setting
|
||||
*/
|
||||
async setErrorReportingEnabled(enabled: boolean): Promise<void> {
|
||||
this.settings.errorReportingEnabled = enabled;
|
||||
await mmkvStorage.setItem(TELEMETRY_KEYS.ERROR_REPORTING_ENABLED, enabled.toString());
|
||||
this.emitSettingsChanged();
|
||||
console.log('[TelemetryService] Error reporting enabled:', enabled);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update session replay setting
|
||||
*/
|
||||
async setSessionReplayEnabled(enabled: boolean): Promise<void> {
|
||||
this.settings.sessionReplayEnabled = enabled;
|
||||
await mmkvStorage.setItem(TELEMETRY_KEYS.SESSION_REPLAY_ENABLED, enabled.toString());
|
||||
this.emitSettingsChanged();
|
||||
console.log('[TelemetryService] Session replay enabled:', enabled);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update PII collection setting
|
||||
*/
|
||||
async setPiiEnabled(enabled: boolean): Promise<void> {
|
||||
this.settings.piiEnabled = enabled;
|
||||
await mmkvStorage.setItem(TELEMETRY_KEYS.PII_ENABLED, enabled.toString());
|
||||
this.emitSettingsChanged();
|
||||
console.log('[TelemetryService] PII enabled:', enabled);
|
||||
}
|
||||
|
||||
/**
|
||||
* Disable all telemetry (global opt-out)
|
||||
*/
|
||||
async disableAllTelemetry(): Promise<void> {
|
||||
this.settings = {
|
||||
analyticsEnabled: false,
|
||||
errorReportingEnabled: false,
|
||||
sessionReplayEnabled: false,
|
||||
piiEnabled: false,
|
||||
};
|
||||
await this.saveSettings(this.settings);
|
||||
this.emitSettingsChanged();
|
||||
console.log('[TelemetryService] All telemetry disabled');
|
||||
}
|
||||
|
||||
/**
|
||||
* Enable recommended telemetry (error reporting only, no PII)
|
||||
*/
|
||||
async enableRecommendedTelemetry(): Promise<void> {
|
||||
this.settings = {
|
||||
analyticsEnabled: false,
|
||||
errorReportingEnabled: true,
|
||||
sessionReplayEnabled: false,
|
||||
piiEnabled: false,
|
||||
};
|
||||
await this.saveSettings(this.settings);
|
||||
this.emitSettingsChanged();
|
||||
console.log('[TelemetryService] Recommended telemetry enabled');
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset to default settings
|
||||
*/
|
||||
async resetToDefaults(): Promise<void> {
|
||||
this.settings = { ...DEFAULT_SETTINGS };
|
||||
await this.saveSettings(this.settings);
|
||||
this.emitSettingsChanged();
|
||||
console.log('[TelemetryService] Reset to defaults');
|
||||
}
|
||||
|
||||
/**
|
||||
* Save all settings to storage
|
||||
*/
|
||||
private async saveSettings(settings: TelemetrySettings): Promise<void> {
|
||||
await Promise.all([
|
||||
mmkvStorage.setItem(TELEMETRY_KEYS.ANALYTICS_ENABLED, settings.analyticsEnabled.toString()),
|
||||
mmkvStorage.setItem(TELEMETRY_KEYS.ERROR_REPORTING_ENABLED, settings.errorReportingEnabled.toString()),
|
||||
mmkvStorage.setItem(TELEMETRY_KEYS.SESSION_REPLAY_ENABLED, settings.sessionReplayEnabled.toString()),
|
||||
mmkvStorage.setItem(TELEMETRY_KEYS.PII_ENABLED, settings.piiEnabled.toString()),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Emit event when settings change
|
||||
*/
|
||||
private emitSettingsChanged(): void {
|
||||
DeviceEventEmitter.emit(TELEMETRY_EVENTS.SETTINGS_CHANGED, this.settings);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get Sentry configuration based on current settings
|
||||
*/
|
||||
getSentryConfig(): {
|
||||
enabled: boolean;
|
||||
sendDefaultPii: boolean;
|
||||
replaysSessionSampleRate: number;
|
||||
replaysOnErrorSampleRate: number;
|
||||
} {
|
||||
return {
|
||||
enabled: this.settings.errorReportingEnabled,
|
||||
sendDefaultPii: this.settings.piiEnabled,
|
||||
replaysSessionSampleRate: this.settings.sessionReplayEnabled ? 0.1 : 0,
|
||||
replaysOnErrorSampleRate: this.settings.sessionReplayEnabled ? 1.0 : 0,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Get PostHog configuration based on current settings
|
||||
*/
|
||||
getPostHogConfig(): {
|
||||
enabled: boolean;
|
||||
} {
|
||||
return {
|
||||
enabled: this.settings.analyticsEnabled,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export const telemetryService = TelemetryService.getInstance();
|
||||
export default telemetryService;
|
||||
Loading…
Reference in a new issue