1
0
mirror of https://github.com/Unleash/unleash.git synced 2025-04-19 01:17:18 +02:00

Custom event tracking (#2151)

* add plausible custom event tracking

* refactor: better comments for analytics tracking
This commit is contained in:
Tymoteusz Czech 2022-10-10 14:06:44 +02:00 committed by GitHub
parent dc2f611257
commit 10eb500360
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 145 additions and 86 deletions

View File

@ -9,13 +9,13 @@ import Loader from 'component/common/Loader/Loader';
import NotFound from 'component/common/NotFound/NotFound'; import NotFound from 'component/common/NotFound/NotFound';
import { ProtectedRoute } from 'component/common/ProtectedRoute/ProtectedRoute'; import { ProtectedRoute } from 'component/common/ProtectedRoute/ProtectedRoute';
import { SWRProvider } from 'component/providers/SWRProvider/SWRProvider'; import { SWRProvider } from 'component/providers/SWRProvider/SWRProvider';
import { PlausibleProvider } from 'component/providers/PlausibleProvider/PlausibleProvider';
import ToastRenderer from 'component/common/ToastRenderer/ToastRenderer'; import ToastRenderer from 'component/common/ToastRenderer/ToastRenderer';
import { routes } from 'component/menu/routes'; import { routes } from 'component/menu/routes';
import { useAuthDetails } from 'hooks/api/getters/useAuth/useAuthDetails'; import { useAuthDetails } from 'hooks/api/getters/useAuth/useAuthDetails';
import { useAuthUser } from 'hooks/api/getters/useAuth/useAuthUser'; import { useAuthUser } from 'hooks/api/getters/useAuth/useAuthUser';
import { SplashPageRedirect } from 'component/splash/SplashPageRedirect/SplashPageRedirect'; import { SplashPageRedirect } from 'component/splash/SplashPageRedirect/SplashPageRedirect';
import { useStyles } from './App.styles'; import { useStyles } from './App.styles';
import { usePlausibleTracker } from 'hooks/usePlausibleTracker';
import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig'; import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig';
export const App = () => { export const App = () => {
@ -24,7 +24,6 @@ export const App = () => {
const { user } = useAuthUser(); const { user } = useAuthUser();
const { isOss } = useUiConfig(); const { isOss } = useUiConfig();
const hasFetchedAuth = Boolean(authDetails || user); const hasFetchedAuth = Boolean(authDetails || user);
usePlausibleTracker();
const availableRoutes = isOss() const availableRoutes = isOss()
? routes.filter(route => !route.enterprise) ? routes.filter(route => !route.enterprise)
@ -34,6 +33,7 @@ export const App = () => {
<ErrorBoundary FallbackComponent={Error}> <ErrorBoundary FallbackComponent={Error}>
<SWRProvider> <SWRProvider>
<Suspense fallback={<Loader />}> <Suspense fallback={<Loader />}>
<PlausibleProvider>
<ConditionallyRender <ConditionallyRender
condition={!hasFetchedAuth} condition={!hasFetchedAuth}
show={<Loader />} show={<Loader />}
@ -73,6 +73,7 @@ export const App = () => {
</div> </div>
} }
/> />
</PlausibleProvider>
</Suspense> </Suspense>
</SWRProvider> </SWRProvider>
</ErrorBoundary> </ErrorBoundary>

View File

@ -17,6 +17,7 @@ import { useInviteTokenApi } from 'hooks/api/actions/useInviteTokenApi/useInvite
import { useInviteTokens } from 'hooks/api/getters/useInviteTokens/useInviteTokens'; import { useInviteTokens } from 'hooks/api/getters/useInviteTokens/useInviteTokens';
import { LinkField } from '../LinkField/LinkField'; import { LinkField } from '../LinkField/LinkField';
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender'; import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
import { usePlausibleTracker } from 'hooks/usePlausibleTracker';
interface ICreateInviteLinkProps {} interface ICreateInviteLinkProps {}
@ -64,12 +65,12 @@ export const InviteLink: VFC<ICreateInviteLinkProps> = () => {
const { data, loading } = useInviteTokens(); const { data, loading } = useInviteTokens();
const [inviteLink, setInviteLink] = useState(''); const [inviteLink, setInviteLink] = useState('');
const { mutate } = useSWRConfig(); const { mutate } = useSWRConfig();
const { trackEvent } = usePlausibleTracker();
const [expiry, setExpiry] = useState(expiryOptions[0].key); const [expiry, setExpiry] = useState(expiryOptions[0].key);
const [showDisableDialog, setDisableDialogue] = useState(false); const [showDisableDialog, setDisableDialogue] = useState(false);
const defaultToken = data?.tokens?.find(token => token.name === 'default'); const defaultToken = data?.tokens?.find(token => token.name === 'default');
const isUpdating = Boolean(defaultToken); const isUpdating = Boolean(defaultToken);
const formatApiCode = useFormatApiCode(isUpdating, expiry); const formatApiCode = useFormatApiCode(isUpdating, expiry);
const [isSending, setIsSending] = useState(false); const [isSending, setIsSending] = useState(false);
const { setToastApiError } = useToast(); const { setToastApiError } = useToast();
const { createToken, updateToken } = useInviteTokenApi(); const { createToken, updateToken } = useInviteTokenApi();
@ -78,6 +79,14 @@ export const InviteLink: VFC<ICreateInviteLinkProps> = () => {
e.preventDefault(); e.preventDefault();
setIsSending(true); setIsSending(true);
trackEvent('invite', {
props: {
eventType: isUpdating
? 'link update submitted'
: 'link created',
},
});
try { try {
if (isUpdating) { if (isUpdating) {
await updateToken(defaultToken!.secret, { await updateToken(defaultToken!.secret, {

View File

@ -1,4 +1,4 @@
import { VFC } from 'react'; import { useEffect, VFC } from 'react';
import { useNavigate } from 'react-router-dom'; import { useNavigate } from 'react-router-dom';
import { Box, Button, Typography } from '@mui/material'; import { Box, Button, Typography } from '@mui/material';
import useLoading from 'hooks/useLoading'; import useLoading from 'hooks/useLoading';
@ -8,11 +8,13 @@ import { LinkField } from '../LinkField/LinkField';
import { add, formatDistanceToNowStrict, isAfter, parseISO } from 'date-fns'; import { add, formatDistanceToNowStrict, isAfter, parseISO } from 'date-fns';
import { formatDateYMD } from 'utils/formatDate'; import { formatDateYMD } from 'utils/formatDate';
import { useLocationSettings } from 'hooks/useLocationSettings'; import { useLocationSettings } from 'hooks/useLocationSettings';
import { usePlausibleTracker } from 'hooks/usePlausibleTracker';
export const InviteLinkBar: VFC = () => { export const InviteLinkBar: VFC = () => {
const navigate = useNavigate(); const navigate = useNavigate();
const { data, loading } = useInviteTokens(); const { data, loading } = useInviteTokens();
const ref = useLoading(loading); const ref = useLoading(loading);
const { trackEvent } = usePlausibleTracker();
const inviteToken = const inviteToken =
data?.tokens?.find(token => token.name === 'default') ?? null; data?.tokens?.find(token => token.name === 'default') ?? null;
const inviteLink = inviteToken?.url; const inviteLink = inviteToken?.url;
@ -40,6 +42,18 @@ export const InviteLinkBar: VFC = () => {
</Typography> </Typography>
); );
const onInviteLinkActionClick = () => {
trackEvent('invite', {
props: {
eventType: Boolean(inviteLink)
? 'link bar action: edit'
: 'link bar action: create',
},
});
navigate('/admin/invite-link');
};
return ( return (
<Box <Box
sx={{ sx={{
@ -111,7 +125,7 @@ export const InviteLinkBar: VFC = () => {
> >
<Button <Button
variant="outlined" variant="outlined"
onClick={() => navigate('/admin/invite-link')} onClick={onInviteLinkActionClick}
data-loading data-loading
> >
{inviteLink ? 'Update' : 'Create'} invite link {inviteLink ? 'Update' : 'Create'} invite link

View File

@ -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<ReturnType<typeof Plausible> | 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 (
<PlausibleContext.Provider value={context}>
{children}
</PlausibleContext.Provider>
);
};

View File

@ -3,6 +3,7 @@ import { useNavigate } from 'react-router-dom';
import { Box, TextField, Typography } from '@mui/material'; import { Box, TextField, Typography } from '@mui/material';
import { CREATED, OK } from 'constants/statusCodes'; import { CREATED, OK } from 'constants/statusCodes';
import useToast from 'hooks/useToast'; import useToast from 'hooks/useToast';
import { usePlausibleTracker } from 'hooks/usePlausibleTracker';
import useResetPassword from 'hooks/api/getters/useResetPassword/useResetPassword'; import useResetPassword from 'hooks/api/getters/useResetPassword/useResetPassword';
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender'; import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
import { useUserInvite } from 'hooks/api/getters/useUserInvite/useUserInvite'; import { useUserInvite } from 'hooks/api/getters/useUserInvite/useUserInvite';
@ -39,11 +40,17 @@ export const NewUser = () => {
const { resetPassword, loading: isPasswordSubmitting } = const { resetPassword, loading: isPasswordSubmitting } =
useAuthResetPasswordApi(); useAuthResetPasswordApi();
const passwordDisabled = authDetails?.defaultHidden === true; const passwordDisabled = authDetails?.defaultHidden === true;
const { trackEvent } = usePlausibleTracker();
const onSubmitInvitedUser = async (password: string) => { const onSubmitInvitedUser = async (password: string) => {
try { try {
const res = await addUser(secret, { name, email, password }); const res = await addUser(secret, { name, email, password });
if (res.status === CREATED) { if (res.status === CREATED) {
trackEvent('invite', {
props: {
eventType: 'user created',
},
});
navigate('/login?invited=true'); navigate('/login?invited=true');
} else { } else {
setToastApiError( setToastApiError(

View File

@ -0,0 +1,6 @@
import { createContext } from 'react';
import Plausible from 'plausible-tracker';
export const PlausibleContext = createContext<ReturnType<
typeof Plausible
> | null>(null);

View File

@ -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);
});

View File

@ -1,37 +1,34 @@
import Plausible from 'plausible-tracker'; import { useCallback, useContext, useEffect } from 'react';
import { useEffect } from 'react'; import { PlausibleContext } from 'contexts/PlausibleContext';
import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig'; import { EventOptions, PlausibleOptions } from 'plausible-tracker';
import { IFlags } from 'interfaces/uiConfig';
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 = () => { export const usePlausibleTracker = () => {
const { uiConfig } = useUiConfig(); const plausible = useContext(PlausibleContext);
const enabled = enablePlausibleTracker(uiConfig.flags);
useEffect(() => { const trackEvent = useCallback(
if (enabled) { (
try { eventName: CustomEvents,
return initPlausibleTracker(); options?: EventOptions | undefined,
} catch (error) { eventData?: PlausibleOptions | undefined
console.warn(error); ) => {
if (plausible?.trackEvent) {
plausible.trackEvent(eventName, options, eventData);
} else {
if (options?.callback) {
options.callback();
} }
} }
}, [enabled]); },
}; [plausible]
);
const initPlausibleTracker = (): (() => void) => { return { trackEvent };
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<IFlags>): boolean => {
return Boolean(flags.T);
}; };