Skip to content

Commit

Permalink
feat: add segment integration
Browse files Browse the repository at this point in the history
closes #928
  • Loading branch information
beguene authored and kyranjamie committed Dec 14, 2022
1 parent 73456ea commit 2e72a3c
Show file tree
Hide file tree
Showing 17 changed files with 622 additions and 15 deletions.
2 changes: 2 additions & 0 deletions .github/workflows/debug-build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ jobs:

env:
STX_NETWORK: ${{ matrix.stx_network }}
SENTRY_DSN: ${{ secrets.SENTRY_DSN }}
SEGMENT_WRITE_KEY: ${{ secrets.SEGMENT_WRITE_KEY }}

strategy:
matrix:
Expand Down
1 change: 1 addition & 0 deletions .github/workflows/publish-version.yml
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,7 @@ jobs:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
NODE_ENV: production
SENTRY_DSN: ${{ secrets.SENTRY_DSN }}
SEGMENT_WRITE_KEY: ${{ secrets.SEGMENT_WRITE_KEY }}
CSC_LINK: ${{ secrets[matrix.CSC_LINK_SECRET_NAME] }}
CSC_KEY_PASSWORD: ${{ secrets[matrix.CSC_KEY_PASSWORD_SECRET_NAME] }}
APPLE_ID: ${{ secrets.APPLE_ID }}
Expand Down
30 changes: 30 additions & 0 deletions app/hooks/use-analytics.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { EventParams } from '@segment/analytics-next/dist/pkg/core/arguments-resolver';
import { useHasUserGivenDiagnosticPermissions } from '@store/settings';
import { getAnalytics } from '@utils/init-segment';
import packageJson from '../../package.json';
import { NETWORK } from '@constants/index';

export function useAnalytics() {
const diagnosticsEnabled = useHasUserGivenDiagnosticPermissions();
const analytics = getAnalytics();
const defaultProperties = {
version: packageJson.version,
network: NETWORK,
};

const defaultOptions = {
context: { ip: '0.0.0.0' },
};

return {
track: async (...args: EventParams) => {
if (!analytics || !diagnosticsEnabled) return;
const [eventName, properties, options, ...rest] = args;

const prop = { ...defaultProperties, ...properties };
const opts = { ...defaultOptions, ...options };

return analytics.track(eventName, prop, opts, ...rest).catch(console.error);
},
};
}
8 changes: 7 additions & 1 deletion app/modals/receive-stx/components/address-displayer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { Box, Button, color, Flex, Text, useClipboard } from '@stacks/ui';
import { ExchangeWithdrawalWarning } from '@components/testnet/exchange-withdrawal-warning';
import { isTestnet } from '@utils/network-utils';
import { HomeSelectors } from 'app/tests/features/home.selectors';
import { useAnalytics } from '@hooks/use-analytics';

