From 10eb50036094a9ab8c2545ef503ebf8907901186 Mon Sep 17 00:00:00 2001 From: Tymoteusz Czech <2625371+Tymek@users.noreply.github.com> Date: Mon, 10 Oct 2022 14:06:44 +0200 Subject: [PATCH] Custom event tracking (#2151) * add plausible custom event tracking * refactor: better comments for analytics tracking --- frontend/src/component/App.tsx | 73 ++++++++++--------- .../admin/users/InviteLink/InviteLink.tsx | 11 ++- .../users/InviteLinkBar/InviteLinkBar.tsx | 18 ++++- .../PlausibleProvider/PlausibleProvider.tsx | 42 +++++++++++ .../src/component/user/NewUser/NewUser.tsx | 7 ++ frontend/src/contexts/PlausibleContext.ts | 6 ++ .../src/hooks/usePlausibleTracker.test.ts | 17 ----- frontend/src/hooks/usePlausibleTracker.ts | 57 +++++++-------- 8 files changed, 145 insertions(+), 86 deletions(-) create mode 100644 frontend/src/component/providers/PlausibleProvider/PlausibleProvider.tsx create mode 100644 frontend/src/contexts/PlausibleContext.ts delete mode 100644 frontend/src/hooks/usePlausibleTracker.test.ts diff --git a/frontend/src/component/App.tsx b/frontend/src/component/App.tsx index c4c037fe65..8c7776bc10 100644 --- a/frontend/src/component/App.tsx +++ b/frontend/src/component/App.tsx @@ -9,13 +9,13 @@ import Loader from 'component/common/Loader/Loader'; import NotFound from 'component/common/NotFound/NotFound'; import { ProtectedRoute } from 'component/common/ProtectedRoute/ProtectedRoute'; import { SWRProvider } from 'component/providers/SWRProvider/SWRProvider'; +import { PlausibleProvider } from 'component/providers/PlausibleProvider/PlausibleProvider'; import ToastRenderer from 'component/common/ToastRenderer/ToastRenderer'; import { routes } from 'component/menu/routes'; import { useAuthDetails } from 'hooks/api/getters/useAuth/useAuthDetails'; import { useAuthUser } from 'hooks/api/getters/useAuth/useAuthUser'; import { SplashPageRedirect } from 'component/splash/SplashPageRedirect/SplashPageRedirect'; import { useStyles } from './App.styles'; -import { usePlausibleTracker } from 'hooks/usePlausibleTracker'; import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig'; export const App = () => { @@ -24,7 +24,6 @@ export const App = () => { const { user } = useAuthUser(); const { isOss } = useUiConfig(); const hasFetchedAuth = Boolean(authDetails || user); - usePlausibleTracker(); const availableRoutes = isOss() ? routes.filter(route => !route.enterprise) @@ -34,45 +33,47 @@ export const App = () => { }> - } - elseShow={ - - - - - {availableRoutes.map(route => ( + + } + elseShow={ + + + + + {availableRoutes.map(route => ( + + } + /> + ))} } /> - ))} - - } - /> - } - /> - - - - - - } - /> + } + /> + + + + + + } + /> + diff --git a/frontend/src/component/admin/users/InviteLink/InviteLink.tsx b/frontend/src/component/admin/users/InviteLink/InviteLink.tsx index d7937a93b0..b12638425b 100644 --- a/frontend/src/component/admin/users/InviteLink/InviteLink.tsx +++ b/frontend/src/component/admin/users/InviteLink/InviteLink.tsx @@ -17,6 +17,7 @@ import { useInviteTokenApi } from 'hooks/api/actions/useInviteTokenApi/useInvite import { useInviteTokens } from 'hooks/api/getters/useInviteTokens/useInviteTokens'; import { LinkField } from '../LinkField/LinkField'; import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender'; +import { usePlausibleTracker } from 'hooks/usePlausibleTracker'; interface ICreateInviteLinkProps {} @@ -64,12 +65,12 @@ export const InviteLink: VFC = () => { const { data, loading } = useInviteTokens(); const [inviteLink, setInviteLink] = useState(''); const { mutate } = useSWRConfig(); + const { trackEvent } = usePlausibleTracker(); const [expiry, setExpiry] = useState(expiryOptions[0].key); const [showDisableDialog, setDisableDialogue] = useState(false); const defaultToken = data?.tokens?.find(token => token.name === 'default'); const isUpdating = Boolean(defaultToken); const formatApiCode = useFormatApiCode(isUpdating, expiry); - const [isSending, setIsSending] = useState(false); const { setToastApiError } = useToast(); const { createToken, updateToken } = useInviteTokenApi(); @@ -78,6 +79,14 @@ export const InviteLink: VFC = () => { e.preventDefault(); setIsSending(true); + trackEvent('invite', { + props: { + eventType: isUpdating + ? 'link update submitted' + : 'link created', + }, + }); + try { if (isUpdating) { await updateToken(defaultToken!.secret, { diff --git a/frontend/src/component/admin/users/InviteLinkBar/InviteLinkBar.tsx b/frontend/src/component/admin/users/InviteLinkBar/InviteLinkBar.tsx index 25ff0122f6..1733b68059 100644 --- a/frontend/src/component/admin/users/InviteLinkBar/InviteLinkBar.tsx +++ b/frontend/src/component/admin/users/InviteLinkBar/InviteLinkBar.tsx @@ -1,4 +1,4 @@ -import { VFC } from 'react'; +import { useEffect, VFC } from 'react'; import { useNavigate } from 'react-router-dom'; import { Box, Button, Typography } from '@mui/material'; import useLoading from 'hooks/useLoading'; @@ -8,11 +8,13 @@ import { LinkField } from '../LinkField/LinkField'; import { add, formatDistanceToNowStrict, isAfter, parseISO } from 'date-fns'; import { formatDateYMD } from 'utils/formatDate'; import { useLocationSettings } from 'hooks/useLocationSettings'; +import { usePlausibleTracker } from 'hooks/usePlausibleTracker'; export const InviteLinkBar: VFC = () => { const navigate = useNavigate(); const { data, loading } = useInviteTokens(); const ref = useLoading(loading); + const { trackEvent } = usePlausibleTracker(); const inviteToken = data?.tokens?.find(token => token.name === 'default') ?? null; const inviteLink = inviteToken?.url; @@ -40,6 +42,18 @@ export const InviteLinkBar: VFC = () => { ); + const onInviteLinkActionClick = () => { + trackEvent('invite', { + props: { + eventType: Boolean(inviteLink) + ? 'link bar action: edit' + : 'link bar action: create', + }, + }); + + navigate('/admin/invite-link'); + }; + return ( { > navigate('/admin/invite-link')} + onClick={onInviteLinkActionClick} data-loading > {inviteLink ? 'Update' : 'Create'} invite link diff --git a/frontend/src/component/providers/PlausibleProvider/PlausibleProvider.tsx b/frontend/src/component/providers/PlausibleProvider/PlausibleProvider.tsx new file mode 100644 index 0000000000..3022827eb8 --- /dev/null +++ b/frontend/src/component/providers/PlausibleProvider/PlausibleProvider.tsx @@ -0,0 +1,42 @@ +import { FC, useState, useEffect } from 'react'; +import Plausible from 'plausible-tracker'; +import { PlausibleContext } from 'contexts/PlausibleContext'; +import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig'; + +const PLAUSIBLE_UNLEASH_API_HOST = 'https://plausible.getunleash.io'; +const PLAUSIBLE_UNLEASH_DOMAIN = 'app.unleash-hosted.com'; +const LOCAL_TESTING = false; + +export const PlausibleProvider: FC = ({ children }) => { + const [context, setContext] = useState | null>( + null + ); + const { uiConfig } = useUiConfig(); + const isEnabled = Boolean(uiConfig?.flags?.T || LOCAL_TESTING); + + useEffect(() => { + if (isEnabled) { + try { + const plausible = Plausible({ + domain: LOCAL_TESTING + ? undefined + : PLAUSIBLE_UNLEASH_DOMAIN, + apiHost: LOCAL_TESTING + ? 'http://localhost:8000' + : PLAUSIBLE_UNLEASH_API_HOST, + trackLocalhost: true, + }); + setContext(() => plausible); + return plausible.enableAutoPageviews(); + } catch (error) { + console.warn(error); + } + } + }, [isEnabled]); + + return ( + + {children} + + ); +}; diff --git a/frontend/src/component/user/NewUser/NewUser.tsx b/frontend/src/component/user/NewUser/NewUser.tsx index 0ccb0516d5..f38bf0255d 100644 --- a/frontend/src/component/user/NewUser/NewUser.tsx +++ b/frontend/src/component/user/NewUser/NewUser.tsx @@ -3,6 +3,7 @@ import { useNavigate } from 'react-router-dom'; import { Box, TextField, Typography } from '@mui/material'; import { CREATED, OK } from 'constants/statusCodes'; import useToast from 'hooks/useToast'; +import { usePlausibleTracker } from 'hooks/usePlausibleTracker'; import useResetPassword from 'hooks/api/getters/useResetPassword/useResetPassword'; import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender'; import { useUserInvite } from 'hooks/api/getters/useUserInvite/useUserInvite'; @@ -39,11 +40,17 @@ export const NewUser = () => { const { resetPassword, loading: isPasswordSubmitting } = useAuthResetPasswordApi(); const passwordDisabled = authDetails?.defaultHidden === true; + const { trackEvent } = usePlausibleTracker(); const onSubmitInvitedUser = async (password: string) => { try { const res = await addUser(secret, { name, email, password }); if (res.status === CREATED) { + trackEvent('invite', { + props: { + eventType: 'user created', + }, + }); navigate('/login?invited=true'); } else { setToastApiError( diff --git a/frontend/src/contexts/PlausibleContext.ts b/frontend/src/contexts/PlausibleContext.ts new file mode 100644 index 0000000000..74b994c15e --- /dev/null +++ b/frontend/src/contexts/PlausibleContext.ts @@ -0,0 +1,6 @@ +import { createContext } from 'react'; +import Plausible from 'plausible-tracker'; + +export const PlausibleContext = createContext | null>(null); diff --git a/frontend/src/hooks/usePlausibleTracker.test.ts b/frontend/src/hooks/usePlausibleTracker.test.ts deleted file mode 100644 index 467bc4b4a8..0000000000 --- a/frontend/src/hooks/usePlausibleTracker.test.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { renderHook } from '@testing-library/react-hooks'; -import { - usePlausibleTracker, - enablePlausibleTracker, -} from 'hooks/usePlausibleTracker'; - -test('usePlausibleTracker', async () => { - const { result } = renderHook(() => usePlausibleTracker()); - expect(result.current).toBeUndefined(); -}); - -test('enablePlausibleTracker', async () => { - expect(enablePlausibleTracker({})).toEqual(false); - expect(enablePlausibleTracker({ SE: true })).toEqual(false); - expect(enablePlausibleTracker({ T: false })).toEqual(false); - expect(enablePlausibleTracker({ T: true })).toEqual(true); -}); diff --git a/frontend/src/hooks/usePlausibleTracker.ts b/frontend/src/hooks/usePlausibleTracker.ts index fc608fdffe..7186d7a316 100644 --- a/frontend/src/hooks/usePlausibleTracker.ts +++ b/frontend/src/hooks/usePlausibleTracker.ts @@ -1,37 +1,34 @@ -import Plausible from 'plausible-tracker'; -import { useEffect } from 'react'; -import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig'; -import { IFlags } from 'interfaces/uiConfig'; +import { useCallback, useContext, useEffect } from 'react'; +import { PlausibleContext } from 'contexts/PlausibleContext'; +import { EventOptions, PlausibleOptions } from 'plausible-tracker'; -const PLAUSIBLE_UNLEASH_API_HOST = 'https://plausible.getunleash.io'; -const PLAUSIBLE_UNLEASH_DOMAIN = 'app.unleash-hosted.com'; +/** + * Allowed event names. Makes it easy to remove, since TS will complain. + * Add those to Plausible as Custom event goals. + * @see https://plausible.io/docs/custom-event-goals#2-create-a-custom-event-goal-in-your-plausible-analytics-account + * @example `'download | 'invite' | 'signup'` + **/ +type CustomEvents = 'invite'; export const usePlausibleTracker = () => { - const { uiConfig } = useUiConfig(); - const enabled = enablePlausibleTracker(uiConfig.flags); + const plausible = useContext(PlausibleContext); - useEffect(() => { - if (enabled) { - try { - return initPlausibleTracker(); - } catch (error) { - console.warn(error); + const trackEvent = useCallback( + ( + eventName: CustomEvents, + options?: EventOptions | undefined, + eventData?: PlausibleOptions | undefined + ) => { + if (plausible?.trackEvent) { + plausible.trackEvent(eventName, options, eventData); + } else { + if (options?.callback) { + options.callback(); + } } - } - }, [enabled]); -}; + }, + [plausible] + ); -const initPlausibleTracker = (): (() => void) => { - const plausible = Plausible({ - domain: PLAUSIBLE_UNLEASH_DOMAIN, - apiHost: PLAUSIBLE_UNLEASH_API_HOST, - trackLocalhost: true, - }); - - return plausible.enableAutoPageviews(); -}; - -// Enable Plausible if we're on the Unleash SaaS domain. -export const enablePlausibleTracker = (flags: Partial): boolean => { - return Boolean(flags.T); + return { trackEvent }; };