Skip to content

Commit db777d2

Browse files
authored
Merge branch 'feat/frontend/track-cfs-sign-event' into fix/frontend/signer-unavailable-toast
2 parents 9d1fcb8 + 74262e2 commit db777d2

26 files changed

Lines changed: 215 additions & 50 deletions

src/frontend/src/lib/canisters/backend.errors.ts

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,11 @@ import type {
77
RateLimitError,
88
SignOnramperWidgetUrlError
99
} from '$declarations/backend/backend.did';
10-
import { CanisterInternalError } from '$lib/canisters/errors';
10+
import {
11+
CanisterInternalError,
12+
OnramperRateLimitedError,
13+
OnramperSecretNotConfiguredError
14+
} from '$lib/canisters/errors';
1115
import { NANO_SECONDS_IN_SECOND } from '$lib/constants/app.constants';
1216
import { assertNever } from '@dfinity/utils';
1317
import { mapIcrc2ApproveError, type ApproveError } from '@icp-sdk/canisters/ledger/icp';
@@ -158,13 +162,18 @@ export const mapSignOnramperWidgetUrlError = (
158162
err: SignOnramperWidgetUrlError
159163
): CanisterInternalError => {
160164
if ('SecretNotConfigured' in err) {
161-
return new CanisterInternalError(
165+
return new OnramperSecretNotConfiguredError(
162166
'OnRamper signing secret is not configured on the backend canister.'
163167
);
164168
}
165169

166170
if ('RateLimited' in err) {
167-
return mapRateLimitError(err.RateLimited);
171+
const { max_calls: maxCalls, window_ns: windowNs } = err.RateLimited;
172+
const windowSeconds = windowNs / NANO_SECONDS_IN_SECOND;
173+
174+
return new OnramperRateLimitedError(
175+
`Rate limit exceeded. Maximum of ${maxCalls} calls allowed every ${windowSeconds} seconds.`
176+
);
168177
}
169178

170179
return assertNeverOr(err, new CanisterInternalError('Unknown SignOnramperWidgetUrlError'));
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,7 @@
11
export class CanisterInternalError extends Error {}
2+
3+
// Distinguishable Onramper widget URL signing failures, so the UI/telemetry can
4+
// tell apart a misconfigured backend secret from a transient rate limit.
5+
export class OnramperSecretNotConfiguredError extends CanisterInternalError {}
6+
7+
export class OnramperRateLimitedError extends CanisterInternalError {}

src/frontend/src/lib/components/buy/Buy.svelte

Lines changed: 0 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,33 +1,14 @@
11
<script lang="ts">
2-
import { nonNullish } from '@dfinity/utils';
32
import BuyButton from '$lib/components/buy/BuyButton.svelte';
43
import BuyModal from '$lib/components/buy/BuyModal.svelte';
5-
import {
6-
HOME_PAGE_ROUTE,
7-
TOKEN_VIEW_ROUTE,
8-
TRACK_BUY_TOKEN
9-
} from '$lib/constants/analytics.constants';
104
import { modalBuy } from '$lib/derived/modal.derived';
11-
import { pageToken } from '$lib/derived/page-token.derived';
12-
import { trackEvent } from '$lib/services/analytics.services';
135
import { modalStore } from '$lib/stores/modal.store';
146
157
const modalId = Symbol();
168
</script>
179

1810
<BuyButton
1911
onclick={() => {
20-
trackEvent({
21-
name: TRACK_BUY_TOKEN,
22-
metadata: nonNullish($pageToken)
23-
? {
24-
source: TOKEN_VIEW_ROUTE,
25-
token: $pageToken.symbol,
26-
network: $pageToken.network.name
27-
}
28-
: { source: HOME_PAGE_ROUTE }
29-
});
30-
3112
modalStore.openBuy(modalId);
3213
}}
3314
/>

src/frontend/src/lib/components/buy/BuyModalContent.svelte

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
</div>
1212
</div>
1313
{:else}
14-
<BuyUnavailableNotice />
14+
<BuyUnavailableNotice reason="disabled" />
1515
{/if}
1616

1717
<style lang="scss">

src/frontend/src/lib/components/buy/BuyUnavailableNotice.svelte

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,18 @@
44
import { i18n } from '$lib/stores/i18n.store';
55
import { modalStore } from '$lib/stores/modal.store';
66
import { replaceOisyPlaceholders } from '$lib/utils/i18n.utils';
7+
8+
interface Props {
9+
reason?: 'disabled' | 'signing-failed';
10+
}
11+
12+
let { reason = 'disabled' }: Props = $props();
13+
14+
let description = $derived(
15+
reason === 'signing-failed'
16+
? $i18n.buy.text.unavailable_description_signing_failed
17+
: $i18n.buy.text.unavailable_description_disabled
18+
);
719
</script>
820

921
<div class="flex flex-col items-center px-4 py-6 text-center">
@@ -15,7 +27,7 @@
1527
><IconShieldCheck size="16" /></span
1628
>{replaceOisyPlaceholders($i18n.core.text.oisy_protects_you)}</strong
1729
>
18-
{$i18n.buy.text.unavailable_description}
30+
{description}
1931
</p>
2032

2133
<p class="mb-6 text-sm text-tertiary">{$i18n.buy.text.unavailable_fallback_hint}</p>

src/frontend/src/lib/components/onramper/OnramperWidget.svelte

Lines changed: 41 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,10 @@
1212
import { erc20Tokens } from '$eth/derived/erc20.derived';
1313
import { harvestAutopilots } from '$eth/derived/harvest-autopilots.derived';
1414
import { icpAccountIdentifierText } from '$icp/derived/ic.derived';
15+
import {
16+
OnramperRateLimitedError,
17+
OnramperSecretNotConfiguredError
18+
} from '$lib/canisters/errors';
1519
import BuyUnavailableNotice from '$lib/components/buy/BuyUnavailableNotice.svelte';
1620
import { BUY_MODAL_ONRAMPER_IFRAME } from '$lib/constants/test-ids.constants';
1721
import { btcAddressMainnet, ethAddress, solAddressMainnet } from '$lib/derived/address.derived';
@@ -21,9 +25,15 @@
2125
import { networkBitcoin, networkEthereum, networkSolana } from '$lib/derived/network.derived';
2226
import { networks } from '$lib/derived/networks.derived';
2327
import { enabledTokens } from '$lib/derived/tokens.derived';
28+
import {
29+
PLAUSIBLE_EVENT_ONRAMPER_ERROR_TYPES,
30+
PLAUSIBLE_EVENT_RESULT_STATUSES
31+
} from '$lib/enums/plausible';
32+
import { trackOnramperOpen } from '$lib/services/analytics.services';
2433
import { i18n } from '$lib/stores/i18n.store';
2534
import { token } from '$lib/stores/token.store';
2635
import { consoleError } from '$lib/utils/console.utils';
36+
import { getTokenIdentifier } from '$lib/utils/identifier.utils';
2737
import { buildOnramperLink, mapOnramperNetworkWallets } from '$lib/utils/onramper.utils';
2838
2939
let vault = $derived(
@@ -73,6 +83,24 @@
7383
let src = $state<string | undefined>(undefined);
7484
let signingFailed = $state(false);
7585
86+
let tokenPayload = $derived(
87+
nonNullish($token)
88+
? {
89+
network: $token.network.id.description ?? '',
90+
symbol: $token.symbol,
91+
name: $token.name,
92+
address: getTokenIdentifier($token)
93+
}
94+
: undefined
95+
);
96+
97+
const classifyOnramperError = (error: unknown): PLAUSIBLE_EVENT_ONRAMPER_ERROR_TYPES =>
98+
error instanceof OnramperSecretNotConfiguredError
99+
? PLAUSIBLE_EVENT_ONRAMPER_ERROR_TYPES.SECRET_NOT_CONFIGURED
100+
: error instanceof OnramperRateLimitedError
101+
? PLAUSIBLE_EVENT_ONRAMPER_ERROR_TYPES.RATE_LIMITED
102+
: PLAUSIBLE_EVENT_ONRAMPER_ERROR_TYPES.SIGNING_FAILED;
103+
76104
// Resolve the signed widget URL through the backend canister whenever the inputs change. Build
77105
// the link asynchronously (the canister returns the HMAC over the sensitive parameters) and
78106
// guard against late resolutions overwriting newer state via a cancellation token.
@@ -104,12 +132,24 @@
104132
.then((url) => {
105133
if (!cancelled) {
106134
src = url;
135+
136+
trackOnramperOpen({
137+
token: tokenPayload,
138+
status: PLAUSIBLE_EVENT_RESULT_STATUSES.SUCCESS
139+
});
107140
}
108141
})
109142
.catch((error: unknown) => {
110143
if (!cancelled) {
111144
consoleError('Could not sign OnRamper widget URL', error);
112145
signingFailed = true;
146+
147+
trackOnramperOpen({
148+
token: tokenPayload,
149+
status: PLAUSIBLE_EVENT_RESULT_STATUSES.ERROR,
150+
errorType: classifyOnramperError(error),
151+
errorMessage: error instanceof Error ? error.message : undefined
152+
});
113153
}
114154
});
115155
@@ -155,7 +195,7 @@
155195
<!-- "In order to do customer verification before purchase, we require the following permissions to be given to the app. So this is definitely merely for the KYC and also for fraud detection algorithms i suppose" -->
156196

157197
{#if signingFailed}
158-
<BuyUnavailableNotice />
198+
<BuyUnavailableNotice reason="signing-failed" />
159199
{:else}
160200
<div
161201
class="absolute top-0 right-0 bottom-0 left-0 bg-surface text-brand-primary transition-all duration-500 ease-in-out"

src/frontend/src/lib/constants/analytics.constants.ts

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -168,9 +168,6 @@ export const AI_ASSISTANT_MESSAGE_FAILED_TO_BE_PARSED = 'ai_assistant_message_fa
168168
export const AI_ASSISTANT_TOOL_EXECUTION_TRIGGERED = 'ai_assistant_tool_execution_triggered';
169169
export const AI_ASSISTANT_TEXTUAL_RESPONSE_RECEIVED = 'ai_assistant_textual_response_received';
170170

171-
// Buy
172-
export const TRACK_BUY_TOKEN = 'buy_token';
173-
174171
// NFTs
175172
export const TRACK_OPEN_ASSETS_TAB = 'open_assets_tab';
176173
export const TRACK_NFT_SETTINGS_CHANGE = 'nft_settings_change';

src/frontend/src/lib/enums/plausible.ts

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,18 @@ export enum PLAUSIBLE_EVENTS {
2020
TRANSACTION_FILTER = 'transaction_filter',
2121
TOKEN_MANAGE = 'token_manage',
2222
EXPORT_DATA = 'export_data',
23-
CFS_SIGN = 'cfs_sign'
23+
CFS_SIGN = 'cfs_sign',
24+
ONRAMPER_OPEN = 'onramper_open'
25+
}
26+
27+
export enum PLAUSIBLE_EVENT_ERROR_SEVERITIES {
28+
MAJOR = 'major'
29+
}
30+
31+
export enum PLAUSIBLE_EVENT_ONRAMPER_ERROR_TYPES {
32+
SECRET_NOT_CONFIGURED = 'secret_not_configured',
33+
RATE_LIMITED = 'rate_limited',
34+
SIGNING_FAILED = 'signing_failed'
2435
}
2536

2637
export enum PLAUSIBLE_EVENT_CONTEXTS {

src/frontend/src/lib/i18n/ar.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1231,7 +1231,8 @@
12311231
"buy": "شراء",
12321232
"buy_dev": "شراء (بيئة تطوير)",
12331233
"unavailable_title": "شراء الرموز غير متاح مؤقتًا",
1234-
"unavailable_description": "شراء الرموز معطل حاليًا بينما نواصل تحسين أمان هذه العملية من خلال إضافة توقيع عناوين URL. ستتم إعادة تفعيله بمجرد اكتمال العمل.",
1234+
"unavailable_description_disabled": "",
1235+
"unavailable_description_signing_failed": "",
12351236
"unavailable_fallback_hint": "في غضون ذلك، لا يزال بإمكانك استلام الرموز عن طريق مشاركة عنوان محفظتك."
12361237
},
12371238
"actions": {

src/frontend/src/lib/i18n/cs.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1231,7 +1231,8 @@
12311231
"buy": "Koupit",
12321232
"buy_dev": "Koupit (vývojové prostředí)",
12331233
"unavailable_title": "Nákup tokenů je dočasně nedostupný",
1234-
"unavailable_description": "Nákup tokenů je momentálně vypnutý, zatímco dále vylepšujeme zabezpečení tohoto procesu zavedením podepisování URL. Bude znovu zapnut, jakmile bude práce dokončena.",
1234+
"unavailable_description_disabled": "",
1235+
"unavailable_description_signing_failed": "",
12351236
"unavailable_fallback_hint": "Mezitím můžete stále přijímat tokeny sdílením adresy své peněženky."
12361237
},
12371238
"actions": {

0 commit comments

Comments
 (0)