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:
parent
dc2f611257
commit
10eb500360
@ -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>
|
||||||
|
@ -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, {
|
||||||
|
@ -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
|
||||||
|
@ -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>
|
||||||
|
);
|
||||||
|
};
|
@ -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(
|
||||||
|
6
frontend/src/contexts/PlausibleContext.ts
Normal file
6
frontend/src/contexts/PlausibleContext.ts
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
import { createContext } from 'react';
|
||||||
|
import Plausible from 'plausible-tracker';
|
||||||
|
|
||||||
|
export const PlausibleContext = createContext<ReturnType<
|
||||||
|
typeof Plausible
|
||||||
|
> | null>(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);
|
|
||||||
});
|
|
@ -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);
|
|
||||||
};
|
};
|
||||||
|
Loading…
Reference in New Issue
Block a user