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:
parent
dc2f611257
commit
10eb500360
@ -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>
|
||||
|
@ -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, {
|
||||
|
@ -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
|
||||
|
@ -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 { 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(
|
||||
|
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 { 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 };
|
||||
};
|
||||
|
Loading…
Reference in New Issue
Block a user