Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Chore/track analytics consent screen view and selection rates #2784 #2793

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 1 addition & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -135,7 +135,7 @@
"@reach/utils": "0.15.3",
"@reach/visually-hidden": "0.15.2",
"@reduxjs/toolkit": "1.8.4",
"@segment/analytics-next": "1.31.1",
"@segment/analytics-next": "1.46.0",
"@sentry/react": "6.16.1",
"@sentry/tracing": "6.16.1",
"@stacks/auth": "5.0.1",
Expand Down Expand Up @@ -256,7 +256,6 @@
"@types/react-test-renderer": "17.0.1",
"@types/redux-persist": "4.3.1",
"@types/remote-redux-devtools": "0.5.5",
"@types/segment-analytics": "0.0.34",
"@types/styled-system__theme-get": "5.0.2",
"@types/valid-url": "1.0.3",
"@types/webextension-polyfill": "0.9.0",
Expand Down
26 changes: 22 additions & 4 deletions src/app/common/hooks/analytics/use-analytics.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,18 @@
import { useMemo } from 'react';
import { useLocation } from 'react-router-dom';

import { EventParams, PageParams } from '@segment/analytics-next/dist/pkg/core/arguments-resolver';
import {
EventParams,
PageParams,
} from '@segment/analytics-next/dist/types/core/arguments-resolver';

import { IS_TEST_ENV } from '@shared/environment';
import { logger } from '@shared/logger';
import { analytics } from '@shared/utils/analytics';

import { useWalletType } from '@app/common/use-wallet-type';
import { useCurrentNetworkState } from '@app/store/networks/networks.hooks';
import { useHasUserExplicitlyDeclinedAnalytics } from '@app/store/settings/settings.selectors';

const IGNORED_PATH_REGEXPS = [/^\/$/];

Expand All @@ -25,6 +29,8 @@ export function useAnalytics() {
const location = useLocation();
const { walletType } = useWalletType();

const hasDeclined = useHasUserExplicitlyDeclinedAnalytics();

return useMemo(() => {
const defaultProperties = {
network: currentNetwork.name.toLowerCase(),
Expand All @@ -45,8 +51,11 @@ export function useAnalytics() {
const opts = { ...defaultOptions, ...options };
logger.info(`Analytics page view: ${name}`, properties);

if (!analytics || IS_TEST_ENV) return;
if (!analytics) return;
if (hasDeclined) return;
if (IS_TEST_ENV) return;
if (typeof name === 'string' && isIgnoredPath(name)) return;

return analytics.page(category, name, prop, opts, ...rest).catch(logger.error);
},
async track(...args: EventParams) {
Expand All @@ -55,9 +64,18 @@ export function useAnalytics() {
const opts = { ...defaultOptions, ...options };
logger.info(`Analytics event: ${eventName}`, properties);

if (!analytics || IS_TEST_ENV) return;
if (!analytics) return;
if (hasDeclined) return;
if (IS_TEST_ENV) return;

return analytics.track(eventName, prop, opts, ...rest).catch(logger.error);
},
};
}, [currentNetwork.chain.stacks.url, currentNetwork.name, location.pathname, walletType]);
}, [
currentNetwork.chain.stacks.url,
currentNetwork.name,
location.pathname,
walletType,
hasDeclined,
]);
}
4 changes: 1 addition & 3 deletions src/app/common/store-utils.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
import { QueryKey, hashQueryKey } from '@tanstack/react-query';
import hash from 'object-hash';

import { userHasAllowedDiagnosticsKey } from '@shared/utils/storage';

export function textToBytes(content: string) {
return new TextEncoder().encode(content);
}
Expand All @@ -16,7 +14,7 @@ export function makeLocalDataKey(params: QueryKey): string {
}

// LocalStorage keys kept across sign-in/signout sessions
const PERSISTENT_LOCAL_DATA: string[] = [userHasAllowedDiagnosticsKey];
const PERSISTENT_LOCAL_DATA: string[] = [];

