Skip to content

Commit

Permalink
feat(analytics): background analytics event
Browse files Browse the repository at this point in the history
  • Loading branch information
kyranjamie committed Jul 31, 2024
1 parent 0608cb8 commit 967f7b1
Show file tree
Hide file tree
Showing 10 changed files with 117 additions and 4 deletions.
38 changes: 37 additions & 1 deletion src/app/common/app-analytics.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,21 @@
import { useEffect } from 'react';

import { z } from 'zod';

import { HIRO_API_BASE_URL_MAINNET, HIRO_API_BASE_URL_TESTNET } from '@leather.io/models';

import { IS_TEST_ENV, SEGMENT_WRITE_KEY } from '@shared/environment';
import { decorateAnalyticsEventsWithContext, initAnalytics } from '@shared/utils/analytics';
import {
analytics,
decorateAnalyticsEventsWithContext,
initAnalytics,
} from '@shared/utils/analytics';

import { store } from '@app/store';
import { selectWalletType } from '@app/store/common/wallet-type.selectors';
import { selectCurrentNetwork } from '@app/store/networks/networks.selectors';

import { useOnMount } from './hooks/use-on-mount';
import { flow, origin } from './initial-search-params';

const defaultStaticAnalyticContext = {
Expand Down Expand Up @@ -57,3 +64,32 @@ decorateAnalyticsEventsWithContext(() => ({
...defaultStaticAnalyticContext,
...getDerivedStateAnalyticsContext(),
}));

const analyticsQueueItemSchema = z.object({
eventName: z.string(),
properties: z.record(z.unknown()).optional(),
});

const analyicsQueueSchema = z.array(analyticsQueueItemSchema);

const analyticsEventKey = 'backgroundAnalyticsRequests';

export function useHandleQueuedBackgroundAnalytics() {
useOnMount(() => {
async function handleQueuedAnalytics() {
const queuedEventsStore = await chrome.storage.local.get(analyticsEventKey);

try {
const events = analyicsQueueSchema.parse(queuedEventsStore[analyticsEventKey] ?? []);
if (!events.length) return;
await chrome.storage.local.remove(analyticsEventKey);
await Promise.all(
events.map(({ eventName, properties }) => analytics.track(eventName, properties))
);
} catch (e) {
void analytics.track('background_analytics_schema_fail');
}
}
void handleQueuedAnalytics();
});
}
6 changes: 5 additions & 1 deletion src/app/features/container/container.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,10 @@ import { RouteUrls } from '@shared/route-urls';
import { closeWindow } from '@shared/utils';
import { analytics } from '@shared/utils/analytics';

import { useInitalizeAnalytics } from '@app/common/app-analytics';
import {
useHandleQueuedBackgroundAnalytics,
useInitalizeAnalytics,
} from '@app/common/app-analytics';
import { LoadingSpinner } from '@app/components/loading-spinner';
import { CurrentAccountAvatar } from '@app/features/current-account/current-account-avatar';
import { CurrentAccountName } from '@app/features/current-account/current-account-name';
Expand Down Expand Up @@ -60,6 +63,7 @@ export function Container() {
useOnSignOut(() => closeWindow());
useRestoreFormState();
useInitalizeAnalytics();
useHandleQueuedBackgroundAnalytics();

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

Expand Down
19 changes: 19 additions & 0 deletions src/background/background-analytics.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
// Segment/Mixpanel libraries are not compatible with extension background
// scripts. This function adds analytics requests to chrome.storage.local so
// that, when opened, an extension frame (that does support analyics) can read
// and fire the requests.
const queueStore = 'backgroundAnalyticsRequests';

export async function queueAnalyticsRequest(
eventName: string,
properties: Record<string, unknown> = {}
) {
const currentQueue = await chrome.storage.local.get(queueStore);
const queue = currentQueue[queueStore] ?? [];
return chrome.storage.local.set({
[queueStore]: [
...queue,
{ eventName, properties: { ...properties, backgroundQueuedMessage: true } },
],
});
}
16 changes: 16 additions & 0 deletions src/background/messaging/rpc-message-handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { RpcErrorCode } from '@btckit/types';

import { WalletRequests, makeRpcErrorResponse } from '@shared/rpc/rpc-methods';

import { queueAnalyticsRequest } from '@background/background-analytics';
import { rpcSignStacksTransaction } from '@background/messaging/rpc-methods/sign-stacks-transaction';

import { getTabIdFromPort } from './messaging-utils';
Expand Down Expand Up @@ -63,3 +64,18 @@ export async function rpcMessageHandler(message: WalletRequests, port: chrome.ru
break;
}
}

interface TrackRpcRequestSuccess {
endpoint: WalletRequests['method'];
}
export async function trackRpcRequestSuccess(args: TrackRpcRequestSuccess) {
return queueAnalyticsRequest('rpc_request_successful', { ...args });
}

interface TrackRpcRequestError {
endpoint: WalletRequests['method'];
error: string;
}
export async function trackRpcRequestError(args: TrackRpcRequestError) {
return queueAnalyticsRequest('rpc_request_error', { ...args });
}
3 changes: 3 additions & 0 deletions src/background/messaging/rpc-methods/get-addresses.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,13 @@ import {
makeSearchParamsWithDefaults,
triggerRequestWindowOpen,
} from '../messaging-utils';
import { trackRpcRequestSuccess } from '../rpc-message-handler';

export async function rpcGetAddresses(message: GetAddressesRequest, port: chrome.runtime.Port) {
const { urlParams, tabId } = makeSearchParamsWithDefaults(port, [['requestId', message.id]]);
const { id } = await triggerRequestWindowOpen(RouteUrls.RpcGetAddresses, urlParams);
void trackRpcRequestSuccess({ endpoint: message.method });

listenForPopupClose({
tabId,
id,
Expand Down
7 changes: 7 additions & 0 deletions src/background/messaging/rpc-methods/send-transfer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,12 +21,15 @@ import {
makeSearchParamsWithDefaults,
triggerRequestWindowOpen,
} from '../messaging-utils';
import { trackRpcRequestError, trackRpcRequestSuccess } from '../rpc-message-handler';

export async function rpcSendTransfer(
message: RpcRequest<'sendTransfer', RpcSendTransferParams | SendTransferRequestParams>,
port: chrome.runtime.Port
) {
if (isUndefined(message.params)) {
void trackRpcRequestError({ endpoint: 'sendTransfer', error: 'Undefined parameters' });

chrome.tabs.sendMessage(
getTabIdFromPort(port),
makeRpcErrorResponse('sendTransfer', {
Expand All @@ -43,6 +46,8 @@ export async function rpcSendTransfer(
: (message.params as RpcSendTransferParams);

if (!validateRpcSendTransferParams(params)) {
void trackRpcRequestError({ endpoint: 'sendTransfer', error: 'Invalid parameters' });

chrome.tabs.sendMessage(
getTabIdFromPort(port),
makeRpcErrorResponse('sendTransfer', {
Expand All @@ -56,6 +61,8 @@ export async function rpcSendTransfer(
return;
}

void trackRpcRequestSuccess({ endpoint: message.method });

const recipients: [string, string][] = params.recipients.map(({ address }) => [
'recipient',
address,
Expand Down
8 changes: 8 additions & 0 deletions src/background/messaging/rpc-methods/sign-message.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,11 @@ import {
makeSearchParamsWithDefaults,
triggerRequestWindowOpen,
} from '../messaging-utils';
import { trackRpcRequestError, trackRpcRequestSuccess } from '../rpc-message-handler';

export async function rpcSignMessage(message: SignMessageRequest, port: chrome.runtime.Port) {
if (isUndefined(message.params)) {
void trackRpcRequestError({ endpoint: 'signMessage', error: 'Undefined parameters' });
chrome.tabs.sendMessage(
getTabIdFromPort(port),
makeRpcErrorResponse('signMessage', {
Expand All @@ -32,6 +34,8 @@ export async function rpcSignMessage(message: SignMessageRequest, port: chrome.r
}

if (!validateRpcSignMessageParams(message.params)) {
void trackRpcRequestError({ endpoint: 'signMessage', error: 'Invalid parameters' });

chrome.tabs.sendMessage(
getTabIdFromPort(port),
makeRpcErrorResponse('signMessage', {
Expand All @@ -49,6 +53,8 @@ export async function rpcSignMessage(message: SignMessageRequest, port: chrome.r
(message.params as any).paymentType ?? 'p2wpkh';

if (!isSupportedMessageSigningPaymentType(paymentType)) {
void trackRpcRequestError({ endpoint: 'signMessage', error: 'Unsupported payment type' });

chrome.tabs.sendMessage(
getTabIdFromPort(port),
makeRpcErrorResponse('signMessage', {
Expand All @@ -63,6 +69,8 @@ export async function rpcSignMessage(message: SignMessageRequest, port: chrome.r
return;
}

void trackRpcRequestSuccess({ endpoint: message.method });

const requestParams: RequestParams = [
['message', message.params.message],
['network', (message.params as any).network ?? 'mainnet'],
Expand Down
11 changes: 9 additions & 2 deletions src/background/messaging/rpc-methods/sign-psbt.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import {
makeSearchParamsWithDefaults,
triggerRequestWindowOpen,
} from '../messaging-utils';
import { trackRpcRequestError, trackRpcRequestSuccess } from '../rpc-message-handler';

function validatePsbt(hex: string) {
try {
Expand All @@ -31,9 +32,10 @@ function validatePsbt(hex: string) {

export async function rpcSignPsbt(message: SignPsbtRequest, port: chrome.runtime.Port) {
if (isUndefined(message.params)) {
void trackRpcRequestError({ endpoint: message.method, error: 'Undefined parameters' });
chrome.tabs.sendMessage(
getTabIdFromPort(port),
makeRpcErrorResponse('signPsbt', {
makeRpcErrorResponse(message.method, {
id: message.id,
error: { code: RpcErrorCode.INVALID_REQUEST, message: 'Parameters undefined' },
})
Expand All @@ -42,9 +44,10 @@ export async function rpcSignPsbt(message: SignPsbtRequest, port: chrome.runtime
}

if (!validateRpcSignPsbtParams(message.params)) {
void trackRpcRequestError({ endpoint: message.method, error: 'Invalid parameters' });
chrome.tabs.sendMessage(
getTabIdFromPort(port),
makeRpcErrorResponse('signPsbt', {
makeRpcErrorResponse(message.method, {
id: message.id,
error: {
code: RpcErrorCode.INVALID_PARAMS,
Expand All @@ -56,6 +59,8 @@ export async function rpcSignPsbt(message: SignPsbtRequest, port: chrome.runtime
}

if (!validatePsbt(message.params.hex)) {
void trackRpcRequestError({ endpoint: message.method, error: 'Invalid PSBT' });

chrome.tabs.sendMessage(
getTabIdFromPort(port),
makeRpcErrorResponse('signPsbt', {
Expand Down Expand Up @@ -88,6 +93,8 @@ export async function rpcSignPsbt(message: SignPsbtRequest, port: chrome.runtime
requestParams.push(['signAtIndex', index.toString()])
);

void trackRpcRequestSuccess({ endpoint: message.method });

const { urlParams, tabId } = makeSearchParamsWithDefaults(port, requestParams);

const { id } = await triggerRequestWindowOpen(RouteUrls.RpcSignPsbt, urlParams);
Expand Down
5 changes: 5 additions & 0 deletions src/background/messaging/rpc-methods/sign-stacks-message.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,12 +17,14 @@ import {
makeSearchParamsWithDefaults,
triggerRequestWindowOpen,
} from '../messaging-utils';
import { trackRpcRequestError, trackRpcRequestSuccess } from '../rpc-message-handler';

export async function rpcSignStacksMessage(
message: SignStacksMessageRequest,
port: chrome.runtime.Port
) {
if (isUndefined(message.params)) {
void trackRpcRequestError({ endpoint: message.method, error: 'Undefined parameters' });
chrome.tabs.sendMessage(
getTabIdFromPort(port),
makeRpcErrorResponse('stx_signMessage', {
Expand All @@ -34,6 +36,7 @@ export async function rpcSignStacksMessage(
}

if (!validateRpcSignStacksMessageParams(message.params)) {
void trackRpcRequestError({ endpoint: message.method, error: 'Invalid parameters' });
chrome.tabs.sendMessage(
getTabIdFromPort(port),
makeRpcErrorResponse('stx_signMessage', {
Expand All @@ -47,6 +50,8 @@ export async function rpcSignStacksMessage(
return;
}

void trackRpcRequestSuccess({ endpoint: message.method });

const requestParams: RequestParams = [
['message', message.params.message],
['messageType', message.params.messageType],
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ import {
makeSearchParamsWithDefaults,
triggerRequestWindowOpen,
} from '../messaging-utils';
import { trackRpcRequestError, trackRpcRequestSuccess } from '../rpc-message-handler';

const MEMO_DESERIALIZATION_STUB = '\u0000';

Expand Down Expand Up @@ -114,6 +115,7 @@ export async function rpcSignStacksTransaction(
port: chrome.runtime.Port
) {
if (isUndefined(message.params)) {
void trackRpcRequestError({ endpoint: message.method, error: 'Undefined parameters' });
chrome.tabs.sendMessage(
getTabIdFromPort(port),
makeRpcErrorResponse('stx_signTransaction', {
Expand All @@ -125,6 +127,8 @@ export async function rpcSignStacksTransaction(
}

if (!validateRpcSignStacksTransactionParams(message.params)) {
void trackRpcRequestError({ endpoint: message.method, error: 'Invalid parameters' });

chrome.tabs.sendMessage(
getTabIdFromPort(port),
makeRpcErrorResponse('stx_signTransaction', {
Expand All @@ -139,6 +143,8 @@ export async function rpcSignStacksTransaction(
}

if (!validateStacksTransaction(message.params.txHex!)) {
void trackRpcRequestError({ endpoint: message.method, error: 'Invalid Stacks transaction' });

chrome.tabs.sendMessage(
getTabIdFromPort(port),
makeRpcErrorResponse('stx_signTransaction', {
Expand All @@ -160,6 +166,8 @@ export async function rpcSignStacksTransaction(
const isMultisig =
hashMode === AddressHashMode.SerializeP2SH || hashMode === AddressHashMode.SerializeP2WSH;

void trackRpcRequestSuccess({ endpoint: message.method });

const requestParams = [
['txHex', message.params.txHex],
['requestId', message.id],
Expand Down

0 comments on commit 967f7b1

Please sign in to comment.