Skip to content

Commit 14bb6e9

Browse files
evavirsedaJuligsVmMad
authored
feat(wallet): add element copied amplitude event (#10268)
# Description of change Please write a summary of your changes and why you made them. ## Links to any relevant issues fixes #10174 ## How the change has been tested Describe the tests that you ran to verify your changes. Make sure to provide instructions for the maintainer as well as any relevant configurations. --------- Co-authored-by: Juliana <115430927+Juligs@users.noreply.github.com> Co-authored-by: JCNoguera <88061365+VmMad@users.noreply.github.com>
1 parent dca66b5 commit 14bb6e9

File tree

12 files changed

+127
-12
lines changed

12 files changed

+127
-12
lines changed

apps/wallet/src/shared/analytics/ampli/index.ts

Lines changed: 37 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -390,7 +390,7 @@ export interface DisconnectedApplicationProperties {
390390
sourceFlow?: string;
391391
}
392392

393-
export interface ExternalLinkOpenedProperties {
393+
export interface ElementCopiedProperties {
394394
type: string;
395395
value?: string;
396396
/**
@@ -401,6 +401,16 @@ export interface ExternalLinkOpenedProperties {
401401
visibility?: 'private' | 'public';
402402
}
403403

404+
export interface ExternalLinkOpenedProperties {
405+
type: string;
406+
value?: string;
407+
/**
408+
* | Rule | Value |
409+
* |---|---|
410+
* | Enum Values | private, public |
411+
*/
412+
visibility?: 'private' | 'public';
413+
}
404414
export interface IotaStakedProperties {
405415
/**
406416
* | Rule | Value |
@@ -904,6 +914,14 @@ export class DisconnectedApplication implements BaseEvent {
904914
}
905915
}
906916

917+
export class ElementCopied implements BaseEvent {
918+
event_type = 'element copied';
919+
920+
constructor(public event_properties: ElementCopiedProperties) {
921+
this.event_properties = event_properties;
922+
}
923+
}
924+
907925
export class ExternalLinkOpened implements BaseEvent {
908926
event_type = 'external link opened';
909927

@@ -1729,6 +1747,24 @@ export class Ampli {
17291747
return this.track(new DisconnectedApplication(properties), options);
17301748
}
17311749

1750+
/**
1751+
* element copied
1752+
*
1753+
* [View in Tracking Plan](https://data.eu.amplitude.com/iota-foundation/IOTA%20Wallet/events/main/latest/element%20copied)
1754+
*
1755+
* Event has no description in tracking plan.
1756+
*
1757+
* @param properties The event's properties (e.g. type)
1758+
* @param options Amplitude event options.
1759+
*/
1760+
elementCopied(
1761+
properties: ElementCopiedProperties,
1762+
options?: EventOptions,
1763+
) {
1764+
return this.track(new ElementCopied(properties), options);
1765+
}
1766+
1767+
/*
17321768
/**
17331769
* external link opened
17341770
*

apps/wallet/src/shared/analytics/amplitude.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import { LogLevel } from '@amplitude/analytics-types';
77
import { attachEnvironmentPlugin, getCustomNetwork } from '@iota/core';
88
import { getNetwork, type Network } from '@iota/iota-sdk/client';
99
import { ampli } from './ampli';
10-
import { externalLinkOpenedPrivacyPlugin } from './plugins';
10+
import { elementCopiedPrivacyPlugin, externalLinkOpenedPrivacyPlugin } from './plugins';
1111
import { dialogContextPlugin } from './plugins/dialogContextPlugin';
1212

1313
const IS_ENABLED = process.env.BUILD_ENV === 'production';
@@ -62,6 +62,7 @@ export async function initAmplitude() {
6262
// Add environment plugin to set prefix dev events
6363
ampli.client.add(attachEnvironmentPlugin(IS_DEV));
6464

65+
ampli.client.add(elementCopiedPrivacyPlugin());
6566
ampli.client.add(externalLinkOpenedPrivacyPlugin());
6667
}
6768

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
// Copyright (c) 2026 IOTA Stiftung
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
import type { EnrichmentPlugin, Event } from '@amplitude/analytics-types';
5+
6+
// these are the types that are always private
7+
const PRIVATE_TYPES = new Set<string>(['address', 'digest', 'object', 'mnemonic']);
8+
9+
export function elementCopiedPrivacyPlugin(): EnrichmentPlugin {
10+
return {
11+
name: 'element-copied-privacy',
12+
type: 'enrichment',
13+
14+
async execute(event: Event) {
15+
if (!event.event_type?.endsWith('element copied')) {
16+
return event;
17+
}
18+
19+
let props = { ...(event.event_properties ?? {}) } as Record<string, unknown>;
20+
21+
const type =
22+
typeof props.type === 'string' && props.type.trim() ? props.type : 'unknown';
23+
24+
let visibility: 'private' | 'public' =
25+
props.visibility === 'public' ? 'public' : 'private';
26+
27+
if (PRIVATE_TYPES.has(type)) {
28+
visibility = 'private';
29+
}
30+
31+
props.type = type;
32+
props.visibility = visibility;
33+
34+
if (visibility === 'private') {
35+
const { value, ...rest } = props;
36+
props = rest;
37+
}
38+
39+
return {
40+
...event,
41+
event_properties: { ...props },
42+
};
43+
},
44+
};
45+
}

apps/wallet/src/shared/analytics/plugins/externalLinkPrivacyPlugin.ts renamed to apps/wallet/src/shared/analytics/plugins/externalLinkOpenedPrivacyPlugin.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,8 @@
33

44
import type { EnrichmentPlugin, Event } from '@amplitude/analytics-types';
55

6-
export const PUBLIC_TYPES = new Set<string>(['documentation', 'application', 'support']);
76
// these are the types that are always private
8-
export const PRIVATE_TYPES = new Set<string>(['address', 'digest', 'object']);
7+
const PRIVATE_TYPES = new Set<string>(['address', 'digest', 'object']);
98

109
export function externalLinkOpenedPrivacyPlugin(): EnrichmentPlugin {
1110
return {
Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
// Copyright (c) 2026 IOTA Stiftung
22
// SPDX-License-Identifier: Apache-2.0
33

4-
export * from './externalLinkPrivacyPlugin';
4+
export * from './elementCopiedPrivacyPlugin';
5+
export * from './externalLinkOpenedPrivacyPlugin';

apps/wallet/src/ui/app/components/HideShowDisplayBox.tsx

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
import { Button, ButtonType, TextArea } from '@iota/apps-ui-kit';
66
import { toast } from '@iota/core';
7+
import { ampli } from '_src/shared/analytics/ampli';
78

89
export interface HideShowDisplayBoxProps {
910
value: string | string[];
@@ -25,6 +26,10 @@ export function HideShowDisplayBox({
2526
const textToCopy = Array.isArray(value) ? value.join(' ') : value;
2627
try {
2728
await navigator.clipboard.writeText(textToCopy);
29+
ampli.elementCopied({
30+
type: 'mnemonic',
31+
value: textToCopy,
32+
});
2833
toast(copiedMessage || 'Copied');
2934
} catch {
3035
toast.error('Failed to copy');

apps/wallet/src/ui/app/components/accounts/AccountItem.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ export function AccountItem({
3535
const accountName = formatAccountName(account?.nickname, iotaName, account?.address);
3636
const copyAddress = useCopyToClipboard(account?.address || '', {
3737
copySuccessMessage: 'Address copied',
38+
textType: 'address',
3839
});
3940
const explorerHref = useExplorerLink({
4041
type: ExplorerLinkType.Address,

apps/wallet/src/ui/app/components/receipt-card/index.tsx

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import { type IotaTransactionBlockResponse } from '@iota/iota-sdk/client';
1515

1616
import { ExplorerLinkHelper } from '../ExplorerLinkHelper';
1717
import { ExplorerLink } from '../explorer-link';
18+
import { ampli } from '_src/shared/analytics/ampli';
1819

1920
interface ReceiptCardProps {
2021
txn: IotaTransactionBlockResponse;
@@ -54,9 +55,13 @@ export function ReceiptCard({ txn, activeAddress }: ReceiptCardProps) {
5455
<div className="self-center">
5556
<OutlinedCopyButton
5657
textToCopy={digest ?? ''}
57-
onCopySuccess={() =>
58-
toast.success('Transaction digest copied to clipboard')
59-
}
58+
onCopySuccess={() => {
59+
ampli.elementCopied({
60+
type: 'digest',
61+
value: digest,
62+
});
63+
toast.success('Transaction digest copied to clipboard');
64+
}}
6065
/>
6166
</div>
6267
</div>

apps/wallet/src/ui/app/hooks/useCopyToClipboard.ts

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,14 +4,18 @@
44

55
import { useCallback, type MouseEventHandler } from 'react';
66
import { toast } from '@iota/core';
7+
import { ampli } from '_src/shared/analytics/ampli';
78

89
export type CopyOptions = {
910
copySuccessMessage?: string;
11+
textType: string;
12+
trackEvent?: boolean;
13+
isPublic?: boolean;
1014
};
1115

1216
export function useCopyToClipboard(
1317
textToCopy: string,
14-
{ copySuccessMessage = 'Copied' }: CopyOptions = {},
18+
{ copySuccessMessage = 'Copied', textType, isPublic = false, trackEvent = true }: CopyOptions,
1519
) {
1620
return useCallback<MouseEventHandler>(
1721
async (e) => {
@@ -20,10 +24,17 @@ export function useCopyToClipboard(
2024
try {
2125
await navigator.clipboard.writeText(textToCopy);
2226
toast(copySuccessMessage);
27+
if (trackEvent) {
28+
ampli.elementCopied({
29+
type: textType,
30+
value: isPublic ? textToCopy : undefined,
31+
visibility: isPublic ? 'public' : 'private',
32+
});
33+
}
2334
} catch (e) {
2435
// silence clipboard errors
2536
}
2637
},
27-
[textToCopy, copySuccessMessage],
38+
[textToCopy, copySuccessMessage, textType, isPublic, trackEvent],
2839
);
2940
}

apps/wallet/src/ui/app/pages/accounts/manage/AccountGroupItem.tsx

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ import { RemoveDialog } from './RemoveDialog';
1616
import { Portal } from '_app/shared/Portal';
1717
import { formatAccountName } from '_src/ui/app/helpers';
1818
import { isLegacyAccount } from '_src/background/accounts/isLegacyAccount';
19-
import { ACCOUNT_TYPE_TO_AMPLI_ACCOUNT_TYPE, ampli } from '_src/shared/analytics';
19+
import { ampli, ACCOUNT_TYPE_TO_AMPLI_ACCOUNT_TYPE } from '_src/shared/analytics';
2020

2121
interface AccountGroupItemProps {
2222
account: SerializedUIAccount;
@@ -50,6 +50,10 @@ export function AccountGroupItem({
5050
});
5151

5252
async function handleCopySuccess() {
53+
ampli.elementCopied({
54+
type: 'address',
55+
value: account.address,
56+
});
5357
toast('Address copied');
5458
}
5559

0 commit comments

Comments
 (0)