Mobile Analytics and some changes (#667)

* update contributing & environment-setup

* fix pods deployment target to 13.0

* Track app screen changes with plausible

* Don't track onboarding

* remove platformType from usePlausibleEvent

* submit custom plausible events
This commit is contained in:
Utku 2023-04-04 08:41:09 +03:00 committed by GitHub
parent 86c47dde4a
commit 63afacbc54
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
24 changed files with 157 additions and 85 deletions

View file

@ -61,7 +61,7 @@ To run the landing page:
If you are having issues ensure you are using the following versions of Rust and Node:
- Rust version: **1.67.0**
- Rust version: **1.68.2**
- Node version: **18**
Be sure to read the [guidelines](https://spacedrive.com/docs/developers/prerequisites/guidelines) to make sure your code is a similar style to ours.

View file

@ -74,6 +74,8 @@ target 'Spacedrive' do
target_installation_result.resource_bundle_targets.each do |resource_bundle_target|
resource_bundle_target.build_configurations.each do |config|
config.build_settings['CODE_SIGNING_ALLOWED'] = 'NO'
# Spacedrive - Added to fix the issue with the deployment target
config.build_settings['IPHONEOS_DEPLOYMENT_TARGET'] = podfile_properties['ios.deploymentTarget'] || '13.0'
end
end
end

View file

@ -1,5 +1,10 @@
import { BottomSheetModalProvider } from '@gorhom/bottom-sheet';
import { DefaultTheme, NavigationContainer, Theme } from '@react-navigation/native';
import {
DefaultTheme,
NavigationContainer,
Theme,
useNavigationContainerRef
} from '@react-navigation/native';
import { loggerLink } from '@rspc/client';
import { QueryClient } from '@tanstack/react-query';
import dayjs from 'dayjs';
@ -8,19 +13,21 @@ import duration from 'dayjs/plugin/duration';
import relativeTime from 'dayjs/plugin/relativeTime';
import * as SplashScreen from 'expo-splash-screen';
import { StatusBar } from 'expo-status-bar';
import { useEffect } from 'react';
import { useEffect, useRef, useState } from 'react';
import { GestureHandlerRootView } from 'react-native-gesture-handler';
import { MenuProvider } from 'react-native-popup-menu';
import { SafeAreaProvider } from 'react-native-safe-area-context';
import { useDeviceContext } from 'twrnc';
import { proxy, useSnapshot } from 'valtio';
import { useSnapshot } from 'valtio';
import {
ClientContextProvider,
LibraryContextProvider,
getDebugState,
initPlausible,
rspc,
useClientContext,
useInvalidateQuery
useInvalidateQuery,
usePlausiblePageViewMonitor
} from '@sd/client';
import { GlobalModals } from './components/modal/GlobalModals';
import { reactNativeLink } from './lib/rspcReactNativeTransport';
@ -42,14 +49,43 @@ const NavigatorTheme: Theme = {
}
};
initPlausible({ platformType: 'mobile' });
function AppNavigation() {
const { library } = useClientContext();
// TODO: Make sure library has actually been loaded by this point - precache with useCachedLibraries?
// if (library === undefined) throw new Error("Tried to render AppNavigation before libraries fetched!")
const navRef = useNavigationContainerRef();
const routeNameRef = useRef<string>();
const [currentPath, setCurrentPath] = useState<string>('/');
usePlausiblePageViewMonitor({ currentPath });
return (
<NavigationContainer theme={NavigatorTheme}>
<NavigationContainer
ref={navRef}
onReady={() => {
routeNameRef.current = navRef.getCurrentRoute()?.name;
}}
theme={NavigatorTheme}
onStateChange={async () => {
const previousRouteName = routeNameRef.current;
const currentRouteName = navRef.getCurrentRoute()?.name;
if (previousRouteName !== currentRouteName) {
// Save the current route name for later comparison
routeNameRef.current = currentRouteName;
// Don't track onboarding screens
if (navRef.getRootState().routeNames.includes('GetStarted')) {
return;
}
console.log(`Navigated from ${previousRouteName} to ${currentRouteName}`);
currentRouteName && setCurrentPath(currentRouteName);
}
}}
>
{!library ? (
<OnboardingNavigator />
) : (

View file

@ -1,6 +1,6 @@
import { useQueryClient } from '@tanstack/react-query';
import { useState } from 'react';
import { useBridgeMutation } from '@sd/client';
import { useBridgeMutation, usePlausibleEvent } from '@sd/client';
import { Input } from '~/components/form/Input';
import Dialog from '~/components/layout/Dialog';
import { currentLibraryStore } from '~/utils/nav';
@ -17,6 +17,8 @@ const CreateLibraryDialog = ({ children, onSubmit, disableBackdropClose }: Props
const [libName, setLibName] = useState('');
const [isOpen, setIsOpen] = useState(false);
const submitPlausibleEvent = usePlausibleEvent();
const { mutate: createLibrary, isLoading: createLibLoading } = useBridgeMutation(
'library.create',
{
@ -30,6 +32,8 @@ const CreateLibraryDialog = ({ children, onSubmit, disableBackdropClose }: Props
// Switch to the new library
currentLibraryStore.id = lib.uuid;
submitPlausibleEvent({ event: { type: 'libraryCreate' } });
onSubmit?.();
},
onSettled: () => {

View file

@ -1,6 +1,6 @@
import { useQueryClient } from '@tanstack/react-query';
import { useRef } from 'react';
import { useBridgeMutation } from '@sd/client';
import { useBridgeMutation, usePlausibleEvent } from '@sd/client';
import { ConfirmModal, ModalRef } from '~/components/layout/Modal';
type Props = {
@ -13,12 +13,19 @@ const DeleteLibraryModal = ({ trigger, onSubmit, libraryUuid }: Props) => {
const queryClient = useQueryClient();
const modalRef = useRef<ModalRef>(null);
const submitPlausibleEvent = usePlausibleEvent();
const { mutate: deleteLibrary, isLoading: deleteLibLoading } = useBridgeMutation(
'library.delete',
{
onSuccess: () => {
queryClient.invalidateQueries(['library.list']);
onSubmit?.();
submitPlausibleEvent({
event: {
type: 'libraryDelete'
}
});
},
onSettled: () => {
modalRef.current?.close();

View file

@ -1,5 +1,5 @@
import { useRef } from 'react';
import { useLibraryMutation } from '@sd/client';
import { useLibraryMutation, usePlausibleEvent } from '@sd/client';
import { ConfirmModal, ModalRef } from '~/components/layout/Modal';
type Props = {
@ -11,10 +11,13 @@ type Props = {
const DeleteLocationModal = ({ trigger, onSubmit, locationId }: Props) => {
const modalRef = useRef<ModalRef>(null);
const submitPlausibleEvent = usePlausibleEvent();
const { mutate: deleteLoc, isLoading: deleteLocLoading } = useLibraryMutation(
'locations.delete',
{
onSuccess: () => {
submitPlausibleEvent({ event: { type: 'locationDelete' } });
onSubmit?.();
},
onSettled: () => {

View file

@ -1,5 +1,5 @@
import { useRef } from 'react';
import { useLibraryMutation } from '@sd/client';
import { useLibraryMutation, usePlausibleEvent } from '@sd/client';
import { ConfirmModal, ModalRef } from '~/components/layout/Modal';
type Props = {
@ -11,8 +11,11 @@ type Props = {
const DeleteTagModal = ({ trigger, onSubmit, tagId }: Props) => {
const modalRef = useRef<ModalRef>(null);
const submitPlausibleEvent = usePlausibleEvent();
const { mutate: deleteTag, isLoading: deleteTagLoading } = useLibraryMutation('tags.delete', {
onSuccess: () => {
submitPlausibleEvent({ event: { type: 'tagDelete' } });
onSubmit?.();
},
onSettled: () => {

View file

@ -2,7 +2,7 @@ import { useQueryClient } from '@tanstack/react-query';
import { forwardRef, useEffect, useState } from 'react';
import { Pressable, Text, View } from 'react-native';
import ColorPicker from 'react-native-wheel-color-picker';
import { useLibraryMutation } from '@sd/client';
import { useLibraryMutation, usePlausibleEvent } from '@sd/client';
import { FadeInAnimation } from '~/components/animation/layout';
import { Input } from '~/components/form/Input';
import { Modal, ModalRef } from '~/components/layout/Modal';
@ -20,6 +20,8 @@ const CreateTagModal = forwardRef<ModalRef, unknown>((_, ref) => {
// TODO: Use react-hook-form?
const submitPlausibleEvent = usePlausibleEvent();
const { mutate: createTag } = useLibraryMutation('tags.create', {
onSuccess: () => {
// Reset form
@ -28,6 +30,8 @@ const CreateTagModal = forwardRef<ModalRef, unknown>((_, ref) => {
setShowPicker(false);
queryClient.invalidateQueries(['tags.list']);
submitPlausibleEvent({ event: { type: 'tagCreate' } });
},
onSettled: () => {
// Close modal

View file

@ -12,6 +12,7 @@ const Drawer = createDrawerNavigator<DrawerNavParamList>();
export default function DrawerNavigator() {
return (
<Drawer.Navigator
id="drawer"
initialRouteName="Home"
screenOptions={{
headerShown: false,

View file

@ -9,7 +9,11 @@ const OnboardingStack = createStackNavigator<OnboardingStackParamList>();
export default function OnboardingNavigator() {
return (
<OnboardingStack.Navigator initialRouteName="GetStarted" screenOptions={{ headerShown: false }}>
<OnboardingStack.Navigator
id="onboarding"
initialRouteName="GetStarted"
screenOptions={{ headerShown: false }}
>
<OnboardingStack.Screen name="GetStarted" component={GetStartedScreen} />
<OnboardingStack.Screen name="NewLibrary" component={NewLibraryScreen} />
<OnboardingStack.Screen name="MasterPassword" component={MasterPasswordScreen} />

View file

@ -20,6 +20,7 @@ const SettingsStack = createStackNavigator<SettingsStackParamList>();
export default function SettingsNavigator() {
return (
<SettingsStack.Navigator
id="settings"
initialRouteName="Home"
screenOptions={{
headerBackTitleVisible: false,

View file

@ -13,6 +13,7 @@ const Tab = createBottomTabNavigator<TabParamList>();
export default function TabNavigator() {
return (
<Tab.Navigator
id="tab"
initialRouteName="OverviewStack"
screenOptions={{
headerShown: false,

View file

@ -7,7 +7,8 @@ import {
telemetryStore,
useBridgeMutation,
useDebugState,
useOnboardingStore
useOnboardingStore,
usePlausibleEvent
} from '@sd/client';
import { PulseAnimation } from '~/components/animation/lottie';
import { tw } from '~/lib/tailwind';
@ -23,12 +24,17 @@ const CreatingLibraryScreen = ({ navigation }: OnboardingStackScreenProps<'Creat
const debugState = useDebugState();
const obStore = useOnboardingStore();
const submitPlausibleEvent = usePlausibleEvent();
const createLibrary = useBridgeMutation('library.create', {
onSuccess: (lib) => {
resetOnboardingStore();
queryClient.setQueryData(['library.list'], (libraries: any) => [...(libraries || []), lib]);
// Switch to the new library
currentLibraryStore.id = lib.uuid;
if (obStore.shareTelemetry) {
submitPlausibleEvent({ event: { type: 'libraryCreate' } });
}
},
onError: () => {
// TODO: Show toast

View file

@ -67,5 +67,5 @@ To run mobile app
If you are having issues ensure you are using the following versions of Rust and Node:
- Rust version: **1.67.0**
- Rust version: **1.68.2**
- Node version: **18**

View file

@ -5,12 +5,10 @@ import { useRef } from 'react';
import { useLibraryMutation, useLibraryQuery, usePlausibleEvent } from '@sd/client';
import { ContextMenu, DropdownMenu, dialogManager, useContextMenu, useDropdownMenu } from '@sd/ui';
import { useScrolled } from '~/hooks/useScrolled';
import { usePlatform } from '~/util/Platform';
import CreateDialog from '../settings/library/tags/CreateDialog';
export default (props: { objectId: number }) => {
const platform = usePlatform();
const submitPlausibleEvent = usePlausibleEvent({ platformType: platform.platform });
const submitPlausibleEvent = usePlausibleEvent();
const tags = useLibraryQuery(['tags.list'], { suspense: true });
const tagsForObject = useLibraryQuery(['tags.getForObject', props.objectId], { suspense: true });

View file

@ -4,6 +4,7 @@ import { Navigate, Outlet, useLocation, useParams } from 'react-router-dom';
import {
ClientContextProvider,
LibraryContextProvider,
initPlausible,
useClientContext,
usePlausiblePageViewMonitor
} from '@sd/client';
@ -17,10 +18,10 @@ const Layout = () => {
const os = useOperatingSystem();
usePlausiblePageViewMonitor({
currentPath: useLocation().pathname,
platformType: usePlatform().platform
initPlausible({
platformType: usePlatform().platform === 'tauri' ? 'desktop' : 'web'
});
usePlausiblePageViewMonitor({ currentPath: useLocation().pathname });
if (library === null && libraries.data) {
const firstLibrary = libraries.data[0];
@ -33,10 +34,10 @@ const Layout = () => {
<div
className={clsx(
// App level styles
'text-ink flex h-screen cursor-default select-none overflow-hidden',
os === 'browser' && 'bg-app border-app-line/50 border-t',
'flex h-screen cursor-default select-none overflow-hidden text-ink',
os === 'browser' && 'border-t border-app-line/50 bg-app',
os === 'macOS' && 'has-blur-effects rounded-[10px]',
os !== 'browser' && os !== 'windows' && 'border-app-frame border'
os !== 'browser' && os !== 'windows' && 'border border-app-frame'
)}
onContextMenu={(e) => {
// TODO: allow this on some UI text at least / disable default browser context menu
@ -48,7 +49,7 @@ const Layout = () => {
<div className="relative flex w-full">
{library ? (
<LibraryContextProvider library={library}>
<Suspense fallback={<div className="bg-app h-screen w-screen" />}>
<Suspense fallback={<div className="h-screen w-screen bg-app" />}>
<Outlet />
</Suspense>
</LibraryContextProvider>

View file

@ -10,8 +10,7 @@ interface Props extends UseDialogProps {
export default (props: Props) => {
const dialog = useDialog(props);
const platform = usePlatform();
const submitPlausibleEvent = usePlausibleEvent({ platformType: platform.platform });
const submitPlausibleEvent = usePlausibleEvent();
const form = useZodForm();

View file

@ -6,8 +6,7 @@ import { usePlatform } from '~/util/Platform';
export default (props: UseDialogProps & { assignToObject?: number }) => {
const dialog = useDialog(props);
const platform = usePlatform();
const submitPlausibleEvent = usePlausibleEvent({ platformType: platform.platform });
const submitPlausibleEvent = usePlausibleEvent();
const form = useZodForm({
schema: z.object({

View file

@ -10,8 +10,7 @@ interface Props extends UseDialogProps {
export default (props: Props) => {
const dialog = useDialog(props);
const platform = usePlatform();
const submitPlausibleEvent = usePlausibleEvent({ platformType: platform.platform });
const submitPlausibleEvent = usePlausibleEvent();
const form = useZodForm();

View file

@ -17,12 +17,11 @@ import {
SelectOption,
Tooltip,
UseDialogProps,
forms,
useDialog
} from '@sd/ui';
import { forms } from '@sd/ui';
import { PasswordMeter } from '~/components/PasswordMeter';
import { generatePassword } from '~/util';
import { usePlatform } from '~/util/Platform';
const { Input, z, useZodForm } = forms;
@ -37,8 +36,7 @@ const schema = z.object({
export default (props: UseDialogProps) => {
const dialog = useDialog(props);
const platform = usePlatform();
const createLibraryEvent = usePlausibleEvent({ platformType: platform.platform });
const submitPlausibleEvent = usePlausibleEvent();
const form = useZodForm({
schema,
@ -62,7 +60,7 @@ export default (props: UseDialogProps) => {
library
]);
createLibraryEvent({
submitPlausibleEvent({
event: {
type: 'libraryCreate'
}
@ -116,7 +114,7 @@ export default (props: UseDialogProps) => {
</div>
<span className="mt-1 text-xs font-medium">Share anonymous usage</span>
<Tooltip label="Share completely anonymous telemetry data to help the developers improve the app">
<Info className="text-ink-faint ml-1.5 h-4 w-4" />
<Info className="ml-1.5 h-4 w-4 text-ink-faint" />
</Tooltip>
</div>

View file

@ -12,9 +12,7 @@ interface Props extends UseDialogProps {
export default function DeleteLibraryDialog(props: Props) {
const dialog = useDialog(props);
const platform = usePlatform();
const submitPlausibleEvent = usePlausibleEvent({ platformType: platform.platform });
const shareTelemetry = useTelemetryState().shareTelemetry;
const submitPlausibleEvent = usePlausibleEvent();
const queryClient = useQueryClient();
const deleteLib = useBridgeMutation('library.delete', {

View file

@ -11,7 +11,6 @@ import {
usePlausibleEvent
} from '@sd/client';
import { Loader } from '@sd/ui';
import { usePlatform } from '~/util/Platform';
import { OnboardingContainer, OnboardingDescription, OnboardingTitle } from './Layout';
import { useUnlockOnboardingScreen } from './Progress';
@ -19,8 +18,7 @@ export default function OnboardingCreatingLibrary() {
const navigate = useNavigate();
const queryClient = useQueryClient();
const debugState = useDebugState();
const platform = usePlatform();
const submitPlausibleEvent = usePlausibleEvent({ platformType: platform.platform });
const submitPlausibleEvent = usePlausibleEvent();
const [status, setStatus] = useState('Creating your library...');
@ -33,7 +31,9 @@ export default function OnboardingCreatingLibrary() {
library
]);
submitPlausibleEvent({ event: { type: 'libraryCreate' } });
if (obStore.shareTelemetry) {
submitPlausibleEvent({ event: { type: 'libraryCreate' } });
}
resetOnboardingStore();
navigate(`/${library.uuid}/overview`);

View file

@ -1,26 +1,16 @@
import Plausible from 'plausible-tracker';
import { PlausibleOptions as PlausibleTrackerOptions } from 'plausible-tracker';
import Plausible, { PlausibleOptions as PlausibleTrackerOptions } from 'plausible-tracker';
import { useCallback, useEffect, useRef } from 'react';
import { useDebugState, useTelemetryState } from '../stores';
import { PlausiblePlatformType, telemetryStore, useDebugState, useTelemetryState } from '../stores';
/**
* This should be in sync with the Core's version.
*/
const Version = '0.1.0';
/**
* Possible Platform types that can be sourced from `usePlatform().platform` or even hardcoded.
*
* @remarks
* The `tauri` platform is renamed to `desktop` for analytic purposes.
*/
type PlatformType = 'web' | 'mobile' | 'tauri';
const Domain = 'app.spacedrive.com';
const VERSION = '0.1.0';
const DOMAIN = 'app.spacedrive.com';
const PlausibleProvider = Plausible({
trackLocalhost: true,
domain: Domain
domain: DOMAIN
});
/**
@ -71,7 +61,7 @@ type PageViewEvent = BasePlausibleEvent<'pageview', 'url'>;
* @example
* ```ts
* const platform = usePlatform();
* const submitPlausibleEvent = usePlausibleEvent({ platformType: platform.platform });
* const submitPlausibleEvent = usePlausibleEvent();
*
* const createLibrary = useBridgeMutation('library.create', {
* onSuccess: (library) => {
@ -97,7 +87,7 @@ type TagAssignEvent = BasePlausibleEvent<'tagAssign'>;
/**
* All union of available, ready-to-use events.
*
* Every possible event must also be added as a "goal" in Plausible's settings (on their site) for the currently active {@link Domain domain}.
* Every possible event must also be added as a "goal" in Plausible's settings (on their site) for the currently active {@link DOMAIN domain}.
*/
type PlausibleEvent =
| PageViewEvent
@ -117,7 +107,7 @@ type PlausibleEvent =
interface PlausibleTrackerEvent {
eventName: string;
props: {
platform: 'web' | 'desktop' | 'mobile';
platform: PlausiblePlatformType;
version: string;
debug: boolean;
};
@ -135,9 +125,9 @@ interface SubmitEventProps {
/**
* The current platform type. This should be the output of `usePlatform().platform`
*
* @see {@link PlatformType}
* @see {@link PlausiblePlatformType}
*/
platformType: PlatformType;
platformType: PlausiblePlatformType;
/**
* An optional screen width. Default is `window.screen.width`
*/
@ -182,6 +172,7 @@ interface SubmitEventProps {
* @see {@link https://plausible-tracker.netlify.app/#tracking-custom-events-and-goals Tracking custom events}
*/
const submitPlausibleEvent = async ({ event, debugState, ...props }: SubmitEventProps) => {
if (props.platformType === 'unknown') return;
if (debugState.enabled && debugState.shareTelemetry !== true) return;
if (
'plausibleOptions' in event && 'telemetryOverride' in event.plausibleOptions
@ -193,8 +184,8 @@ const submitPlausibleEvent = async ({ event, debugState, ...props }: SubmitEvent
const fullEvent: PlausibleTrackerEvent = {
eventName: event.type,
props: {
platform: props.platformType === 'tauri' ? 'desktop' : props.platformType,
version: Version,
platform: props.platformType,
version: VERSION,
debug: debugState.enabled
},
options: {
@ -224,9 +215,9 @@ interface UsePlausibleEventProps {
/**
* The current platform type. This should be the output of `usePlatform().platform`
*
* @see {@link PlatformType}
* @see {@link PlausiblePlatformType}
*/
platformType: PlatformType;
platformType: PlausiblePlatformType;
}
interface EventSubmissionCallbackProps {
@ -260,7 +251,7 @@ interface EventSubmissionCallbackProps {
* @example
* ```ts
* const platform = usePlatform();
* const submitPlausibleEvent = usePlausibleEvent({ platformType: platform.platform });
* const submitPlausibleEvent = usePlausibleEvent();
*
* const createLibrary = useBridgeMutation('library.create', {
* onSuccess: (library) => {
@ -273,9 +264,9 @@ interface EventSubmissionCallbackProps {
* });
* ```
*/
export const usePlausibleEvent = ({ platformType }: UsePlausibleEventProps) => {
export const usePlausibleEvent = () => {
const debugState = useDebugState();
const shareTelemetry = useTelemetryState().shareTelemetry;
const telemetryState = useTelemetryState();
const previousEvent = useRef({} as BasePlausibleEvent<string>);
return useCallback(
@ -283,9 +274,14 @@ export const usePlausibleEvent = ({ platformType }: UsePlausibleEventProps) => {
if (previousEvent.current === props.event) return;
else previousEvent.current = props.event;
submitPlausibleEvent({ debugState, shareTelemetry, platformType, ...props });
submitPlausibleEvent({
debugState,
shareTelemetry: telemetryState.shareTelemetry,
platformType: telemetryState.platform,
...props
});
},
[debugState, platformType, shareTelemetry]
[debugState, telemetryState]
);
};
@ -331,12 +327,6 @@ export interface PageViewMonitorProps {
* @see {@link PageViewRegexRules} for sanitization
*/
currentPath: string;
/**
* The current platform type. This should be the output of `usePlatform().platform`
*
* @see {@link PlatformType}
*/
platformType: PlatformType;
}
/**
@ -358,15 +348,14 @@ export interface PageViewMonitorProps {
* @example
* ```ts
* usePlausiblePageViewMonitor({
* currentPath: useLocation().pathname,
* platformType: usePlatform().platform
* currentPath: useLocation().pathname
* });
* ```
*/
export const usePlausiblePageViewMonitor = (props: PageViewMonitorProps) => {
const plausibleEvent = usePlausibleEvent({ platformType: props.platformType });
export const usePlausiblePageViewMonitor = ({ currentPath }: PageViewMonitorProps) => {
const plausibleEvent = usePlausibleEvent();
let path = props.currentPath;
let path = currentPath;
PageViewRegexRules.forEach((e) => (path = path.replace(e[0], e[1])));
useEffect(() => {
@ -378,3 +367,8 @@ export const usePlausiblePageViewMonitor = (props: PageViewMonitorProps) => {
});
}, [path, plausibleEvent]);
};
export const initPlausible = ({ platformType }: { platformType: PlausiblePlatformType }) => {
telemetryStore.platform = platformType;
return;
};

View file

@ -1,8 +1,22 @@
import { useSnapshot } from 'valtio';
import { valtioPersist } from './util';
export const telemetryStore = valtioPersist('sd-telemetryStore', {
shareTelemetry: false // false by default, so functions cannot accidentally send data if the user has not decided
/**
* Possible Platform types that can be sourced from `usePlatform().platform` or even hardcoded.
*
* @remarks
* The `tauri` platform is renamed to `desktop` for analytic purposes.
*/
export type PlausiblePlatformType = 'web' | 'mobile' | 'desktop' | 'unknown';
type TelemetryState = {
shareTelemetry: boolean;
platform: PlausiblePlatformType;
};
export const telemetryStore = valtioPersist<TelemetryState>('sd-telemetryStore', {
shareTelemetry: false, // false by default, so functions cannot accidentally send data if the user has not decided
platform: 'unknown'
});
export function useTelemetryState() {