mirror of
https://github.com/spacedriveapp/spacedrive.git
synced 2026-05-16 03:04:27 -04:00
[ENG-227] Desktop app and landing page telemetry using Plausible Analytics (#583)
* add Plausible analytics to landing page
* proxy plausible through vercel
* fix typo & add other options
* add plausible to `sd/client`
* add telemetry sharing option into library config
* add telemetry config option to lib creation dialog
* revert error message change but keep the typo fix
* add telemetry sharing & error handling to client context
* add important note about requiring the tracker component in root/base layouts
* add the `PlausibleTracker`
* grammatical tweaks
* some TS cleanup
* disable analytics in debug mode
* further component improvements and use custom event props
* more cleanup
* remove tracking from onboarding (no telemetry sharing config option)
* update comment
* add fancy new plausible hooks/tracking
* add `pageview` monitoring hook to `$libraryId` layout
* add library creation events to onboarding and creation dialog
* revert `useCurrentLibraryId()` error handling & add important comment
* minor comment tweaks
* replace `usage` with `telemetry`
* add missing newline
* add location create & delete events
* add tag create & delete events
* add/update library create & delete events
* add fn for getting telemetry settings for library by uuid
* add more events + fix a few bugs
* update generics
* add `telemetryState` `valtio` store
* use new telemetry state
* remove old artifacts from `ClientContext`
* Revert "add telemetry sharing option into library config"
This reverts commit afb9f892ab.
* update events, docs & generics
* add `tagAssign` event
* light comment updates
* const names, comments, etc
* add additional info to props and update comment
* add telemetry sharing to debug state (for sharing telemetry in debug mode)
* update `debugState` item name
* change how `Switch` updates the store in privacy settings
* remove `getTelemetryState` from `telemetryState`
* cleanup library creation event handling/telemetry config updating
* add `DebugPopover` to onboarding in debug mode
* improve code quality/comments
* remove useless comment
* rename `ob_store` and `shareTelemetryDataWithDevelopers`
* fix typo
* add `telemetryLogger` and prevent multiple of the same events from firing consecutively
* add more unique path matching and fix an issue with events
* rename `telemetryLogger` -> `telemetryLogging`
---------
Co-authored-by: brxken128 <77554505+brxken128@users.noreply.github.com>
This commit is contained in:
@@ -36,7 +36,7 @@ export function ErrorPage({
|
||||
<pre className="text-ink m-2">Error: {message}</pre>
|
||||
{debug.enabled && (
|
||||
<pre className="text-ink-dull m-2 text-sm">
|
||||
Check the console (CMD/CRTL + OPTION + i) for stack trace.
|
||||
Check the console (CMD/CTRL + OPTION + i) for stack trace.
|
||||
</pre>
|
||||
)}
|
||||
<div className="text-ink flex flex-row space-x-2">
|
||||
|
||||
@@ -18,7 +18,8 @@ import {
|
||||
isObject,
|
||||
useLibraryContext,
|
||||
useLibraryMutation,
|
||||
useLibraryQuery
|
||||
useLibraryQuery,
|
||||
usePlausibleEvent
|
||||
} from '@sd/client';
|
||||
import { ContextMenu, dialogManager } from '@sd/ui';
|
||||
import { useExplorerParams } from '~/app/$libraryId/location/$id';
|
||||
@@ -248,9 +249,16 @@ export default ({ data, ...props }: Props) => {
|
||||
};
|
||||
|
||||
const AssignTagMenuItems = (props: { objectId: number }) => {
|
||||
const platform = usePlatform();
|
||||
const submitPlausibleEvent = usePlausibleEvent({ platformType: platform.platform });
|
||||
|
||||
const tags = useLibraryQuery(['tags.list'], { suspense: true });
|
||||
const tagsForObject = useLibraryQuery(['tags.getForObject', props.objectId], { suspense: true });
|
||||
const assignTag = useLibraryMutation('tags.assign');
|
||||
const assignTag = useLibraryMutation('tags.assign', {
|
||||
onSuccess: () => {
|
||||
submitPlausibleEvent({ event: { type: 'tagAssign' } });
|
||||
}
|
||||
});
|
||||
|
||||
return (
|
||||
<>
|
||||
|
||||
@@ -1,12 +1,20 @@
|
||||
import { useLibraryMutation } from '@sd/client';
|
||||
import { useLibraryMutation, usePlausibleEvent } from '@sd/client';
|
||||
import { dialogManager } from '@sd/ui';
|
||||
import { usePlatform } from '~/util/Platform';
|
||||
import AddLocationDialog from '../../settings/library/locations/AddDialog';
|
||||
|
||||
export default () => {
|
||||
const platform = usePlatform();
|
||||
|
||||
const createLocation = useLibraryMutation('locations.create');
|
||||
const submitPlausibleEvent = usePlausibleEvent({ platformType: platform.platform });
|
||||
const createLocation = useLibraryMutation('locations.create', {
|
||||
onSuccess: () => {
|
||||
submitPlausibleEvent({
|
||||
event: {
|
||||
type: 'locationCreate'
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
return (
|
||||
<button
|
||||
|
||||
@@ -24,13 +24,45 @@ export default () => {
|
||||
<Setting
|
||||
mini
|
||||
title="rspc Logger"
|
||||
description="Enable the logger link so you can see what's going on in the browser logs."
|
||||
description="Enable the RSPC logger so you can see what's going on in the browser logs."
|
||||
>
|
||||
<Switch
|
||||
checked={debugState.rspcLogger}
|
||||
onClick={() => (getDebugState().rspcLogger = !debugState.rspcLogger)}
|
||||
/>
|
||||
</Setting>
|
||||
<Setting
|
||||
mini
|
||||
title="Share telemetry"
|
||||
description="Share telemetry, even in debug mode (telemetry sharing must also be enabled in your client settings)"
|
||||
>
|
||||
<Switch
|
||||
checked={debugState.shareTelemetry}
|
||||
onClick={() => {
|
||||
// if debug telemetry sharing is about to be disabled, but telemetry logging is enabled
|
||||
// then disable it
|
||||
if (!debugState.shareTelemetry === false && debugState.telemetryLogging)
|
||||
getDebugState().telemetryLogging = false;
|
||||
getDebugState().shareTelemetry = !debugState.shareTelemetry;
|
||||
}}
|
||||
/>
|
||||
</Setting>
|
||||
<Setting
|
||||
mini
|
||||
title="Telemetry logger"
|
||||
description="Enable the telemetry logger so you can see what's going on in the browser logs"
|
||||
>
|
||||
<Switch
|
||||
checked={debugState.telemetryLogging}
|
||||
onClick={() => {
|
||||
// if telemetry logging is about to be enabled, but debug telemetry sharing is disabled
|
||||
// then enable it
|
||||
if (!debugState.telemetryLogging && debugState.shareTelemetry === false)
|
||||
getDebugState().shareTelemetry = true;
|
||||
getDebugState().telemetryLogging = !debugState.telemetryLogging;
|
||||
}}
|
||||
/>
|
||||
</Setting>
|
||||
{platform.openPath && (
|
||||
<Setting
|
||||
mini
|
||||
|
||||
@@ -1,8 +1,14 @@
|
||||
import clsx from 'clsx';
|
||||
import { Suspense } from 'react';
|
||||
import { Navigate, Outlet, useParams } from 'react-router-dom';
|
||||
import { ClientContextProvider, LibraryContextProvider, useClientContext } from '@sd/client';
|
||||
import { Navigate, Outlet, useLocation, useParams } from 'react-router-dom';
|
||||
import {
|
||||
ClientContextProvider,
|
||||
LibraryContextProvider,
|
||||
useClientContext,
|
||||
usePlausiblePageViewMonitor
|
||||
} from '@sd/client';
|
||||
import { useOperatingSystem } from '~/hooks/useOperatingSystem';
|
||||
import { usePlatform } from '~/util/Platform';
|
||||
import Sidebar from './Sidebar';
|
||||
import Toasts from './Toasts';
|
||||
|
||||
@@ -11,6 +17,11 @@ const Layout = () => {
|
||||
|
||||
const os = useOperatingSystem();
|
||||
|
||||
usePlausiblePageViewMonitor({
|
||||
currentPath: useLocation().pathname,
|
||||
platformType: usePlatform().platform
|
||||
});
|
||||
|
||||
if (library === null && libraries.data) {
|
||||
const firstLibrary = libraries.data[0];
|
||||
|
||||
|
||||
@@ -1,21 +1,24 @@
|
||||
import { useState } from 'react';
|
||||
import { Switch } from '@sd/ui';
|
||||
import { telemetryStore, useTelemetryState } from '~/../packages/client/src';
|
||||
import { Heading } from '../Layout';
|
||||
import Setting from '../Setting';
|
||||
|
||||
export default function PrivacySettings() {
|
||||
const [shareUsageData, setShareUsageData] = useState(true);
|
||||
const [blurEffects, setBlurEffects] = useState(true);
|
||||
const shareTelemetry = useTelemetryState().shareTelemetry;
|
||||
|
||||
return (
|
||||
<>
|
||||
<Heading title="Privacy" description="" />
|
||||
<Setting
|
||||
mini
|
||||
title="Share Usage Data"
|
||||
title="Share Telemetry and Usage Data"
|
||||
description="Share anonymous usage data to help us improve the app."
|
||||
>
|
||||
<Switch checked={shareUsageData} onCheckedChange={setShareUsageData} className="m-2 ml-4" />
|
||||
<Switch
|
||||
checked={shareTelemetry}
|
||||
onClick={() => (telemetryStore.shareTelemetry = !shareTelemetry)}
|
||||
className="m-2 ml-4"
|
||||
/>
|
||||
</Setting>
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { useLibraryMutation } from '@sd/client';
|
||||
import { useLibraryMutation, usePlausibleEvent } from '@sd/client';
|
||||
import { Dialog, UseDialogProps, useDialog } from '@sd/ui';
|
||||
import { useZodForm } from '@sd/ui/src/forms';
|
||||
import { usePlatform } from '~/util/Platform';
|
||||
|
||||
interface Props extends UseDialogProps {
|
||||
onSuccess: () => void;
|
||||
@@ -9,11 +10,16 @@ interface Props extends UseDialogProps {
|
||||
|
||||
export default (props: Props) => {
|
||||
const dialog = useDialog(props);
|
||||
const platform = usePlatform();
|
||||
const submitPlausibleEvent = usePlausibleEvent({ platformType: platform.platform });
|
||||
|
||||
const form = useZodForm();
|
||||
|
||||
const deleteLocation = useLibraryMutation('locations.delete', {
|
||||
onSuccess: props.onSuccess
|
||||
onSuccess: () => {
|
||||
submitPlausibleEvent({ event: { type: 'locationDelete' } });
|
||||
props.onSuccess();
|
||||
}
|
||||
});
|
||||
|
||||
return (
|
||||
|
||||
@@ -1,10 +1,13 @@
|
||||
import { useLibraryMutation } from '@sd/client';
|
||||
import { useClientContext, useLibraryMutation, usePlausibleEvent } from '@sd/client';
|
||||
import { Dialog, UseDialogProps, useDialog } from '@sd/ui';
|
||||
import { Input, useZodForm, z } from '@sd/ui/src/forms';
|
||||
import ColorPicker from '~/components/ColorPicker';
|
||||
import { usePlatform } from '~/util/Platform';
|
||||
|
||||
export default (props: UseDialogProps) => {
|
||||
const dialog = useDialog(props);
|
||||
const platform = usePlatform();
|
||||
const submitPlausibleEvent = usePlausibleEvent({ platformType: platform.platform });
|
||||
|
||||
const form = useZodForm({
|
||||
schema: z.object({
|
||||
@@ -17,6 +20,9 @@ export default (props: UseDialogProps) => {
|
||||
});
|
||||
|
||||
const createTag = useLibraryMutation('tags.create', {
|
||||
onSuccess: () => {
|
||||
submitPlausibleEvent({ event: { type: 'tagCreate' } });
|
||||
},
|
||||
onError: (e) => {
|
||||
console.error('error', e);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { useLibraryMutation } from '@sd/client';
|
||||
import { useLibraryMutation, usePlausibleEvent } from '@sd/client';
|
||||
import { Dialog, UseDialogProps, useDialog } from '@sd/ui';
|
||||
import { useZodForm } from '@sd/ui/src/forms';
|
||||
import { usePlatform } from '~/util/Platform';
|
||||
|
||||
interface Props extends UseDialogProps {
|
||||
tagId: number;
|
||||
@@ -9,11 +10,16 @@ interface Props extends UseDialogProps {
|
||||
|
||||
export default (props: Props) => {
|
||||
const dialog = useDialog(props);
|
||||
const platform = usePlatform();
|
||||
const submitPlausibleEvent = usePlausibleEvent({ platformType: platform.platform });
|
||||
|
||||
const form = useZodForm();
|
||||
|
||||
const deleteTag = useLibraryMutation('tags.delete', {
|
||||
onSuccess: props.onSuccess
|
||||
onSuccess: () => {
|
||||
submitPlausibleEvent({ event: { type: 'tagDelete' } });
|
||||
props.onSuccess();
|
||||
}
|
||||
});
|
||||
|
||||
return (
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useQueryClient } from '@tanstack/react-query';
|
||||
import { ArrowsClockwise, Clipboard, Eye, EyeSlash } from 'phosphor-react';
|
||||
import { ArrowsClockwise, Clipboard, Eye, EyeSlash, Info } from 'phosphor-react';
|
||||
import { useState } from 'react';
|
||||
import {
|
||||
Algorithm,
|
||||
@@ -7,18 +7,22 @@ import {
|
||||
HashingAlgoSlug,
|
||||
generatePassword,
|
||||
hashingAlgoSlugSchema,
|
||||
useBridgeMutation
|
||||
useBridgeMutation,
|
||||
usePlausibleEvent
|
||||
} from '@sd/client';
|
||||
import {
|
||||
Button,
|
||||
CheckBox,
|
||||
Dialog,
|
||||
PasswordMeter,
|
||||
Select,
|
||||
SelectOption,
|
||||
Tooltip,
|
||||
UseDialogProps,
|
||||
useDialog
|
||||
} from '@sd/ui';
|
||||
import { forms } from '@sd/ui';
|
||||
import { usePlatform } from '~/util/Platform';
|
||||
|
||||
const { Input, z, useZodForm } = forms;
|
||||
|
||||
@@ -27,11 +31,14 @@ const schema = z.object({
|
||||
password: z.string(),
|
||||
password_validate: z.string(),
|
||||
algorithm: z.string(),
|
||||
hashing_algorithm: hashingAlgoSlugSchema
|
||||
hashing_algorithm: hashingAlgoSlugSchema,
|
||||
share_telemetry: z.boolean()
|
||||
});
|
||||
|
||||
export default (props: UseDialogProps) => {
|
||||
const dialog = useDialog(props);
|
||||
const platform = usePlatform();
|
||||
const createLibraryEvent = usePlausibleEvent({ platformType: platform.platform });
|
||||
|
||||
const form = useZodForm({
|
||||
schema,
|
||||
@@ -54,6 +61,12 @@ export default (props: UseDialogProps) => {
|
||||
...(libraries || []),
|
||||
library
|
||||
]);
|
||||
|
||||
createLibraryEvent({
|
||||
event: {
|
||||
type: 'libraryCreate'
|
||||
}
|
||||
});
|
||||
},
|
||||
onError: (err: any) => {
|
||||
console.error(err);
|
||||
@@ -95,6 +108,20 @@ export default (props: UseDialogProps) => {
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="mt-3 mb-1 flex flex-row items-center">
|
||||
<div className="space-x-2">
|
||||
<CheckBox
|
||||
className="bg-app-selected"
|
||||
defaultChecked={true}
|
||||
{...form.register('share_telemetry', { required: true })}
|
||||
/>
|
||||
</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" />
|
||||
</Tooltip>
|
||||
</div>
|
||||
|
||||
{/* TODO: Proper UI for this. Maybe checkbox for encrypted or not and then reveal these fields. Select encrypted by default. */}
|
||||
{/* <span className="text-sm">Make the secret key field empty to skip key setup.</span> */}
|
||||
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { useQueryClient } from '@tanstack/react-query';
|
||||
import { useBridgeMutation } from '@sd/client';
|
||||
import { useBridgeMutation, usePlausibleEvent, useTelemetryState } from '@sd/client';
|
||||
import { Dialog, UseDialogProps, useDialog } from '@sd/ui';
|
||||
import { forms } from '@sd/ui';
|
||||
import { usePlatform } from '~/util/Platform';
|
||||
|
||||
const { useZodForm, z } = forms;
|
||||
|
||||
@@ -11,11 +12,20 @@ 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 queryClient = useQueryClient();
|
||||
const deleteLib = useBridgeMutation('library.delete', {
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries(['library.list']);
|
||||
|
||||
submitPlausibleEvent({
|
||||
event: {
|
||||
type: 'libraryDelete'
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -20,6 +20,11 @@ const Index = () => {
|
||||
return <Navigate to={`${libraryId}/overview`} />;
|
||||
};
|
||||
|
||||
|
||||
// NOTE: all route `Layout`s below should contain
|
||||
// the `usePlausiblePageViewMonitor` hook, as early as possible (ideally within the layout itself).
|
||||
// the hook should only be included if there's a valid `ClientContext` (so not onboarding)
|
||||
|
||||
const routes = [
|
||||
{
|
||||
index: true,
|
||||
|
||||
@@ -2,10 +2,11 @@ import BloomOne from '@sd/assets/images/bloom-one.png';
|
||||
import clsx from 'clsx';
|
||||
import { useEffect } from 'react';
|
||||
import { Outlet, useNavigate } from 'react-router';
|
||||
import { getOnboardingStore } from '@sd/client';
|
||||
import { getOnboardingStore, useDebugState } from '@sd/client';
|
||||
import { tw } from '@sd/ui';
|
||||
import DragRegion from '~/components/DragRegion';
|
||||
import { useOperatingSystem } from '~/hooks/useOperatingSystem';
|
||||
import DebugPopover from '../$libraryId/Layout/Sidebar/DebugPopover';
|
||||
import Progress from './Progress';
|
||||
|
||||
export const OnboardingContainer = tw.div`flex flex-col items-center`;
|
||||
@@ -15,6 +16,7 @@ export const OnboardingImg = tw.img`w-20 h-20 mb-2`;
|
||||
|
||||
export default () => {
|
||||
const os = useOperatingSystem();
|
||||
const debugState = useDebugState();
|
||||
const navigate = useNavigate();
|
||||
|
||||
useEffect(
|
||||
@@ -39,7 +41,6 @@ export default () => {
|
||||
)}
|
||||
>
|
||||
<DragRegion className="z-50 h-9" />
|
||||
|
||||
<div className="-mt-5 flex grow flex-col p-10">
|
||||
<div className="flex grow flex-col items-center justify-center">
|
||||
<Outlet />
|
||||
@@ -55,6 +56,7 @@ export default () => {
|
||||
{/* <img src={BloomThree} className="absolute w-[2000px] h-[2000px] -right-[200px]" /> */}
|
||||
</div>
|
||||
</div>
|
||||
{debugState.enabled && <DebugPopover />}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -24,7 +24,7 @@ export function useUnlockOnboardingScreen() {
|
||||
}
|
||||
|
||||
export default function OnboardingProgress() {
|
||||
const ob_store = useOnboardingStore();
|
||||
const obStore = useOnboardingStore();
|
||||
const navigate = useNavigate();
|
||||
const currentScreenKey = useCurrentOnboardingScreenKey();
|
||||
|
||||
@@ -37,7 +37,7 @@ export default function OnboardingProgress() {
|
||||
return (
|
||||
<button
|
||||
key={path}
|
||||
disabled={!ob_store.unlockedScreens.includes(path)}
|
||||
disabled={!obStore.unlockedScreens.includes(path)}
|
||||
onClick={() => navigate(`/onboarding/${path}`)}
|
||||
className={clsx(
|
||||
'hover:bg-ink h-2 w-2 rounded-full transition disabled:opacity-10',
|
||||
|
||||
@@ -6,30 +6,37 @@ import {
|
||||
Algorithm,
|
||||
HASHING_ALGOS,
|
||||
resetOnboardingStore,
|
||||
telemetryStore,
|
||||
useBridgeMutation,
|
||||
useDebugState,
|
||||
useOnboardingStore
|
||||
useOnboardingStore,
|
||||
usePlausibleEvent
|
||||
} from '@sd/client';
|
||||
import { Loader } from '@sd/ui';
|
||||
import { usePlatform } from '~/util/Platform';
|
||||
import { OnboardingContainer, OnboardingDescription, OnboardingTitle } from './Layout';
|
||||
import { useUnlockOnboardingScreen } from './Progress';
|
||||
|
||||
export default function OnboardingCreatingLibrary() {
|
||||
const navigate = useNavigate();
|
||||
const queryClient = useQueryClient();
|
||||
const debugState = useDebugState();
|
||||
const platform = usePlatform();
|
||||
const submitPlausibleEvent = usePlausibleEvent({ platformType: platform.platform });
|
||||
|
||||
const [status, setStatus] = useState('Creating your library...');
|
||||
|
||||
useUnlockOnboardingScreen();
|
||||
|
||||
const debugState = useDebugState();
|
||||
|
||||
const createLibrary = useBridgeMutation('library.create', {
|
||||
onSuccess: (library) => {
|
||||
queryClient.setQueryData(['library.list'], (libraries: any) => [
|
||||
...(libraries || []),
|
||||
library
|
||||
]);
|
||||
|
||||
submitPlausibleEvent({ event: { type: 'libraryCreate' } });
|
||||
|
||||
resetOnboardingStore();
|
||||
navigate(`/${library.uuid}/overview`);
|
||||
},
|
||||
@@ -39,17 +46,21 @@ export default function OnboardingCreatingLibrary() {
|
||||
}
|
||||
});
|
||||
|
||||
const ob_store = useOnboardingStore();
|
||||
const obStore = useOnboardingStore();
|
||||
|
||||
const create = async () => {
|
||||
// opted to place this here as users could change their mind before library creation/onboarding finalization
|
||||
// it feels more fitting to configure it here (once)
|
||||
telemetryStore.shareTelemetry = obStore.shareTelemetry;
|
||||
|
||||
createLibrary.mutate({
|
||||
name: ob_store.newLibraryName,
|
||||
name: obStore.newLibraryName,
|
||||
auth: {
|
||||
type: 'TokenizedPassword',
|
||||
value: ob_store.passwordSetToken || ''
|
||||
value: obStore.passwordSetToken || ''
|
||||
},
|
||||
algorithm: ob_store.algorithm as Algorithm,
|
||||
hashing_algorithm: HASHING_ALGOS[ob_store.hashingAlgorithm]
|
||||
algorithm: obStore.algorithm as Algorithm,
|
||||
hashing_algorithm: HASHING_ALGOS[obStore.hashingAlgorithm]
|
||||
});
|
||||
|
||||
return;
|
||||
@@ -58,7 +69,7 @@ export default function OnboardingCreatingLibrary() {
|
||||
const created = useRef(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (created.current == true) return;
|
||||
if (created.current) return;
|
||||
created.current = true;
|
||||
create();
|
||||
const timer = setTimeout(() => {
|
||||
|
||||
@@ -41,7 +41,7 @@ export default function OnboardingNewLibrary() {
|
||||
}
|
||||
});
|
||||
|
||||
const ob_store = useOnboardingStore();
|
||||
const obStore = useOnboardingStore();
|
||||
|
||||
const onSubmit = form.handleSubmit(async (data) => {
|
||||
if (data.password !== data.password_validate) {
|
||||
@@ -104,7 +104,7 @@ export default function OnboardingNewLibrary() {
|
||||
<PasswordMeter password={form.watch('password')} />
|
||||
</div>
|
||||
<div className="mt-7 flex w-full justify-between">
|
||||
{!ob_store.passwordSetToken ? (
|
||||
{!obStore.passwordSetToken ? (
|
||||
<Button
|
||||
disabled={form.formState.isSubmitting}
|
||||
type="submit"
|
||||
|
||||
@@ -20,12 +20,12 @@ export default function OnboardingNewLibrary() {
|
||||
const navigate = useNavigate();
|
||||
const [importMode, setImportMode] = useState(false);
|
||||
|
||||
const ob_store = useOnboardingStore();
|
||||
const obStore = useOnboardingStore();
|
||||
|
||||
const form = useZodForm({
|
||||
schema,
|
||||
defaultValues: {
|
||||
name: ob_store.newLibraryName
|
||||
name: obStore.newLibraryName
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -36,8 +36,7 @@ export default function OnboardingPrivacy() {
|
||||
});
|
||||
|
||||
const onSubmit = form.handleSubmit(async (data) => {
|
||||
getOnboardingStore().shareTelemetryDataWithDevelopers =
|
||||
data.shareTelemetry === 'share-telemetry';
|
||||
getOnboardingStore().shareTelemetry = data.shareTelemetry === 'share-telemetry';
|
||||
|
||||
navigate('/onboarding/creating-library');
|
||||
});
|
||||
|
||||
@@ -19,6 +19,7 @@
|
||||
"@sd/config": "workspace:*",
|
||||
"@tanstack/react-query": "^4.12.0",
|
||||
"crypto-random-string": "^5.0.0",
|
||||
"plausible-tracker": "^0.3.8",
|
||||
"valtio": "^1.7.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
export * from './useClientContext';
|
||||
export * from './useLibraryContext';
|
||||
export * from './useOnlineLocations';
|
||||
export * from './usePlausible';
|
||||
|
||||
380
packages/client/src/hooks/usePlausible.tsx
Normal file
380
packages/client/src/hooks/usePlausible.tsx
Normal file
@@ -0,0 +1,380 @@
|
||||
import Plausible from 'plausible-tracker';
|
||||
import { PlausibleOptions as PlausibleTrackerOptions } from 'plausible-tracker';
|
||||
import { useCallback, useEffect, useRef } from 'react';
|
||||
import { 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 PlausibleProvider = Plausible({
|
||||
trackLocalhost: true,
|
||||
domain: Domain
|
||||
});
|
||||
|
||||
/**
|
||||
* This defines all possible options that may be provided by events upon submission.
|
||||
*
|
||||
* This extends the standard options provided by the `plausible-tracker`
|
||||
* package, but also offers some additiional options for custom functionality.
|
||||
*/
|
||||
interface PlausibleOptions extends PlausibleTrackerOptions {
|
||||
/**
|
||||
* This should **only** be used in contexts where telemetry sharing
|
||||
* must be allowed/denied via external means. Currently it is not used by anything,
|
||||
* but probably will be in the future.
|
||||
*/
|
||||
telemetryOverride?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* The base Plausible event, that all other events must be derived
|
||||
* from in an effort to keep things type-safe.
|
||||
*/
|
||||
type BasePlausibleEventWithOption<T, O extends keyof PlausibleOptions> = {
|
||||
type: T;
|
||||
plausibleOptions: Required<{
|
||||
[K in O]: PlausibleOptions[O];
|
||||
}>;
|
||||
};
|
||||
|
||||
type BasePlausibleEventWithoutOption<T> = {
|
||||
type: T;
|
||||
};
|
||||
|
||||
export type BasePlausibleEvent<T, O = void> = O extends keyof PlausibleOptions
|
||||
? BasePlausibleEventWithOption<T, O>
|
||||
: BasePlausibleEventWithoutOption<T>;
|
||||
|
||||
/**
|
||||
* The Plausible `pageview` event.
|
||||
*
|
||||
* **Do not use this directly. Instead, use the
|
||||
* {@link usePlausiblePageViewMonitor `usePlausiblePageViewMonitor`} hook**.
|
||||
*/
|
||||
type PageViewEvent = BasePlausibleEvent<'pageview', 'url'>;
|
||||
|
||||
/**
|
||||
* The custom Plausible `libraryCreate` event.
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* const platform = usePlatform();
|
||||
* const submitPlausibleEvent = usePlausibleEvent({ platformType: platform.platform });
|
||||
*
|
||||
* const createLibrary = useBridgeMutation('library.create', {
|
||||
* onSuccess: (library) => {
|
||||
* submitPlausibleEvent({
|
||||
* event: {
|
||||
* type: 'libraryCreate'
|
||||
* }
|
||||
* });
|
||||
* }
|
||||
* });
|
||||
* ```
|
||||
*/
|
||||
type LibraryCreateEvent = BasePlausibleEvent<'libraryCreate'>;
|
||||
type LibraryDeleteEvent = BasePlausibleEvent<'libraryDelete'>;
|
||||
|
||||
type LocationCreateEvent = BasePlausibleEvent<'locationCreate'>;
|
||||
type LocationDeleteEvent = BasePlausibleEvent<'locationDelete'>;
|
||||
|
||||
type TagCreateEvent = BasePlausibleEvent<'tagCreate'>;
|
||||
type TagDeleteEvent = BasePlausibleEvent<'tagDelete'>;
|
||||
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}.
|
||||
*/
|
||||
type PlausibleEvent =
|
||||
| PageViewEvent
|
||||
| LibraryCreateEvent
|
||||
| LibraryDeleteEvent
|
||||
| LocationCreateEvent
|
||||
| LocationDeleteEvent
|
||||
| TagCreateEvent
|
||||
| TagDeleteEvent
|
||||
| TagAssignEvent;
|
||||
|
||||
/**
|
||||
* An event information wrapper for internal use only.
|
||||
*
|
||||
* It means that events can both be logged to the console (if enabled) and submitted to Plausible with ease.
|
||||
*/
|
||||
interface PlausibleTrackerEvent {
|
||||
eventName: string;
|
||||
props: {
|
||||
platform: 'web' | 'desktop' | 'mobile';
|
||||
version: string;
|
||||
debug: boolean;
|
||||
};
|
||||
options: PlausibleTrackerOptions;
|
||||
callback?: () => void;
|
||||
}
|
||||
|
||||
interface SubmitEventProps {
|
||||
/**
|
||||
* The Plausible event to submit.
|
||||
*
|
||||
* @see {@link PlausibleEvent}
|
||||
*/
|
||||
event: PlausibleEvent;
|
||||
/**
|
||||
* The current platform type. This should be the output of `usePlatform().platform`
|
||||
*
|
||||
* @see {@link PlatformType}
|
||||
*/
|
||||
platformType: PlatformType;
|
||||
/**
|
||||
* An optional screen width. Default is `window.screen.width`
|
||||
*/
|
||||
screenWidth?: number;
|
||||
/**
|
||||
* Whether or not telemetry sharing is enabled for the current client.
|
||||
*
|
||||
* It is **crucial** that this is the direct output of `useTelemetryState().shareTelemetry`,
|
||||
* regardless of other conditions that may affect whether we share it (such as event overrides).
|
||||
*/
|
||||
shareTelemetry: boolean;
|
||||
/**
|
||||
* It is **crucial** that this is sourced from the output of `useDebugState()`
|
||||
*/
|
||||
debugState: {
|
||||
enabled: boolean;
|
||||
shareTelemetry: boolean;
|
||||
telemetryLogging: boolean;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* This function is for directly submitting events to Plausible.
|
||||
*
|
||||
* **Avoid using this directly, but if it's necessary then do not misuse this API and only
|
||||
* send telemetry when certain that it has been allowed by the user. Always prefer the
|
||||
* {@link usePlausibleEvent `usePlausibleEvent`} hook.**
|
||||
*
|
||||
* @remarks
|
||||
* If any of the following conditions are met, this will return and no data will be submitted:
|
||||
*
|
||||
* * If the app is in debug/development mode
|
||||
* * If a telemetry override is present, but it is not true
|
||||
* * If no telemetry override is present, and telemetry sharing is not true
|
||||
*
|
||||
* @privateRemarks
|
||||
* Telemetry sharing settings are never matched to `=== false`, but to `!== true` instead.
|
||||
* This means we can always guarantee that **nothing** will be sent unless the user
|
||||
* explicitly allows it.
|
||||
*
|
||||
* @see {@link https://plausible.io/docs/custom-event-goals Custom events}
|
||||
* @see {@link https://plausible-tracker.netlify.app/#tracking-custom-events-and-goals Tracking custom events}
|
||||
*/
|
||||
const submitPlausibleEvent = async ({ event, debugState, ...props }: SubmitEventProps) => {
|
||||
if (debugState.enabled && debugState.shareTelemetry !== true) return;
|
||||
if (
|
||||
'plausibleOptions' in event && 'telemetryOverride' in event.plausibleOptions
|
||||
? event.plausibleOptions.telemetryOverride !== true
|
||||
: props.shareTelemetry !== true
|
||||
)
|
||||
return;
|
||||
|
||||
const fullEvent: PlausibleTrackerEvent = {
|
||||
eventName: event.type,
|
||||
props: {
|
||||
platform: props.platformType === 'tauri' ? 'desktop' : props.platformType,
|
||||
version: Version,
|
||||
debug: debugState.enabled
|
||||
},
|
||||
options: {
|
||||
deviceWidth: props.screenWidth ?? window.screen.width,
|
||||
// referrer: '', // TODO(brxken128): see if we could have this blank to prevent accidental IP logging
|
||||
...('plausibleOptions' in event ? event.plausibleOptions : undefined)
|
||||
},
|
||||
callback: debugState.telemetryLogging
|
||||
? () => {
|
||||
const { callback: _, ...event } = fullEvent;
|
||||
console.log(event);
|
||||
}
|
||||
: undefined
|
||||
};
|
||||
|
||||
PlausibleProvider.trackEvent(
|
||||
fullEvent.eventName,
|
||||
{
|
||||
props: fullEvent.props,
|
||||
callback: fullEvent.callback
|
||||
},
|
||||
fullEvent.options
|
||||
);
|
||||
};
|
||||
|
||||
interface UsePlausibleEventProps {
|
||||
/**
|
||||
* The current platform type. This should be the output of `usePlatform().platform`
|
||||
*
|
||||
* @see {@link PlatformType}
|
||||
*/
|
||||
platformType: PlatformType;
|
||||
}
|
||||
|
||||
interface EventSubmissionCallbackProps {
|
||||
/**
|
||||
* The plausible event to submit.
|
||||
*
|
||||
* @see {@link PlausibleEvent}
|
||||
*/
|
||||
event: PlausibleEvent;
|
||||
}
|
||||
|
||||
/**
|
||||
* A Plausible Analytics event submission hook.
|
||||
*
|
||||
* The returned callback should only be fired once,
|
||||
* in order to prevent our analytics from being flooded.
|
||||
*
|
||||
* Certain events provide functionality to override the clients's telemetry sharing configuration.
|
||||
* This is not to ignore the user's choice, but because it should **only** be used in contexts where
|
||||
* telemetry sharing must be allowed/denied via external means.
|
||||
*
|
||||
* @remarks
|
||||
* If any of the following conditions are met, this will return and no data will be submitted:
|
||||
*
|
||||
* * If the app is in debug/development mode
|
||||
* * If a telemetry override is present, but it is not true
|
||||
* * If no telemetry override is present, and telemetry sharing is not true
|
||||
*
|
||||
* @returns a callback that, once executed, will submit the desired event
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* const platform = usePlatform();
|
||||
* const submitPlausibleEvent = usePlausibleEvent({ platformType: platform.platform });
|
||||
*
|
||||
* const createLibrary = useBridgeMutation('library.create', {
|
||||
* onSuccess: (library) => {
|
||||
* submitPlausibleEvent({
|
||||
* event: {
|
||||
* type: 'libraryCreate'
|
||||
* }
|
||||
* });
|
||||
* }
|
||||
* });
|
||||
* ```
|
||||
*/
|
||||
export const usePlausibleEvent = ({ platformType }: UsePlausibleEventProps) => {
|
||||
const debugState = useDebugState();
|
||||
const shareTelemetry = useTelemetryState().shareTelemetry;
|
||||
const previousEvent = useRef({} as BasePlausibleEvent<string>);
|
||||
|
||||
return useCallback(
|
||||
async (props: EventSubmissionCallbackProps) => {
|
||||
if (previousEvent.current === props.event) return;
|
||||
else previousEvent.current = props.event;
|
||||
|
||||
submitPlausibleEvent({ debugState, shareTelemetry, platformType, ...props });
|
||||
},
|
||||
[debugState, platformType, shareTelemetry]
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* These rules will be matched as regular expressions via `.replace()`
|
||||
* in a `forEach` loop.
|
||||
*
|
||||
* If a rule matches, the expression will be replaced with the target value.
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* let path = "/ed0c715c-d095-4f6a-b83c-1d0b25cc89e7/location/1";
|
||||
* PageViewRegexRules.forEach((e) => (path = path.replace(e[0], e[1])));
|
||||
* assert(path === "/location");
|
||||
* ```
|
||||
*/
|
||||
const PageViewRegexRules: [RegExp, string][] = [
|
||||
/**
|
||||
* This is for removing the library UUID from the current path
|
||||
*/
|
||||
[RegExp('/[a-f0-9]{8}-?[a-f0-9]{4}-?4[a-f0-9]{3}-?[89ab][a-f0-9]{3}-?[a-f0-9]{12}'), ''],
|
||||
/**
|
||||
* This is for removing location IDs from the current path
|
||||
*/
|
||||
[RegExp('/location/[0-9]+'), '/location'],
|
||||
/**
|
||||
* This is for removing tag IDs from the current path
|
||||
*/
|
||||
[RegExp('/tag/[0-9]+'), '/tag'],
|
||||
/**
|
||||
* This is for removing location IDs from the current path, when in library settings (e.g. `/settings/library/locations/12`)
|
||||
*/
|
||||
[RegExp('/locations/[0-9]+'), '/locations']
|
||||
];
|
||||
|
||||
export interface PageViewMonitorProps {
|
||||
/**
|
||||
* This should be unsanitized, and should still contain
|
||||
* all dynamic parameters (such as the library UUID).
|
||||
*
|
||||
* Ideally, this should be the output of `useLocation().pathname`
|
||||
*
|
||||
* @see {@link PageViewRegexRules} for sanitization
|
||||
*/
|
||||
currentPath: string;
|
||||
/**
|
||||
* The current platform type. This should be the output of `usePlatform().platform`
|
||||
*
|
||||
* @see {@link PlatformType}
|
||||
*/
|
||||
platformType: PlatformType;
|
||||
}
|
||||
|
||||
/**
|
||||
* A Plausible Analytics `pageview` monitoring hook. It watches the router's current
|
||||
* path, and sends events if a change in the path is detected.
|
||||
*
|
||||
* Ideally this should be added to the app extremely early - the sooner the better.
|
||||
* This means we don't need as many hooks to cover the same amount of routes.
|
||||
*
|
||||
* For desktop/web, we use this hook in the `$libraryId` layout and it covers the
|
||||
* entire app (excluding onboarding, which should not be monitored).
|
||||
*
|
||||
* @remarks
|
||||
* If any of the following conditions are met, this will return and no data will be submitted:
|
||||
*
|
||||
* * If the app is in debug/development mode
|
||||
* * If telemetry sharing (sourced from the client configuration) is not true
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* usePlausiblePageViewMonitor({
|
||||
* currentPath: useLocation().pathname,
|
||||
* platformType: usePlatform().platform
|
||||
* });
|
||||
* ```
|
||||
*/
|
||||
export const usePlausiblePageViewMonitor = (props: PageViewMonitorProps) => {
|
||||
const plausibleEvent = usePlausibleEvent({ platformType: props.platformType });
|
||||
|
||||
let path = props.currentPath;
|
||||
PageViewRegexRules.forEach((e) => (path = path.replace(e[0], e[1])));
|
||||
|
||||
useEffect(() => {
|
||||
plausibleEvent({
|
||||
event: {
|
||||
type: 'pageview',
|
||||
plausibleOptions: { url: path }
|
||||
}
|
||||
});
|
||||
}, [path, plausibleEvent]);
|
||||
};
|
||||
@@ -1,13 +1,20 @@
|
||||
import { useSnapshot } from 'valtio';
|
||||
import { valtioPersist } from './util';
|
||||
|
||||
export const debugState = valtioPersist('sd-debugState', {
|
||||
export interface DebugState {
|
||||
enabled: boolean;
|
||||
rspcLogger: boolean;
|
||||
reactQueryDevtools: 'enabled' | 'disabled' | 'invisible';
|
||||
shareTelemetry: boolean; // used for sending telemetry even if the app is in debug mode, and ONLY if client settings also allow telemetry sharing
|
||||
telemetryLogging: boolean;
|
||||
}
|
||||
|
||||
export const debugState: DebugState = valtioPersist('sd-debugState', {
|
||||
enabled: globalThis.isDev,
|
||||
rspcLogger: false,
|
||||
reactQueryDevtools: (globalThis.isDev ? 'invisible' : 'enabled') as
|
||||
| 'enabled'
|
||||
| 'disabled'
|
||||
| 'invisible'
|
||||
reactQueryDevtools: globalThis.isDev ? 'invisible' : 'enabled',
|
||||
shareTelemetry: false,
|
||||
telemetryLogging: false
|
||||
});
|
||||
|
||||
export function useDebugState() {
|
||||
|
||||
@@ -2,3 +2,4 @@ export * from './debugState';
|
||||
export * from './themeStore';
|
||||
export * from './util';
|
||||
export * from './onboardingStore';
|
||||
export * from './telemetryState';
|
||||
|
||||
@@ -18,7 +18,7 @@ const onboardingStoreDefaults = {
|
||||
algorithm: 'XChaCha20Poly1305',
|
||||
hashingAlgorithm: 'Argon2id-s' as HashingAlgoSlug,
|
||||
passwordSetToken: null as string | null,
|
||||
shareTelemetryDataWithDevelopers: true,
|
||||
shareTelemetry: true,
|
||||
useCases: [] as UseCase[],
|
||||
grantedFullDiskAccess: false
|
||||
};
|
||||
|
||||
10
packages/client/src/stores/telemetryState.tsx
Normal file
10
packages/client/src/stores/telemetryState.tsx
Normal file
@@ -0,0 +1,10 @@
|
||||
import { useSnapshot } from 'valtio';
|
||||
import { valtioPersist } from '.';
|
||||
|
||||
export const telemetryStore = valtioPersist('sd-telemetryStore', {
|
||||
shareTelemetry: false // false by default, so functions cannot accidentally send data if the user has not decided
|
||||
});
|
||||
|
||||
export function useTelemetryState() {
|
||||
return useSnapshot(telemetryStore);
|
||||
}
|
||||
BIN
pnpm-lock.yaml
generated
BIN
pnpm-lock.yaml
generated
Binary file not shown.
Reference in New Issue
Block a user