Skip to content

Commit

Permalink
feat: capture analytics on consent screen only by default, closes #2784
Browse files Browse the repository at this point in the history
  • Loading branch information
edu-stx committed Nov 14, 2022
1 parent ea16169 commit 8f7d61d
Show file tree
Hide file tree
Showing 21 changed files with 167 additions and 140 deletions.
2 changes: 1 addition & 1 deletion 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
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
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
);
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);
}
}
3 changes: 0 additions & 3 deletions src/shared/environment.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,7 @@ export const BRANCH_NAME = process.env.GITHUB_HEAD_REF;
export const COINBASE_APP_ID = process.env.COINBASE_APP_ID ?? '';
export const COMMIT_SHA = process.env.GITHUB_SHA;
export const IS_DEV_ENV = process.env.WALLET_ENVIRONMENT === 'development';
export const IS_PROD_ENV =
process.env.WALLET_ENVIRONMENT === 'production' || process.env.WALLET_ENVIRONMENT === 'preview';
export const MOONPAY_API_KEY = process.env.MOONPAY_API_KEY ?? '';
export const IS_TEST_ENV = process.env.TEST_ENV === 'true';
export const SEGMENT_WRITE_KEY = process.env.SEGMENT_WRITE_KEY ?? '';
export const SENTRY_DSN = process.env.SENTRY_DSN ?? '';
export const TRANSAK_API_KEY = process.env.TRANSAK_API_KEY ?? '';
Loading

0 comments on commit 8f7d61d

Please sign in to comment.