interface AddressDisplayerProps {
address: string;
Expand All @@ -12,6 +13,11 @@ export const AddressDisplayer: FC<AddressDisplayerProps> = props => {
const { address } = props;

const { hasCopied, onCopy } = useClipboard(address);
const analytics = useAnalytics();
const onCopyTracked = () => {
void analytics.track('copy_address_to_clipboard');
onCopy();
};

return (
<Box>
Expand Down Expand Up @@ -43,7 +49,7 @@ export const AddressDisplayer: FC<AddressDisplayerProps> = props => {
You do not need a memo to receive STX from elsewhere, such as a cryptocurrency exchange
</Text>
<Flex justifyContent="center" mt="base-tight" mb="tight">
<Button variant="link" onClick={onCopy}>
<Button variant="link" onClick={onCopyTracked}>
{hasCopied ? 'Copied' : 'Copy address'}
</Button>
</Flex>
Expand Down
5 changes: 4 additions & 1 deletion app/modals/reset-wallet/reset-wallet-modal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { TxModalFooter } from '../send-stx/send-stx-modal-layout';
import { clearDiskStorage } from '@utils/disk-store';
import { ModalHeader } from '@modals/components/modal-header';
import { SettingsSelectors } from 'app/tests/features/settings.selectors';
import { useAnalytics } from '@hooks/use-analytics';

interface ResetWalletModalProps {
isOpen: boolean;
Expand All @@ -16,6 +17,7 @@ export const ResetWalletModal: FC<ResetWalletModalProps> = ({ isOpen, onClose })
const timer = useRef<number>(0);
const cancelBtnRef = useRef<HTMLDivElement>();
const PANIC_CANCEL_TIME = 3500;
const analytics = useAnalytics();

const closeModal = useCallback(() => {
setWipingWallet(false);
Expand All @@ -30,12 +32,13 @@ export const ResetWalletModal: FC<ResetWalletModalProps> = ({ isOpen, onClose })

const resetWallet = useCallback(() => {
setWipingWallet(true);
void analytics.track('reset_wallet');
// Allow user to grace period to panic cancel operations
// Focusing cancel btn ensures any key press of: enter, space, esc
// will cancel the pending operation
cancelBtnRef.current?.focus();
timer.current = window.setTimeout(() => void clearStorage(), PANIC_CANCEL_TIME);
}, [cancelBtnRef, timer]);
}, [cancelBtnRef, timer, analytics]);

return (
<Modal isOpen={isOpen} handleClose={onClose} minWidth={['100%', '488px']}>
Expand Down
4 changes: 4 additions & 0 deletions app/modals/send-stx/send-stx-modal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ import { TransactionError } from '@modals/components/transaction-error';
import { ModalHeader } from '@modals/components/modal-header';
import { HomeSelectors } from 'app/tests/features/home.selectors';
import { useCalculateFee } from '@hooks/use-calculate-fee';
import { useAnalytics } from '@hooks/use-analytics';

interface TxModalProps {
balance: string;
Expand Down Expand Up @@ -78,6 +79,8 @@ export const SendStxModal: FC<TxModalProps> = ({ address, isOpen }) => {

const [txDetails, setTxDetails] = useState<TokenTransferOptions | null>(null);

const analytics = useAnalytics();

const totalIsMoreThanBalance = total.isGreaterThan(balance);

const exceedsMaxLengthBytes = (string: string, maxLengthBytes: number): boolean =>
Expand Down Expand Up @@ -185,6 +188,7 @@ export const SendStxModal: FC<TxModalProps> = ({ address, isOpen }) => {
};
useHotkeys('esc', closeModal);
const sendStx = (tx: StacksTransaction) => {
void analytics.track('broadcast_transaction');
broadcastTx({
async onSuccess(txId: string) {
await safeAwait(watchForNewTxToAppear({ txId, nodeUrl: stacksApi.baseUrl }));
Expand Down
8 changes: 7 additions & 1 deletion app/modals/send-stx/steps/transaction-form.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { FormikProps } from 'formik';
import { capitalize } from '@utils/capitalize';
import { toHumanReadableStx } from '@utils/unit-convert';
import { HomeSelectors } from 'app/tests/features/home.selectors';
import { useAnalytics } from '@hooks/use-analytics';
interface TxModalFormProps {
balance: string;
form: FormikProps<{ recipient: string; amount: string; memo: string; noMemoRequired: boolean }>;
Expand All @@ -16,6 +17,11 @@ interface TxModalFormProps {

export const TxModalForm: FC<TxModalFormProps> = props => {
const { balance, form, isCalculatingMaxSpend, feeEstimateError, onSendEntireBalance } = props;
const analytics = useAnalytics();
const onSendEntireBalanceTracked = () => {
void analytics.track('select_maximum_amount_for_send');
onSendEntireBalance();
};
return (
<Box mb="extra-loose">
<Flex flexDirection="column" alignItems="center" mt="48px">
Expand Down Expand Up @@ -76,7 +82,7 @@ export const TxModalForm: FC<TxModalFormProps> = props => {
top="22px"
style={{ position: 'absolute' }}
width="80px"
onClick={onSendEntireBalance}
onClick={onSendEntireBalanceTracked}
isLoading={isCalculatingMaxSpend}
>
Send max
Expand Down
12 changes: 10 additions & 2 deletions app/pages/home/home.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ import {
BalanceCard,
} from '@components/home';
import { HomeLayout } from './home-layout';
import { useAnalytics } from '@hooks/use-analytics';

export const Home: FC = () => {
const dispatch = useDispatch();
Expand Down Expand Up @@ -82,8 +83,15 @@ export const Home: FC = () => {

const { txs, pendingTxs, txCount, focusedTxIdRef, txDomNodeRefMap } = useTransactionList();

const analytics = useAnalytics();

if (!address) return <Spinner />;

const openTxInExplorerTracked = (txId: string) => {
void analytics.track('view_transaction');
return openTxInExplorer(txId);
};

const transactionList = (
<>
<TransactionList
Expand All @@ -99,7 +107,7 @@ export const Home: FC = () => {
activeTxIdRef={focusedTxIdRef}
key={pendingTxs.tx_id}
tx={pendingTxs}
onSelectTx={openTxInExplorer}
onSelectTx={openTxInExplorerTracked}
/>
))}
{txs.map(tx => (
Expand All @@ -108,7 +116,7 @@ export const Home: FC = () => {
activeTxIdRef={focusedTxIdRef}
key={tx.tx.tx_id}
txWithEvents={tx}
onSelectTx={openTxInExplorer}
onSelectTx={openTxInExplorerTracked}
/>
))}
</TransactionList>
Expand Down
5 changes: 5 additions & 0 deletions app/pages/onboarding/04-restore-wallet/restore-wallet.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import {
import { ExternalLink } from '@components/external-link';
import { parseSeedPhraseInput } from '@utils/parse-seed-phrase';
import { OnboardingSelector } from 'app/tests/features/onboarding.selectors';
import { useAnalytics } from '@hooks/use-analytics';

export const RestoreWallet: React.FC = () => {
useBackButton(routes.WELCOME);
Expand All @@ -29,6 +30,7 @@ export const RestoreWallet: React.FC = () => {
const [error, setError] = useState<string | null>(null);
const history = useHistory();
const dispatch = useDispatch();
const analytics = useAnalytics();

const handleMnemonicInput = (e: React.FormEvent<HTMLInputElement>) => {
if (hasSubmitted) setHasSubmitted(false);
Expand All @@ -44,17 +46,20 @@ export const RestoreWallet: React.FC = () => {

if (parsedMnemonic === null) {
setError('Unable to parse Secret Key input');
void analytics.track('submit_invalid_secret_key');
return;
}

const mnemonicLength = parsedMnemonic.split(' ').length;

if (mnemonicLength !== 12 && mnemonicLength !== 24) {
setError('The Hiro Wallet can be used with only 12 and 24-word Secret Keys');
void analytics.track('submit_invalid_secret_key');
return;
}

if (!validateMnemonic(parsedMnemonic)) {
void analytics.track('submit_invalid_secret_key');
setError('bip39error');
return;
}
Expand Down
8 changes: 7 additions & 1 deletion app/pages/onboarding/06-secret-key/secret-key.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import {
} from '@components/onboarding';
import { selectMnemonic } from '@store/keys/keys.reducer';
import { useBackButton } from '@hooks/use-back-url';
import { useAnalytics } from '@hooks/use-analytics';

export const SecretKey: React.FC = () => {
const history = useHistory();
Expand All @@ -28,6 +29,11 @@ export const SecretKey: React.FC = () => {
}

const { onCopy, hasCopied } = useClipboard(mnemonic);
const analytics = useAnalytics();
const onCopyTracked = () => {
void analytics.track('copy_secret_key_to_clipboard');
onCopy();
};

return (
<Onboarding>
Expand All @@ -40,7 +46,7 @@ export const SecretKey: React.FC = () => {
<Text textStyle="body.small" mt="loose" mx="loose" lineHeight="20px" display="block">
{mnemonic}
</Text>
<Button variant="link" mt="tight" onClick={onCopy}>
<Button variant="link" mt="tight" onClick={onCopyTracked}>
<Text textStyle="caption.medium" fontSize="12px">
Copy to clipboard
</Text>
Expand Down
8 changes: 7 additions & 1 deletion app/pages/onboarding/07-verify-key/verify-key.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,13 +17,15 @@ import {
OnboardingFooter,
OnboardingFooterLink,
} from '@components/onboarding';
import { useAnalytics } from '@hooks/use-analytics';

export const VerifyKey: React.FC = () => {
const history = useHistory();
useBackButton(routes.SECRET_KEY);
const mnemonic = useSelector(selectMnemonic);
const [inputMnemonic, setInputMnemonic] = useState('');
const [hasSubmitted, setHasSubmitted] = useState(false);
const analytics = useAnalytics();

const mnemonicCorrect = mnemonic === inputMnemonic;

Expand All @@ -36,7 +38,11 @@ export const VerifyKey: React.FC = () => {
const handleMnemonicValidation = (e: React.FormEvent) => {
e.preventDefault();
setHasSubmitted(true);
if (!mnemonicCorrect) return;
if (!mnemonicCorrect) {
void analytics.track('submit_invalid_secret_key');
return;
}
void analytics.track('submit_valid_secret_key');
history.push(routes.SET_PASSWORD);
};

Expand Down
5 changes: 5 additions & 0 deletions app/pages/onboarding/08-set-password/set-password.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import {
import { ExplainerTooltip } from '@components/tooltip';
import { blastUndoStackToRemovePasswordFromMemory } from '@utils/blast-undo-stack';
import { OnboardingSelector } from 'app/tests/features/onboarding.selectors';
import { useAnalytics } from '@hooks/use-analytics';

const weakPasswordWarningMessage = (result: ValidatedPassword) => {
if (result.isMnemonicPhrase) {
Expand Down Expand Up @@ -51,6 +52,7 @@ export const SetPassword: React.FC = () => {
const [hasSubmitted, setHasSubmitted] = useState(false);
const [btnDisabled, setBtnDisabled] = useState(false);
const [successfullyChosenStrongPass, setSuccessfullyChosenStrongPass] = useState(false);
const analytics = useAnalytics();

const handleBack = useCallback(() => {
history.goBack();
Expand All @@ -76,8 +78,11 @@ export const SetPassword: React.FC = () => {
if (result.meetsAllStrengthRequirements) {
setBtnDisabled(true);
setSuccessfullyChosenStrongPass(true);
void analytics.track('submit_valid_password');
blastUndoStackToRemovePasswordFromMemory(passwordInputRef.current);
dispatch(setSoftwareWallet({ password, history }));
} else {
void analytics.track('submit_invalid_password');
}
};

Expand Down
3 changes: 3 additions & 0 deletions app/routes.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import { ChooseStackingMethod } from './pages/start-stacking/start-stacking';
import { StackingDelegation } from './pages/stacking/delegated-stacking/pooled-stacking';
import { useHasUserGivenDiagnosticPermissions } from '@store/settings';
import { Diagnostics } from './pages/onboarding/01-diagnostics/diagnostics';
import { initSegment } from '@utils/init-segment';

let diagnosticsEnabled = false;

Expand All @@ -39,6 +40,8 @@ if (process.env.SENTRY_DSN) {
});
}

initSegment();

Sentry.setContext('network', { network: process.env.STX_NETWORK });

export const routerConfig = [
Expand Down
19 changes: 19 additions & 0 deletions app/utils/init-segment.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { Analytics, AnalyticsBrowser } from '@segment/analytics-next';

let analytics: Analytics | null = null;

export function initSegment() {
const writeKey = process.env.SEGMENT_WRITE_KEY;

if (writeKey) {
AnalyticsBrowser.standalone(writeKey)
.then(res => (analytics = res))
.catch(console.error);
} else {
console.warn('segment init aborted: No WRITE_KEY setup.');
}
}

export function getAnalytics() {
return analytics;
}
1 change: 1 addition & 0 deletions configs/webpack.config.base.js
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,7 @@ export default {

new webpack.EnvironmentPlugin({
STX_NETWORK: process.env.STX_NETWORK,
SEGMENT_WRITE_KEY: process.env.SEGMENT_WRITE_KEY || '',
SENTRY_DSN: process.env.SENTRY_DSN || '',
}),
],
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -209,6 +209,7 @@
"@hot-loader/react-dom": "17.0.1",
"@popperjs/core": "2.9.1",
"@reduxjs/toolkit": "1.5.1",
"@segment/analytics-next": "1.30.0",
"@sentry/electron": "2.5.4",
"@sentry/react": "6.13.2",
"@stacks/blockchain-api-client": "0.62.3",
Expand Down
Loading

0 comments on commit 2e72a3c

Please sign in to comment.