Skip to content

Commit f014dee

Browse files
Merge pull request #9035 from LedgerHQ/feat/live-15766-data-tracking-in-the-add-account-flow
feat(live-15766): Data tracking in the add account flow
2 parents 0290316 + 03fde82 commit f014dee

File tree

10 files changed

+334
-13
lines changed

10 files changed

+334
-13
lines changed

.changeset/thick-bobcats-wait.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"ledger-live-desktop": minor
3+
---
4+
5+
Add data tracking in the add account flow
Lines changed: 199 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,199 @@
1+
import { renderHook } from "@testing-library/react";
2+
import { UseTrackAddAccountModal, useTrackAddAccountModal } from "./useTrackAddAccountModal";
3+
import { track } from "../segment";
4+
import { CantOpenDevice, UserRefusedOnDevice, LockedDeviceError } from "@ledgerhq/errors";
5+
import type { Device } from "@ledgerhq/types-devices";
6+
import { CONNECTION_TYPES } from "./variables";
7+
8+
jest.mock("../segment", () => ({
9+
track: jest.fn(),
10+
}));
11+
12+
describe("useTrackAddAccountModal", () => {
13+
const deviceMock: Device = {
14+
modelId: "europa",
15+
wired: false,
16+
};
17+
18+
const defaultArgs: UseTrackAddAccountModal = {
19+
location: "Add account modal",
20+
requestOpenApp: "Bitcoin",
21+
device: deviceMock,
22+
error: null,
23+
isTrackingEnabled: true,
24+
userMustConnectDevice: null,
25+
isLocked: null,
26+
};
27+
28+
afterEach(() => {
29+
jest.clearAllMocks();
30+
});
31+
32+
it('should track "Connection failed" if error is CantOpenDevice', () => {
33+
renderHook(() =>
34+
useTrackAddAccountModal({
35+
...defaultArgs,
36+
error: new CantOpenDevice(),
37+
}),
38+
);
39+
40+
expect(track).toHaveBeenCalledWith(
41+
"Connection failed",
42+
expect.objectContaining({
43+
deviceType: "europa",
44+
connectionType: CONNECTION_TYPES.BLE,
45+
platform: "LLD",
46+
page: "Add account modal",
47+
}),
48+
true,
49+
);
50+
});
51+
52+
it('should track "Open app denied" if there was a previous "requestOpenApp" and now the error is UserRefusedOnDevice', () => {
53+
const { rerender } = renderHook(props => useTrackAddAccountModal(props), {
54+
initialProps: {
55+
...defaultArgs,
56+
requestOpenApp: "Bitcoin",
57+
},
58+
});
59+
60+
rerender({
61+
...defaultArgs,
62+
//@ts-expect-error requestOpenApp should be a string
63+
requestOpenApp: null,
64+
error: new UserRefusedOnDevice(),
65+
});
66+
67+
expect(track).toHaveBeenCalledWith(
68+
"Open app denied",
69+
expect.objectContaining({
70+
deviceType: "europa",
71+
connectionType: CONNECTION_TYPES.BLE,
72+
platform: "LLD",
73+
page: "Add account modal",
74+
}),
75+
true,
76+
);
77+
});
78+
79+
it('should track "Device connection lost" if userMustConnectDevice is true', () => {
80+
renderHook(() =>
81+
useTrackAddAccountModal({
82+
...defaultArgs,
83+
userMustConnectDevice: true,
84+
}),
85+
);
86+
87+
expect(track).toHaveBeenCalledWith(
88+
"Device connection lost",
89+
expect.objectContaining({
90+
deviceType: "europa",
91+
connectionType: CONNECTION_TYPES.BLE,
92+
platform: "LLD",
93+
page: "Add account modal",
94+
}),
95+
true,
96+
);
97+
});
98+
99+
it('should track "Device locked" if isLocked is true', () => {
100+
renderHook(() =>
101+
useTrackAddAccountModal({
102+
...defaultArgs,
103+
isLocked: true,
104+
}),
105+
);
106+
107+
expect(track).toHaveBeenCalledWith(
108+
"Device locked",
109+
expect.objectContaining({
110+
deviceType: "europa",
111+
connectionType: CONNECTION_TYPES.BLE,
112+
platform: "LLD",
113+
page: "Add account modal",
114+
}),
115+
true,
116+
);
117+
});
118+
119+
it('should track "Device locked" if error is LockedDeviceError', () => {
120+
renderHook(() =>
121+
useTrackAddAccountModal({
122+
...defaultArgs,
123+
error: new LockedDeviceError(),
124+
}),
125+
);
126+
127+
expect(track).toHaveBeenCalledWith(
128+
"Device locked",
129+
expect.objectContaining({
130+
deviceType: "europa",
131+
connectionType: CONNECTION_TYPES.BLE,
132+
platform: "LLD",
133+
page: "Add account modal",
134+
}),
135+
true,
136+
);
137+
});
138+
139+
it("should use the previous device if the current device is undefined on subsequent renders", () => {
140+
const { rerender } = renderHook(props => useTrackAddAccountModal(props), {
141+
initialProps: {
142+
...defaultArgs,
143+
device: { modelId: "europa", wired: true },
144+
},
145+
});
146+
147+
rerender({
148+
...defaultArgs,
149+
// @ts-expect-error device should be a Device
150+
device: undefined,
151+
error: new CantOpenDevice(),
152+
});
153+
154+
expect(track).toHaveBeenCalledWith(
155+
"Connection failed",
156+
expect.objectContaining({
157+
deviceType: "europa",
158+
connectionType: CONNECTION_TYPES.USB,
159+
platform: "LLD",
160+
page: "Add account modal",
161+
}),
162+
true,
163+
);
164+
});
165+
166+
it('should not track if location is not "Add account modal"', () => {
167+
renderHook(() =>
168+
useTrackAddAccountModal({
169+
...defaultArgs,
170+
location: "NOT Add account",
171+
}),
172+
);
173+
174+
expect(track).not.toHaveBeenCalled();
175+
});
176+
177+
it("should include correct connection type based on device.wired", () => {
178+
const wiredDeviceMock = { ...deviceMock, wired: true };
179+
180+
renderHook((props: UseTrackAddAccountModal) => useTrackAddAccountModal(props), {
181+
initialProps: {
182+
...defaultArgs,
183+
device: wiredDeviceMock,
184+
error: new LockedDeviceError(),
185+
},
186+
});
187+
188+
expect(track).toHaveBeenCalledWith(
189+
"Device locked",
190+
expect.objectContaining({
191+
deviceType: "europa",
192+
connectionType: CONNECTION_TYPES.USB,
193+
platform: "LLD",
194+
page: "Add account modal",
195+
}),
196+
true,
197+
);
198+
});
199+
});
Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
import { useRef, useEffect } from "react";
2+
import { track } from "../segment";
3+
import { Device } from "@ledgerhq/types-devices";
4+
import { UserRefusedOnDevice, LockedDeviceError, CantOpenDevice } from "@ledgerhq/errors";
5+
import { CONNECTION_TYPES } from "./variables";
6+
import { LedgerError } from "~/renderer/components/DeviceAction";
7+
8+
export type UseTrackAddAccountModal = {
9+
location: string | undefined;
10+
device: Device;
11+
error:
12+
| (LedgerError & {
13+
name?: string;
14+
managerAppName?: string;
15+
})
16+
| undefined
17+
| null;
18+
isTrackingEnabled?: boolean | null | undefined;
19+
userMustConnectDevice?: boolean | null | undefined;
20+
isLocked?: boolean | null | undefined;
21+
requestOpenApp?: string | null | undefined;
22+
};
23+
24+
function getConnectionType(d?: Device): string | undefined {
25+
if (d?.wired === true) return CONNECTION_TYPES.USB;
26+
if (d?.wired === false) return CONNECTION_TYPES.BLE;
27+
return undefined;
28+
}
29+
30+
/**
31+
* a custom hook to track events in the Add Account Modal.
32+
* tracks user interactions with the Add Account Modal based on state changes and errors.
33+
*
34+
* @param location - current location in the app (expected "Add account modal").
35+
* @param device - the connected device information.
36+
* @param error - current error state.
37+
* @param isTrackingEnabled - flag indicating if tracking is enabled.
38+
* @param requestOpenApp - optional - the app requested to be opened.
39+
* @param userMustConnectDevice - optional - flag indicating if the user must connect the device.
40+
* @param isLocked - optional - flag indicating if the device is locked.
41+
*/
42+
export const useTrackAddAccountModal = ({
43+
location,
44+
device,
45+
error,
46+
isTrackingEnabled,
47+
requestOpenApp = null,
48+
userMustConnectDevice = null,
49+
isLocked = null,
50+
}: UseTrackAddAccountModal) => {
51+
const previousOpenAppRequested = useRef<string | null | undefined>(undefined);
52+
const previousDevice = useRef<Device | null | undefined>(undefined);
53+
54+
useEffect(() => {
55+
if (location !== "Add account modal") return;
56+
57+
const defaultPayload = {
58+
deviceType: device?.modelId || previousDevice.current?.modelId || undefined,
59+
connectionType: getConnectionType(device) ?? getConnectionType(previousDevice.current),
60+
platform: "LLD",
61+
page: "Add account modal",
62+
};
63+
64+
if (error instanceof CantOpenDevice) {
65+
// device disconnected during account creation
66+
track("Connection failed", defaultPayload, isTrackingEnabled);
67+
}
68+
69+
if (previousOpenAppRequested.current && error instanceof UserRefusedOnDevice) {
70+
// user refused to open app
71+
track("Open app denied", defaultPayload, isTrackingEnabled);
72+
}
73+
74+
if (userMustConnectDevice) {
75+
// device disconnected during account creation
76+
track("Device connection lost", defaultPayload, isTrackingEnabled);
77+
}
78+
79+
if (isLocked || error instanceof LockedDeviceError) {
80+
// device locked during account creation
81+
track("Device locked", defaultPayload, isTrackingEnabled);
82+
}
83+
84+
previousOpenAppRequested.current = requestOpenApp;
85+
if (device) previousDevice.current = device;
86+
}, [error, location, isTrackingEnabled, device, requestOpenApp, userMustConnectDevice, isLocked]);
87+
};

