1
0
mirror of https://github.com/Unleash/unleash.git synced 2025-04-10 01:16:39 +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 { 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 = () => {
<ErrorBoundary FallbackComponent={Error}>
<SWRProvider>
<Suspense fallback={<Loader />}>
<ConditionallyRender
condition={!hasFetchedAuth}
show={<Loader />}
elseShow={
<div className={styles.container}>
<ToastRenderer />
<LayoutPicker>
<Routes>
{availableRoutes.map(route => (
<PlausibleProvider>
<ConditionallyRender
condition={!hasFetchedAuth}
show={<Loader />}
elseShow={
<div className={styles.container}>
<ToastRenderer />
<LayoutPicker>
<Routes>
{availableRoutes.map(route => (
<Route
key={route.path}
path={route.path}
element={
<ProtectedRoute
route={route}
/>
}
/>
))}
<Route
key={route.path}
path={route.path}
path="/"
element={
<ProtectedRoute
route={route}
<Navigate
to="/features"
replace
/>
}
/>
))}
<Route
path="/"
element={
<Navigate
to="/features"
replace
/>
}
/>
<Route
path="*"
element={<NotFound />}
/>
</Routes>
<FeedbackNPS openUrl="http://feedback.unleash.run" />
<SplashPageRedirect />
</LayoutPicker>
</div>
}
/>
<Route
path="*"
element={<NotFound />}
/>
</Routes>
<FeedbackNPS openUrl="http://feedback.unleash.run" />
<SplashPageRedirect />
</LayoutPicker>
</div>
}
/>
</PlausibleProvider>
</Suspense>
</SWRProvider>
</ErrorBoundary>

View File

@ -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<ICreateInviteLinkProps> = () => {
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<ICreateInviteLinkProps> = () => {
e.preventDefault();
setIsSending(true);
trackEvent('invite', {
props: {
eventType: isUpdating
? 'link update submitted'
: 'link created',
},
});
try {
if (isUpdating) {
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 { 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 = () => {
</Typography>
);
const onInviteLinkActionClick = () => {
trackEvent('invite', {
props: {
eventType: Boolean(inviteLink)
? 'link bar action: edit'
: 'link bar action: create',
},
});
navigate('/admin/invite-link');
};
return (
<Box
sx={{
@ -111,7 +125,7 @@ export const InviteLinkBar: VFC = () => {
>
<Button
variant="outlined"
onClick={() => navigate('/admin/invite-link')}
onClick={onInviteLinkActionClick}
data-loading
>
{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 { 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(

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 { 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<IFlags>): boolean => {
return Boolean(flags.T);
return { trackEvent };
};