Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 15 additions & 16 deletions __tests__/components/screens/SwapScreen/SwapAmountScreen.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -1197,7 +1197,7 @@ describe("SwapAmountScreen", () => {
expect(queryByTestId("swap-receive-pill")).toBeNull();
});

it("fires SWAP_TO_PICKER_OPENED with source:dropdown when the Receive pill is tapped", () => {
it("fires SWAP_PICKER_OPENED side:to with source:dropdown when the Receive pill is tapped", () => {
jest.spyOn(analytics, "track").mockClear();

setSwapStoreState({
Expand All @@ -1219,8 +1219,8 @@ describe("SwapAmountScreen", () => {
fireEvent.press(getByTestId("swap-receive-pill"));

expect(analytics.track).toHaveBeenCalledWith(
AnalyticsEvent.SWAP_TO_PICKER_OPENED,
{ source: "dropdown" },
AnalyticsEvent.SWAP_PICKER_OPENED,
{ side: "to", source: "dropdown" },
);
});
});
Expand Down Expand Up @@ -1354,7 +1354,7 @@ describe("SwapAmountScreen", () => {
jest.spyOn(analytics, "track").mockClear();
});

it("fires SWAP_TO_PICKER_OPENED with source:cta when the 'Select a token' CTA is pressed", async () => {
it("fires SWAP_PICKER_OPENED side:to with source:cta when the 'Select a token' CTA is pressed", async () => {
setSwapStoreState({ destinationToken: null, sourceAmount: "0" });

const navigation = makeNavigation();
Expand All @@ -1370,12 +1370,12 @@ describe("SwapAmountScreen", () => {
});

expect(analytics.track).toHaveBeenCalledWith(
AnalyticsEvent.SWAP_TO_PICKER_OPENED,
{ source: "cta" },
AnalyticsEvent.SWAP_PICKER_OPENED,
{ side: "to", source: "cta" },
);
});

it("fires SWAP_TO_PICKER_OPENED with source:dropdown when the Receive dropdown is tapped", () => {
it("fires SWAP_PICKER_OPENED side:to with source:dropdown when the Receive dropdown is tapped", () => {
// destinationToken is null so the "Select a token" placeholder pill is rendered
setSwapStoreState({ destinationToken: null, sourceAmount: "0" });

Expand All @@ -1386,12 +1386,12 @@ describe("SwapAmountScreen", () => {
fireEvent.press(getByTestId("swap-receive-choose-pill"));

expect(analytics.track).toHaveBeenCalledWith(
AnalyticsEvent.SWAP_TO_PICKER_OPENED,
{ source: "dropdown" },
AnalyticsEvent.SWAP_PICKER_OPENED,
{ side: "to", source: "dropdown" },
);
});

it("fires SWAP_FROM_PICKER_OPENED with source:dropdown when the Sell pill is tapped", () => {
it("fires SWAP_PICKER_OPENED side:from with source:dropdown when the Sell pill is tapped", () => {
// Held source (XLM) → "swap-sell-pill" is rendered; press routes
// through navigateToSelectSourceTokenScreen which is the only call
// site for the source-side picker event.
Expand All @@ -1407,14 +1407,13 @@ describe("SwapAmountScreen", () => {
fireEvent.press(getByTestId("swap-sell-pill"));

expect(analytics.track).toHaveBeenCalledWith(
AnalyticsEvent.SWAP_FROM_PICKER_OPENED,
{ source: "dropdown" },
AnalyticsEvent.SWAP_PICKER_OPENED,
{ side: "from", source: "dropdown" },
);
// Negative assertion: source picker MUST NOT reuse the destination
// event after the caa4aeab split.
// Negative assertion: the source picker MUST tag side:from, never side:to.
expect(analytics.track).not.toHaveBeenCalledWith(
AnalyticsEvent.SWAP_TO_PICKER_OPENED,
{ source: "dropdown" },
AnalyticsEvent.SWAP_PICKER_OPENED,
{ side: "to", source: "dropdown" },
);
});

Expand Down
24 changes: 24 additions & 0 deletions __tests__/services/Analytics.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,4 +14,28 @@ describe("Analytics Service", () => {
expect(AnalyticsEvent.SEND_PAYMENT_FAIL).toBeDefined();
expect(AnalyticsEvent.RE_AUTH_SUCCESS).toBeDefined();
});

it("maps domain events to the shared cross-platform wire strings (#2883)", () => {
// A representative slice of the renamed / consolidated catalog. These are
// the values sent to Amplitude and are a dashboard contract; changing one
// requires coordinating with analytics.
expect(AnalyticsEvent.SEND_PAYMENT_SUCCESS).toBe("payment.completed");
expect(AnalyticsEvent.SEND_PAYMENT_FAIL).toBe("payment.failed");
expect(AnalyticsEvent.SWAP_SUCCESS).toBe("swap.completed");
expect(AnalyticsEvent.SWAP_PICKER_OPENED).toBe("swap.picker_opened");
expect(AnalyticsEvent.SIGN_TRANSACTION_SUCCESS).toBe(
"signing.transaction_approved",
);
expect(AnalyticsEvent.SIGN_TRANSACTION_FAIL).toBe(
"signing.transaction_rejected",
);
expect(AnalyticsEvent.BLOCKAID_SCAN_COMPLETED).toBe(
"blockaid.scan_completed",
);
expect(AnalyticsEvent.BLOCKAID_SCAN_FAILED).toBe("blockaid.scan_failed");
expect(AnalyticsEvent.ASSET_ADD_RESPONDED).toBe("asset_add.responded");
expect(AnalyticsEvent.APP_UPDATE_STORE_OPENED).toBe(
"app_update.store_opened",
);
});
});
57 changes: 57 additions & 0 deletions __tests__/services/blockaid/api.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import axios from "axios";
import { AnalyticsEvent } from "config/analyticsConfig";
import { NETWORKS } from "config/constants";
import { analytics } from "services/analytics";
import { freighterBackendV1 } from "services/backend";
import { scanBulkTokens, scanToken } from "services/blockaid/api";

Expand All @@ -10,6 +12,7 @@ jest.mock("services/analytics", () => ({ analytics: { track: jest.fn() } }));
jest.mock("helpers/networks", () => ({ isMainnet: () => true }));

const mockGet = freighterBackendV1.get as jest.Mock;
const mockTrack = analytics.track as jest.Mock;

describe("scanBulkTokens error handling", () => {
beforeEach(() => jest.clearAllMocks());
Expand Down Expand Up @@ -67,3 +70,57 @@ describe("scanToken native XLM handling", () => {
expect(mockGet).toHaveBeenCalled();
});
});

describe("blockaid scan analytics (#2883 consolidation)", () => {
beforeEach(() => jest.clearAllMocks());

it("emits the consolidated scan_completed event with scan_target=asset + result", async () => {
mockGet.mockResolvedValue({
data: { data: { result_type: "Malicious" } },
});

await scanToken({
tokenCode: "USDC",
tokenIssuer: "GISSUER",
network: NETWORKS.PUBLIC,
});

expect(mockTrack).toHaveBeenCalledWith(
AnalyticsEvent.BLOCKAID_SCAN_COMPLETED,
expect.objectContaining({ scan_target: "asset", result: "MALICIOUS" }),
);
});

it("emits the consolidated scan_completed event with scan_target=asset_bulk", async () => {
mockGet.mockResolvedValue({
data: { data: { results: { "USDC-GISSUER": { result_type: "Benign" } } } },
});

await scanBulkTokens({
addressList: ["USDC-GISSUER"],
network: NETWORKS.PUBLIC,
});

expect(mockTrack).toHaveBeenCalledWith(
AnalyticsEvent.BLOCKAID_SCAN_COMPLETED,
expect.objectContaining({ scan_target: "asset_bulk" }),
);
});

it("emits the added scan_failed event when a scan throws", async () => {
mockGet.mockRejectedValue(new Error("backend down"));

await expect(
scanToken({
tokenCode: "USDC",
tokenIssuer: "GISSUER",
network: NETWORKS.PUBLIC,
}),
).rejects.toThrow();

expect(mockTrack).toHaveBeenCalledWith(
AnalyticsEvent.BLOCKAID_SCAN_FAILED,
expect.objectContaining({ scan_target: "asset" }),
);
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,9 @@ export const ForceUpdateScreen: React.FC<ForceUpdateScreenProps> = ({

const handleGoToAppStore = () => {
openAppStore().then(() => {
analytics.track(AnalyticsEvent.APP_UPDATE_OPEN_STORE_FROM_SCREEN);
analytics.track(AnalyticsEvent.APP_UPDATE_STORE_OPENED, {
source: "screen",
});
onDismiss?.();
});
};
Expand Down
4 changes: 3 additions & 1 deletion src/components/screens/HomeScreen/HomeScreenHeader.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,9 @@ const HomeScreenHeader = (

const handleBannerPress = useCallback(() => {
openAppStore().then(() => {
analytics.track(AnalyticsEvent.APP_UPDATE_OPEN_STORE_FROM_BANNER);
analytics.track(AnalyticsEvent.APP_UPDATE_STORE_OPENED, {
source: "banner",
});
});
}, [openAppStore]);

Expand Down
13 changes: 8 additions & 5 deletions src/components/screens/SwapScreen/hooks/useSwapNavigation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,9 @@ type SwapNavigation = NativeStackNavigationProp<

/**
* Centralises the SwapAmountScreen's two "open the SwapToScreen picker"
* navigation callbacks. Each emits its own analytics event
* (SWAP_TO_PICKER_OPENED vs SWAP_FROM_PICKER_OPENED) tagged with the
* picker entrypoint (cta vs dropdown), then routes to the same
* navigation callbacks. Each emits the shared SWAP_PICKER_OPENED event
* discriminated by `side` (to vs from) and tagged with the picker
* entrypoint (cta vs dropdown via `source`), then routes to the same
* SWAP_SCREEN with the corresponding selectionType.
*
* Returns:
Expand All @@ -40,7 +40,7 @@ export const useSwapNavigation = ({
} => {
const openDestinationPicker = useCallback(
(source: SwapPickerEntrypoint = SwapPickerEntrypoint.DROPDOWN) => {
analytics.track(AnalyticsEvent.SWAP_TO_PICKER_OPENED, { source });
analytics.track(AnalyticsEvent.SWAP_PICKER_OPENED, { side: "to", source });
navigation.navigate(SWAP_ROUTES.SWAP_SCREEN, {
selectionType: SWAP_SELECTION_TYPES.DESTINATION,
});
Expand All @@ -54,7 +54,10 @@ export const useSwapNavigation = ({

const openSourcePicker = useCallback(
(source: SwapPickerEntrypoint) => {
analytics.track(AnalyticsEvent.SWAP_FROM_PICKER_OPENED, { source });
analytics.track(AnalyticsEvent.SWAP_PICKER_OPENED, {
side: "from",
source,
});
navigation.navigate(SWAP_ROUTES.SWAP_SCREEN, {
selectionType: SWAP_SELECTION_TYPES.SOURCE,
});
Expand Down
Loading
Loading