apps/ledger-live-desktop/src/renderer/analytics/hooks/useTrackManagerSectionEvents.test.ts

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import {
99
UserRefusedDeviceNameChange,
1010
UserRefusedFirmwareUpdate,
1111
} from "@ledgerhq/errors";
12+
import { CONNECTION_TYPES } from "./variables";
1213

1314
jest.mock("../segment", () => ({
1415
track: jest.fn(),
@@ -48,7 +49,7 @@ describe("useTrackManagerSectionEvents", () => {
4849
"Secure Channel approved",
4950
expect.objectContaining({
5051
deviceType: "europa",
51-
connectionType: "USB",
52+
connectionType: CONNECTION_TYPES.USB,
5253
platform: "LLD",
5354
page: "Manager Dashboard",
5455
}),
@@ -72,7 +73,7 @@ describe("useTrackManagerSectionEvents", () => {
7273
"Deleted Custom Lock Screen",
7374
expect.objectContaining({
7475
deviceType: "europa",
75-
connectionType: "USB",
76+
connectionType: CONNECTION_TYPES.USB,
7677
platform: "LLD",
7778
page: "Manager Dashboard",
7879
}),
@@ -91,7 +92,7 @@ describe("useTrackManagerSectionEvents", () => {
9192
"Secure Channel denied",
9293
expect.objectContaining({
9394
deviceType: "europa",
94-
connectionType: "USB",
95+
connectionType: CONNECTION_TYPES.USB,
9596
platform: "LLD",
9697
page: "Manager Dashboard",
9798
}),
@@ -110,7 +111,7 @@ describe("useTrackManagerSectionEvents", () => {
110111
"Renamed Device cancelled",
111112
expect.objectContaining({
112113
deviceType: "europa",
113-
connectionType: "USB",
114+
connectionType: CONNECTION_TYPES.USB,
114115
platform: "LLD",
115116
page: "Manager Dashboard",
116117
}),
@@ -129,7 +130,7 @@ describe("useTrackManagerSectionEvents", () => {
129130
"User refused OS update via LL",
130131
expect.objectContaining({
131132
deviceType: "europa",
132-
connectionType: "USB",
133+
connectionType: CONNECTION_TYPES.USB,
133134
platform: "LLD",
134135
page: "Manager Dashboard",
135136
}),

apps/ledger-live-desktop/src/renderer/analytics/hooks/useTrackManagerSectionEvents.ts

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,8 @@ import {
66
} from "@ledgerhq/errors";
77
import { track } from "../segment";
88
import { Device } from "@ledgerhq/types-devices";
9-
import { LedgerErrorConstructor } from "@ledgerhq/errors/lib/helpers";
10-
11-
type LedgerError = InstanceType<LedgerErrorConstructor<{ [key: string]: unknown }>>;
9+
import { CONNECTION_TYPES } from "./variables";
10+
import { LedgerError } from "~/renderer/components/DeviceAction";
1211

1312
export type UseTrackManagerSectionEvents = {
1413
location: string | undefined;
@@ -52,7 +51,7 @@ export const useTrackManagerSectionEvents = ({
5251

5352
const defaultPayload = {
5453
deviceType: device?.modelId,
55-
connectionType: device?.wired ? "USB" : "BLE",
54+
connectionType: device?.wired ? CONNECTION_TYPES.USB : CONNECTION_TYPES.BLE,
5655
platform: "LLD",
5756
page: "Manager Dashboard",
5857
};

apps/ledger-live-desktop/src/renderer/analytics/hooks/useTrackReceiveFlow.ts

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,8 @@
11
import { useEffect } from "react";
22
import { track } from "../segment";
33
import { Device } from "@ledgerhq/types-devices";
4-
import { LedgerErrorConstructor } from "@ledgerhq/errors/lib/helpers";
54
import { UserRefusedOnDevice } from "@ledgerhq/errors";
6-
7-
type LedgerError = InstanceType<LedgerErrorConstructor<{ [key: string]: unknown }>>;
5+
import { LedgerError } from "~/renderer/components/DeviceAction";
86

97
export type UseTrackReceiveFlow = {
108
location: string | undefined;
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
export enum CONNECTION_TYPES {
2+
USB = "USB",
3+
BLE = "BLE",
4+
}

0 commit comments

Comments
 (0)