export function partiallyClearLocalStorage() {
const backup = PERSISTENT_LOCAL_DATA.map((key: string) => [key, localStorage.getItem(key)]);
Expand Down
4 changes: 2 additions & 2 deletions src/app/common/theme-provider.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { createContext, useContext, useEffect, useState } from 'react';

import { store } from '@app/store';
import { userSelectedThemeActions } from '@app/store/settings/settings.actions';
import { settingsActions } from '@app/store/settings/settings.actions';
import { useUserSelectedTheme } from '@app/store/settings/settings.selectors';

export const themeLabelMap = {
Expand Down Expand Up @@ -38,7 +38,7 @@ function getComputedTheme(userSelectedTheme: UserSelectedTheme): ComputedTheme {
}

function setUserSelectedTheme(theme: UserSelectedTheme) {
store.dispatch(userSelectedThemeActions.setUserSelectedTheme(theme));
store.dispatch(settingsActions.setUserSelectedTheme(theme));
}

interface ThemeSwitcherProviderProps {
Expand Down
3 changes: 1 addition & 2 deletions src/app/index.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import ReactDOM from 'react-dom';

import { InternalMethods } from '@shared/message-types';
import { initSegment, initSentry } from '@shared/utils/analytics';
import { initSentry } from '@shared/utils/analytics';
import { warnUsersAboutDevToolsDangers } from '@shared/utils/dev-tools-warning-log';

import { persistAndRenderApp } from '@app/common/persistence';
Expand All @@ -11,7 +11,6 @@ import { store } from './store';
import { inMemoryKeyActions } from './store/in-memory-key/in-memory-key.actions';

initSentry();
void initSegment();
warnUsersAboutDevToolsDangers();

declare global {
Expand Down
7 changes: 3 additions & 4 deletions src/app/pages/allow-diagnostics/allow-diagnostics-layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,11 +26,10 @@ const ReasonToAllowDiagnostics: FC<ReasonToAllowDiagnosticsProps> = ({ text }) =

interface AllowDiagnosticsLayoutProps {
onUserAllowDiagnostics(): void;
onUserDenyDiagnosticsPermissions(): void;
onUserDenyDiagnostics(): void;
}
export function AllowDiagnosticsLayout(props: AllowDiagnosticsLayoutProps) {
const { onUserAllowDiagnostics, onUserDenyDiagnosticsPermissions } = props;

const { onUserAllowDiagnostics, onUserDenyDiagnostics } = props;
return (
<CenteredPageContainer>
<Stack
Expand Down Expand Up @@ -67,7 +66,7 @@ export function AllowDiagnosticsLayout(props: AllowDiagnosticsLayoutProps) {
fontSize="14px"
mode="tertiary"
ml="base"
onClick={() => onUserDenyDiagnosticsPermissions()}
onClick={() => onUserDenyDiagnostics()}
type="button"
variant="link"
data-testid={OnboardingSelectors.AnalyticsDenyBtn}
Expand Down
43 changes: 27 additions & 16 deletions src/app/pages/allow-diagnostics/allow-diagnostics.tsx
Original file line number Diff line number Diff line change
@@ -1,38 +1,49 @@
import { useCallback } from 'react';
import { useNavigate } from 'react-router-dom';
import { useCallback, useEffect } from 'react';
import { useDispatch } from 'react-redux';
import { useLocation, useNavigate } from 'react-router-dom';

import { RouteUrls } from '@shared/route-urls';
import { initSegment, initSentry } from '@shared/utils/analytics';

import { useAnalytics } from '@app/common/hooks/analytics/use-analytics';
import { useRouteHeader } from '@app/common/hooks/use-route-header';
import { Header } from '@app/components/header';
import { useHasAllowedDiagnostics } from '@app/store/onboarding/onboarding.hooks';
import { settingsActions } from '@app/store/settings/settings.actions';

import { AllowDiagnosticsLayout } from './allow-diagnostics-layout';

export const AllowDiagnosticsPage = () => {
const navigate = useNavigate();
const [, setHasAllowedDiagnostics] = useHasAllowedDiagnostics();
const dispatch = useDispatch();
const analytics = useAnalytics();
const { pathname } = useLocation();

useEffect(() => void analytics.page('view', `${pathname}`), [analytics, pathname]);

useRouteHeader(<Header hideActions />);

const goToOnboardingAndSetDiagnosticsPermissionTo = useCallback(
(areDiagnosticsAllowed: boolean | undefined) => {
if (typeof areDiagnosticsAllowed === undefined) return;
setHasAllowedDiagnostics(areDiagnosticsAllowed);
if (areDiagnosticsAllowed) {
initSentry();
void initSegment();
}
const setDiagnosticsPermissionsAndGoToOnboarding = useCallback(
(areDiagnosticsAllowed: boolean) => {
dispatch(settingsActions.setHasAllowedAnalytics(areDiagnosticsAllowed));

navigate(RouteUrls.Onboarding);
},
[navigate, setHasAllowedDiagnostics]
[navigate, dispatch]
);

return (
<AllowDiagnosticsLayout
onUserDenyDiagnosticsPermissions={() => goToOnboardingAndSetDiagnosticsPermissionTo(false)}
onUserAllowDiagnostics={() => goToOnboardingAndSetDiagnosticsPermissionTo(true)}
onUserDenyDiagnostics={() => {
void analytics.track('respond_diagnostics_consent', {
areDiagnosticsAllowed: false,
});
setDiagnosticsPermissionsAndGoToOnboarding(false);
}}
onUserAllowDiagnostics={() => {
void analytics.track('respond_diagnostics_consent', {
areDiagnosticsAllowed: true,
});
setDiagnosticsPermissionsAndGoToOnboarding(true);
}}
/>
);
};
6 changes: 3 additions & 3 deletions src/app/pages/onboarding/welcome/welcome.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,12 @@ import { useRouteHeader } from '@app/common/hooks/use-route-header';
import { doesBrowserSupportWebUsbApi, whenPageMode } from '@app/common/utils';
import { openIndexPageInNewTab } from '@app/common/utils/open-in-new-tab';
import { Header } from '@app/components/header';
import { useHasAllowedDiagnostics } from '@app/store/onboarding/onboarding.hooks';
import { useHasUserRespondedToAnalyticsConsent } from '@app/store/settings/settings.selectors';

import { WelcomeLayout } from './welcome.layout';

export const WelcomePage = memo(() => {
const [hasAllowedDiagnostics] = useHasAllowedDiagnostics();
const hasResponded = useHasUserRespondedToAnalyticsConsent();
const navigate = useNavigate();
const { decodedAuthRequest } = useOnboardingState();
const analytics = useAnalytics();
Expand All @@ -36,7 +36,7 @@ export const WelcomePage = memo(() => {
}, [keyActions, analytics, decodedAuthRequest, navigate]);

useEffect(() => {
if (hasAllowedDiagnostics === undefined) navigate(RouteUrls.RequestDiagnostics);
if (!hasResponded) navigate(RouteUrls.RequestDiagnostics);

return () => setIsGeneratingWallet(false);
// eslint-disable-next-line react-hooks/exhaustive-deps
Expand Down
27 changes: 22 additions & 5 deletions src/app/routes/app-routes.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -35,24 +35,21 @@ import { Unlock } from '@app/pages/unlock';
import { ViewSecretKey } from '@app/pages/view-secret-key/view-secret-key';
import { AccountGate } from '@app/routes/account-gate';
import { useHasStateRehydrated } from '@app/store';
import { useHasUserRespondedToAnalyticsConsent } from '@app/store/settings/settings.selectors';

import { useOnSignOut } from './hooks/use-on-sign-out';
import { useOnWalletLock } from './hooks/use-on-wallet-lock';
import { OnboardingGate } from './onboarding-gate';

export function AppRoutes() {
function AppRoutesAfterUserHasConsented() {
const { pathname } = useLocation();
const navigate = useNavigate();
const analytics = useAnalytics();

useOnWalletLock(() => navigate(RouteUrls.Unlock));
useOnSignOut(() => window.close());

useEffect(() => void analytics.page('view', `${pathname}`), [analytics, pathname]);

const hasStateRehydrated = useHasStateRehydrated();
if (!hasStateRehydrated) return <LoadingSpinner />;

const settingsModalRoutes = (
<>
<Route path={RouteUrls.SignOutConfirm} element={<SignOutConfirmDrawer />} />
Expand Down Expand Up @@ -201,3 +198,23 @@ export function AppRoutes() {
</Routes>
);
}

function AppRoutesBeforeUserHasConsented() {
return (
<Routes>
<Route path={RouteUrls.RequestDiagnostics} element={<AllowDiagnosticsPage />} />
<Route path="*" element={<Navigate replace to={RouteUrls.RequestDiagnostics} />} />
</Routes>
);
}

export function AppRoutes() {
const hasStateRehydrated = useHasStateRehydrated();
const hasResponded = useHasUserRespondedToAnalyticsConsent();

if (!hasStateRehydrated) return <LoadingSpinner />;

if (!hasResponded) return <AppRoutesBeforeUserHasConsented />;

return <AppRoutesAfterUserHasConsented />;
}
6 changes: 1 addition & 5 deletions src/app/store/onboarding/onboarding.hooks.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { useAtom } from 'jotai';
import { useAtomValue } from 'jotai/utils';

import { hasAllowedDiagnosticsState, secretKeyState, seedInputErrorState } from './onboarding';
import { secretKeyState, seedInputErrorState } from './onboarding';

export function useSeedInputErrorState() {
return useAtom(seedInputErrorState);
Expand All @@ -10,7 +10,3 @@ export function useSeedInputErrorState() {
export function useSecretKeyState() {
return useAtomValue(secretKeyState);
}

export function useHasAllowedDiagnostics() {
return useAtom(hasAllowedDiagnosticsState);
}
8 changes: 0 additions & 8 deletions src/app/store/onboarding/onboarding.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,4 @@
import { atom } from 'jotai';
import { atomWithStorage } from 'jotai/utils';

import { userHasAllowedDiagnosticsKey } from '@shared/utils/storage';

export const seedInputErrorState = atom<string | undefined>(undefined);
export const secretKeyState = atom(null);

export const hasAllowedDiagnosticsState = atomWithStorage<boolean | undefined>(
userHasAllowedDiagnosticsKey,
undefined
);
2 changes: 1 addition & 1 deletion src/app/store/settings/settings.actions.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
import { settingsSlice } from './settings.slice';

export const userSelectedThemeActions = settingsSlice.actions;
export const settingsActions = settingsSlice.actions;
16 changes: 15 additions & 1 deletion src/app/store/settings/settings.selectors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,21 @@ import { RootState } from '@app/store';
const selectSettings = (state: RootState) => state.settings;

const selectUserSelectedTheme = createSelector(selectSettings, state => state.userSelectedTheme);

export function useUserSelectedTheme() {
return useSelector(selectUserSelectedTheme);
}

const selectHasUserExplicitlyDeclinedAnalytics = createSelector(
selectSettings,
state => state.hasAllowedAnalytics === false
);
export function useHasUserExplicitlyDeclinedAnalytics() {
return useSelector(selectHasUserExplicitlyDeclinedAnalytics);
}
const selectHasUserRespondedToAnalyticsConsent = createSelector(
selectSettings,
state => state.hasAllowedAnalytics !== null
);
export function useHasUserRespondedToAnalyticsConsent() {
return useSelector(selectHasUserRespondedToAnalyticsConsent);
}
6 changes: 6 additions & 0 deletions src/app/store/settings/settings.slice.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,15 @@ import { PayloadAction, createSlice } from '@reduxjs/toolkit';

import { UserSelectedTheme } from '@app/common/theme-provider';

type HasAcceptedAnalytics = null | boolean;
interface InitialState {
userSelectedTheme: UserSelectedTheme;
hasAllowedAnalytics: HasAcceptedAnalytics;
}

const initialState: InitialState = {
userSelectedTheme: 'system',
hasAllowedAnalytics: null,
};

export const settingsSlice = createSlice({
Expand All @@ -17,5 +20,8 @@ export const settingsSlice = createSlice({
setUserSelectedTheme(state, action: PayloadAction<UserSelectedTheme>) {
state.userSelectedTheme = action.payload;
},
setHasAllowedAnalytics(state, action: PayloadAction<boolean>) {
state.hasAllowedAnalytics = action.payload;
},
},
});
2 changes: 1 addition & 1 deletion src/shared/actions/finalize-auth-response.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ export function finalizeAuthResponse({
const origin = new URL(requestingOrigin);

if (redirectUri.hostname !== origin.hostname) {
void analytics.track('auth_response_with_illegal_redirect_uri');
analytics?.track('auth_response_with_illegal_redirect_uri');
throw new Error('Cannot redirect to a different domain than the one requesting');
}

Expand Down
2 changes: 1 addition & 1 deletion src/shared/actions/finalize-tx-signature.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ export function finalizeTxSignature({ requestPayload, data, tabId }: FinalizeTxS
// My own testing shows that `sendMessage` doesn't throw yet users have
// reported these errors. Tracking here to see if we are able to detect this
// happening.
void analytics.track('finalize_tx_signature_error_thrown', { data: e });
analytics?.track('finalize_tx_signature_error_thrown', { data: e });
logger.error('Error in finalising tx signature', e);
}
}
Loading