Skip to content

Commit

Permalink
feat (live-15768): Data tracking in the send flow
Browse files Browse the repository at this point in the history
  • Loading branch information
fAnselmi-Ledger committed Feb 7, 2025
1 parent 4c07450 commit 7d8ff18
Show file tree
Hide file tree
Showing 8 changed files with 209 additions and 19 deletions.
5 changes: 5 additions & 0 deletions .changeset/green-cats-call.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"ledger-live-desktop": minor
---

Add data tracking in the send flow
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import { renderHook } from "tests/testUtils";
import { useTrackReceiveFlow, UseTrackReceiveFlow } from "./useTrackReceiveFlow";
import { track } from "../segment";
import { UserRefusedOnDevice } from "@ledgerhq/errors";
import { UserRefusedOnDevice, UserRefusedAddress } from "@ledgerhq/errors";
import { CONNECTION_TYPES } from "./variables";

jest.mock("../segment", () => ({
track: jest.fn(),
Expand Down Expand Up @@ -38,24 +39,26 @@ describe("useTrackReceiveFlow", () => {
"Open app denied",
expect.objectContaining({
deviceType: "europa",
connectionType: "BLE",
connectionType: CONNECTION_TYPES.BLE,
platform: "LLD",
page: "Receive",
}),
true,
);
});

it("should track 'Address confirmation rejected' when verifyAddressError has name 'UserRefusedAddress'", () => {
it("should track 'Address confirmation rejected' when verifyAddressError is an instance of UserRefusedAddress", () => {
const verifyAddressError = new UserRefusedAddress();

renderHook((props: UseTrackReceiveFlow) => useTrackReceiveFlow(props), {
initialProps: { ...defaultArgs, verifyAddressError: { name: "UserRefusedAddress" } },
initialProps: { ...defaultArgs, verifyAddressError },
});

expect(track).toHaveBeenCalledWith(
"Address confirmation rejected",
expect.objectContaining({
deviceType: "europa",
connectionType: "BLE",
connectionType: CONNECTION_TYPES.BLE,
platform: "LLD",
page: "Receive",
}),
Expand All @@ -72,7 +75,7 @@ describe("useTrackReceiveFlow", () => {
"Wrong device association",
expect.objectContaining({
deviceType: "europa",
connectionType: "BLE",
connectionType: CONNECTION_TYPES.BLE,
platform: "LLD",
page: "Receive",
}),
Expand All @@ -90,20 +93,21 @@ describe("useTrackReceiveFlow", () => {

it("should include correct connection type based on device.wired", () => {
const wiredDeviceMock = { ...deviceMock, wired: true };
const verifyAddressError = new UserRefusedAddress();

renderHook((props: UseTrackReceiveFlow) => useTrackReceiveFlow(props), {
initialProps: {
...defaultArgs,
device: wiredDeviceMock,
verifyAddressError: { name: "UserRefusedAddress" },
verifyAddressError,
},
});

expect(track).toHaveBeenCalledWith(
"Address confirmation rejected",
expect.objectContaining({
deviceType: "europa",
connectionType: "USB",
connectionType: CONNECTION_TYPES.USB,
platform: "LLD",
page: "Receive",
}),
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import { useEffect } from "react";
import { track } from "../segment";
import { Device } from "@ledgerhq/types-devices";
import { UserRefusedOnDevice } from "@ledgerhq/errors";
import { UserRefusedOnDevice, UserRefusedAddress } from "@ledgerhq/errors";
import { LedgerError } from "~/renderer/components/DeviceAction";
import { CONNECTION_TYPES } from "./variables";

export type UseTrackReceiveFlow = {
location: string | undefined;
Expand Down Expand Up @@ -53,7 +54,7 @@ export const useTrackReceiveFlow = ({

const defaultPayload = {
deviceType: device?.modelId,
connectionType: device?.wired ? "USB" : "BLE",
connectionType: device?.wired ? CONNECTION_TYPES.USB : CONNECTION_TYPES.BLE,
platform: "LLD",
page: "Receive",
};
Expand All @@ -62,20 +63,13 @@ export const useTrackReceiveFlow = ({
// user refused to open app
track("Open app denied", defaultPayload, isTrackingEnabled);
}
if (verifyAddressError?.name === "UserRefusedAddress") {
if ((verifyAddressError as unknown) instanceof UserRefusedAddress) {
// user refused to confirm address
track("Address confirmation rejected", defaultPayload, isTrackingEnabled);
}
if (inWrongDeviceForAccount) {
// device used is not associated with the account
track("Wrong device association", defaultPayload, isTrackingEnabled);
}
}, [
error,
location,
isTrackingEnabled,
device,
verifyAddressError?.name,
inWrongDeviceForAccount,
]);
}, [error, location, isTrackingEnabled, device, inWrongDeviceForAccount, verifyAddressError]);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
import { renderHook } from "tests/testUtils";
import { useTrackSendFlow, UseTrackSendFlow } from "./useTrackSendFlow";
import { track } from "../segment";
import { UserRefusedOnDevice } from "@ledgerhq/errors";
import { CONNECTION_TYPES, HOOKS_TRACKING_LOCATIONS } from "./variables";

jest.mock("../segment", () => ({
track: jest.fn(),
setAnalyticsFeatureFlagMethod: jest.fn(),
}));

describe("useTrackSendFlow", () => {
const deviceMock = {
modelId: "europa",
wired: false,
};

const defaultArgs: UseTrackSendFlow = {
location: HOOKS_TRACKING_LOCATIONS.sendModal,
device: deviceMock,
error: null,
inWrongDeviceForAccount: null,
isTrackingEnabled: true,
};

afterEach(() => {
jest.clearAllMocks();
});

it("should track 'Open app denied' when UserRefusedOnDevice error is thrown", () => {
const error = new UserRefusedOnDevice();

renderHook((props: UseTrackSendFlow) => useTrackSendFlow(props), {
initialProps: { ...defaultArgs, error },
});

expect(track).toHaveBeenCalledWith(
"Open app denied",
expect.objectContaining({
deviceType: "europa",
connectionType: CONNECTION_TYPES.BLE,
platform: "LLD",
page: "Send",
}),
true,
);
});

it("should track 'Wrong device association' when inWrongDeviceForAccount is provided", () => {
renderHook((props: UseTrackSendFlow) => useTrackSendFlow(props), {
initialProps: { ...defaultArgs, inWrongDeviceForAccount: { accountName: "Test Account" } },
});

expect(track).toHaveBeenCalledWith(
"Wrong device association",
expect.objectContaining({
deviceType: "europa",
connectionType: CONNECTION_TYPES.BLE,
platform: "LLD",
page: "Send",
}),
true,
);
});

it("should not track events if location is not 'Send Modal'", () => {
renderHook((props: UseTrackSendFlow) => useTrackSendFlow(props), {
initialProps: { ...defaultArgs, location: "Other Modal" },
});

expect(track).not.toHaveBeenCalled();
});

it("should include correct connection type based on device.wired", () => {
const wiredDeviceMock = { ...deviceMock, wired: true };

renderHook((props: UseTrackSendFlow) => useTrackSendFlow(props), {
initialProps: { ...defaultArgs, device: wiredDeviceMock, error: new UserRefusedOnDevice() },
});

expect(track).toHaveBeenCalledWith(
"Open app denied",
expect.objectContaining({
deviceType: "europa",
connectionType: CONNECTION_TYPES.USB,
platform: "LLD",
page: "Send",
}),
true,
);
});

it("should handle no tracking when isTrackingEnabled is false", () => {
renderHook((props: UseTrackSendFlow) => useTrackSendFlow(props), {
initialProps: { ...defaultArgs, isTrackingEnabled: false, error: new UserRefusedOnDevice() },
});

expect(track).toHaveBeenCalledWith(
"Open app denied",
expect.objectContaining({
deviceType: "europa",
connectionType: CONNECTION_TYPES.BLE,
platform: "LLD",
page: "Send",
}),
false,
);
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import { useEffect } from "react";
import { track } from "../segment";
import { Device } from "@ledgerhq/types-devices";
import { UserRefusedOnDevice } from "@ledgerhq/errors";
import { CONNECTION_TYPES, HOOKS_TRACKING_LOCATIONS } from "./variables";
import { LedgerError } from "~/renderer/components/DeviceAction";

export type UseTrackSendFlow = {
location: HOOKS_TRACKING_LOCATIONS.sendModal | undefined;
device: Device;
error:
| (LedgerError & {
name?: string;
managerAppName?: string;
})
| undefined
| null;
inWrongDeviceForAccount:
| {
accountName: string;
}
| null
| undefined;
isTrackingEnabled: boolean;
};

/**
* a custom hook to track events in the Send modal.
* tracks user interactions with the Send modal based on state changes and errors.
*
* @param location - current location in the app (expected "Send Modal" from HOOKS_TRACKING_LOCATIONS enum).
* @param device - the connected device information.
* @param error - current error state.
* @param inWrongDeviceForAccount - error from verifying address.
* @param isTrackingEnabled - flag indicating if tracking is enabled.
*/
export const useTrackSendFlow = ({
location,
device,
error = null,
inWrongDeviceForAccount = null,
isTrackingEnabled,
}: UseTrackSendFlow) => {
useEffect(() => {
if (location !== HOOKS_TRACKING_LOCATIONS.sendModal) return;

const defaultPayload = {
deviceType: device?.modelId,
connectionType: device?.wired ? CONNECTION_TYPES.USB : CONNECTION_TYPES.BLE,
platform: "LLD",
page: "Send",
};

if ((error as unknown) instanceof UserRefusedOnDevice) {
// user refused to open app
track("Open app denied", defaultPayload, isTrackingEnabled);
}
if (inWrongDeviceForAccount) {
// device used is not associated with the account
track("Wrong device association", defaultPayload, isTrackingEnabled);
}
}, [error, location, isTrackingEnabled, device, inWrongDeviceForAccount]);
};
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,7 @@ export enum CONNECTION_TYPES {
USB = "USB",
BLE = "BLE",
}

export enum HOOKS_TRACKING_LOCATIONS {
sendModal = "Send Modal",
}
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,7 @@ import { walletSelector } from "~/renderer/reducers/wallet";
import { useTrackManagerSectionEvents } from "~/renderer/analytics/hooks/useTrackManagerSectionEvents";
import { useTrackReceiveFlow } from "~/renderer/analytics/hooks/useTrackReceiveFlow";
import { useTrackAddAccountModal } from "~/renderer/analytics/hooks/useTrackAddAccountModal";
import { useTrackSendFlow } from "~/renderer/analytics/hooks/useTrackSendFlow";

export type LedgerError = InstanceType<LedgerErrorConstructor<{ [key: string]: unknown }>>;

Expand Down Expand Up @@ -268,6 +269,14 @@ export const DeviceActionDefaultRendering = <R, H extends States, P>({
isLocked,
});

useTrackSendFlow({
location,
device,
error,
inWrongDeviceForAccount,
isTrackingEnabled: useSelector(trackingEnabledSelector),
});

const type = useTheme().colors.palette.type;

const modelId = device ? device.modelId : overridesPreferredDeviceModel || preferredDeviceModel;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import { DeviceBlocker } from "~/renderer/components/DeviceAction/DeviceBlocker"
import { closeModal } from "~/renderer/actions/modals";
import { mevProtectionSelector } from "~/renderer/reducers/settings";
import connectApp from "@ledgerhq/live-common/hw/connectApp";
import { HOOKS_TRACKING_LOCATIONS } from "~/renderer/analytics/hooks/variables";
const action = createAction(getEnv("MOCK") ? mockedEventEmitter : connectApp);
const Result = (
props:
Expand Down Expand Up @@ -119,6 +120,7 @@ export default function StepConnectDevice({
}
}}
analyticsPropertyFlow="send"
location={HOOKS_TRACKING_LOCATIONS.sendModal}
/>
);
}

0 comments on commit 7d8ff18

Please sign in to comment.