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
14 changes: 14 additions & 0 deletions apps/evm-bridge/ampli.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
{
"Zone": "eu",
"OrgId": "100007351",
"WorkspaceId": "c8601ae9-a355-4ca7-8db4-82c9a2153122",
"SourceId": "d91b6563-334b-4db1-92a5-9e364f5d2ca2",
"Branch": "main",
"Version": "1.0.0",
"VersionId": "57f759dd-f594-4c8d-8f19-d4a3e07129e8",
"Runtime": "browser:typescript-ampli-v2",
"Platform": "Browser",
"Language": "TypeScript",
"SDK": "@amplitude/analytics-browser@^1.0",
"Path": "./src/shared/analytics/ampli"
}
121 changes: 121 additions & 0 deletions apps/evm-bridge/amplitude-events-tracking.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
# Amplitude Events Tracking List for IOTA EVM Bridge

This document outlines user action events to track with Amplitude for analytics and user behavior analysis in the IOTA EVM Bridge application.

## Table of Contents

1. [Wallet Actions](#wallet-actions)
2. [Bridge Configuration](#bridge-configuration)
3. [Bridge Transactions](#bridge-transactions)
4. [Form Interactions](#form-interactions)
5. [Error Events](#error-events)

---

## Wallet Actions

- **Event**: `user connected l1 wallet`
- **Description**: User successfully connected their IOTA L1 wallet
- **Properties**: `wallet_type`, `address`

- **Event**: `user connected l2 wallet`
- **Description**: User successfully connected their L2 wallet (MetaMask/RainbowKit)
- **Properties**: `wallet_type`, `address`, `chain_id`

- **Event**: `user requested faucet funds`
- **Description**: User clicked to request test funds from faucet
- **Properties**: `address`, `success`

---

## Bridge Configuration

- **Event**: `user toggled bridge direction`
- **Description**: User switched bridge direction between L1→L2 and L2→L1
- **Properties**: `from_layer`, `to_layer`

- **Event**: `user selected coin`
- **Description**: User selected a different coin type for bridging
- **Properties**: `coin_type`, `coin_symbol`, `bridge_direction`

- **Event**: `user clicked max amount`
- **Description**: User clicked "Max" button to use full available balance
- **Properties**: `coin_type`, `bridge_direction`, `max_amount`

---

## Bridge Transactions

- **Event**: `user sent from l1 to l2`
- **Description**: User initiated a deposit transaction from IOTA L1 to IOTA EVM
- **Properties**: `amount`, `coin_type`, `receiving_address`, `gas_estimate`

- **Event**: `user sent from l2 to l1`
- **Description**: User initiated a withdraw transaction from IOTA EVM to IOTA L1
- **Properties**: `amount`, `coin_type`, `receiving_address`, `gas_estimate`

- **Event**: `user cancelled transaction`
- **Description**: User rejected/cancelled transaction in wallet
- **Properties**: `transaction_type`, `amount`, `coin_type`

---

## Form Interactions

- **Event**: `user entered amount`
- **Description**: User manually entered a bridge amount
- **Properties**: `amount`, `coin_type`, `bridge_direction`

- **Event**: `user toggled address input`
- **Description**: User switched between manual address entry and wallet auto-fill
- **Properties**: `input_mode` (manual, auto), `bridge_direction`

- **Event**: `user entered receiving address`
- **Description**: User manually entered a receiving address
- **Properties**: `address_format` (iota, evm), `bridge_direction`

---

## Error Events

- **Event**: `user encountered insufficient balance`
- **Description**: User attempted to bridge more than available balance
- **Properties**: `requested_amount`, `available_balance`, `coin_type`

- **Event**: `user entered invalid address`
- **Description**: User entered an invalid receiving address format
- **Properties**: `address_format` (iota, evm), `bridge_direction`

- **Event**: `user experienced transaction failure`
- **Description**: User's transaction failed after submission
- **Properties**: `transaction_type`, `error_type`, `amount`, `coin_type`

---

## Event Properties Schema

### Common Properties (included in all events)
- `timestamp`: ISO 8601 timestamp
- `session_id`: Unique session identifier
- `user_id`: Anonymous user identifier
- `app_version`: Application version
- `environment`: deployment environment (development, testnet, mainnet)
- `user_agent`: Browser user agent string
- `viewport_size`: Browser viewport dimensions

### Bridge-Specific Properties
- `bridge_direction`: "l1_to_l2" | "l2_to_l1"
- `coin_type`: Full coin type identifier
- `coin_symbol`: Human-readable coin symbol (IOTA, etc.)
- `amount`: Numeric amount in human-readable format
- `address`: Wallet address (anonymized if needed)
- `wallet_type`: Type of connected wallet (iota_wallet, metamask, rainbow, etc.)
- `chain_id`: EVM chain identifier
- `gas_estimate`: Estimated gas cost
- `input_mode`: "manual" | "auto"
- `address_format`: "iota" | "evm"
- `transaction_type`: "deposit" | "withdraw"
- `error_type`: Categorized error type
- `from_layer`: "l1" | "l2"
- `to_layer`: "l1" | "l2"
- `success`: boolean indicating operation success
7 changes: 6 additions & 1 deletion apps/evm-bridge/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,12 @@
"test:prepare:L2": "vite-node scripts/downloadWallet.js",
"test:prepare": "pnpm test:prepare:L1 && pnpm test:prepare:L2",
"playwright": "playwright",
"test:e2e": "playwright test"
"test:e2e": "playwright test",
"ampli": "ampli",
"pull-amplitude": "ampli pull web"
},
"dependencies": {
"@amplitude/analytics-browser": "^1.10.3",
"@growthbook/growthbook": "^1.0.0",
"@growthbook/growthbook-react": "^1.0.0",
"@hookform/resolvers": "^3.9.0",
Expand All @@ -47,6 +50,8 @@
"zustand": "^4.4.1"
},
"devDependencies": {
"@amplitude/ampli": "^1.35.0",
"@amplitude/analytics-types": "^0.20.0",
"@playwright/test": "^1.46.1",
"@types/node": "^20.14.10",
"@types/react": "^18.3.3",
Expand Down
177 changes: 177 additions & 0 deletions apps/evm-bridge/src/shared/analytics/ampli/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,177 @@
/* tslint:disable */
/* eslint-disable */
// @ts-nocheck
/**
* Ampli - A strong typed wrapper for your Analytics
*
* This file is generated by Amplitude.
* To update run 'ampli pull web'
*
* Required dependencies: @amplitude/analytics-browser@^1.3.0
* Tracking Plan Version: 1
* Build: 1.0.0
* Runtime: browser:typescript-ampli-v2
*
* [View Tracking Plan](https://data.eu.amplitude.com/iota-foundation/IOTA%20EVM%20Bridge/events/main/latest)
*
* [Full Setup Instructions](https://data.eu.amplitude.com/iota-foundation/IOTA%20EVM%20Bridge/implementation/web)
*/

import * as amplitude from '@amplitude/analytics-browser';

export type Environment = 'iotaevmbridge';

export const ApiKey: Record<Environment, string> = {
iotaevmbridge: 'bc860617cd112db8797d4b8809b15142'
};

/**
* Default Amplitude configuration options. Contains tracking plan information.
*/
export const DefaultConfiguration: BrowserOptions = {
plan: {
version: '1',
branch: 'main',
source: 'web',
versionId: '57f759dd-f594-4c8d-8f19-d4a3e07129e8'
},
...{
ingestionMetadata: {
sourceName: 'browser-typescript-ampli',
sourceVersion: '2.0.0'
}
},
serverZone: amplitude.Types.ServerZone.EU
};

export interface LoadOptionsBase { disabled?: boolean }

export type LoadOptionsWithEnvironment = LoadOptionsBase & { environment: Environment; client?: { configuration?: BrowserOptions; }; };
export type LoadOptionsWithApiKey = LoadOptionsBase & { client: { apiKey: string; configuration?: BrowserOptions; } };
export type LoadOptionsWithClientInstance = LoadOptionsBase & { client: { instance: BrowserClient; } };

export type LoadOptions = LoadOptionsWithEnvironment | LoadOptionsWithApiKey | LoadOptionsWithClientInstance;

export type PromiseResult<T> = { promise: Promise<T | void> };

const getVoidPromiseResult = () => ({ promise: Promise.resolve() });

// prettier-ignore
export class Ampli {
private disabled: boolean = false;
private amplitude?: BrowserClient;

get client(): BrowserClient {
this.isInitializedAndEnabled();
return this.amplitude!;
}

get isLoaded(): boolean {
return this.amplitude != null;
}

private isInitializedAndEnabled(): boolean {
if (!this.amplitude) {
console.error('ERROR: Ampli is not yet initialized. Have you called ampli.load() on app start?');
return false;
}
return !this.disabled;
}

/**
* Initialize the Ampli SDK. Call once when your application starts.
*
* @param options Configuration options to initialize the Ampli SDK with.
*/
load(options: LoadOptions): PromiseResult<void> {
this.disabled = options.disabled ?? false;

if (this.amplitude) {
console.warn('WARNING: Ampli is already initialized. Ampli.load() should be called once at application startup.');
return getVoidPromiseResult();
}

let apiKey: string | null = null;
if (options.client && 'apiKey' in options.client) {
apiKey = options.client.apiKey;
} else if ('environment' in options) {
apiKey = ApiKey[options.environment];
}

if (options.client && 'instance' in options.client) {
this.amplitude = options.client.instance;
} else if (apiKey) {
this.amplitude = amplitude.createInstance();
const configuration = (options.client && 'configuration' in options.client) ? options.client.configuration : {};
return this.amplitude.init(apiKey, undefined, { ...DefaultConfiguration, ...configuration });
} else {
console.error("ERROR: ampli.load() requires 'environment', 'client.apiKey', or 'client.instance'");
}

return getVoidPromiseResult();
}

/**
* Identify a user and set user properties.
*
* @param userId The user's id.
* @param options Optional event options.
*/
identify(
userId: string | undefined,
options?: EventOptions,
): PromiseResult<Result> {
if (!this.isInitializedAndEnabled()) {
return getVoidPromiseResult();
}

if (userId) {
options = {...options, user_id: userId};
}

const amplitudeIdentify = new amplitude.Identify();
return this.amplitude!.identify(
amplitudeIdentify,
options,
);
}

/**
* Flush the event.
*/
flush() : PromiseResult<Result> {
if (!this.isInitializedAndEnabled()) {
return getVoidPromiseResult();
}

return this.amplitude!.flush();
}

/**
* Track event
*
* @param event The event to track.
* @param options Optional event options.
*/
track(event: Event, options?: EventOptions): PromiseResult<Result> {
if (!this.isInitializedAndEnabled()) {
return getVoidPromiseResult();
}

return this.amplitude!.track(event, undefined, options);
}

}

export const ampli = new Ampli();

// BASE TYPES
type BrowserOptions = amplitude.Types.BrowserOptions;

export type BrowserClient = amplitude.Types.BrowserClient;
export type BaseEvent = amplitude.Types.BaseEvent;
export type IdentifyEvent = amplitude.Types.IdentifyEvent;
export type GroupEvent = amplitude.Types.GroupIdentifyEvent;
export type Event = amplitude.Types.Event;
export type EventOptions = amplitude.Types.EventOptions;
export type Result = amplitude.Types.Result;
44 changes: 44 additions & 0 deletions apps/evm-bridge/src/shared/analytics/amplitude.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
// Copyright (c) 2024 IOTA Stiftung
// SPDX-License-Identifier: Apache-2.0

import * as amplitude from '@amplitude/analytics-browser';
import { LogLevel, TransportType, type UserSession } from '@amplitude/analytics-types';
import { PersistableStorage } from '@iota/core';

import { ampli } from './ampli';

const IS_PROD_ENV = import.meta.env.VITE_BUILD_ENV === 'production';

export const persistableStorage = new PersistableStorage<UserSession>();

const ApiKey = {
production: 'placeholder-production-api-key',
development: 'placeholder-development-api-key',
};

export async function initAmplitude() {
await ampli.load({
environment: 'iotaevmbridge',
// Flip this if you'd like to test Amplitude locally
disabled: !IS_PROD_ENV,
client: {
configuration: {
cookieStorage: persistableStorage,
logLevel: IS_PROD_ENV ? LogLevel.Warn : amplitude.Types.LogLevel.Debug,
},
},
});

window.addEventListener('pagehide', () => {
amplitude.setTransport(TransportType.SendBeacon);
amplitude.flush();
});
}

export function getUrlWithDeviceId(url: URL) {
const deviceId = ampli.client.getDeviceId();
if (deviceId) {
url.searchParams.set('amplitude_device_id', deviceId);
}
return url;
}
5 changes: 5 additions & 0 deletions apps/evm-bridge/src/shared/analytics/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
// Copyright (c) 2024 IOTA Stiftung
// SPDX-License-Identifier: Apache-2.0

export * from './ampli';
export * from './amplitude';
Loading
Loading