diff --git a/.woodpecker/e2e.yml b/.woodpecker/e2e.yml index 9e1489f..3163697 100644 --- a/.woodpecker/e2e.yml +++ b/.woodpecker/e2e.yml @@ -228,6 +228,15 @@ services: echo "@harb/utils linked with viem dep" fi + # Overlay @harb/analytics shared package from workspace + if [ -d "$WS/packages/analytics" ]; then + mkdir -p /app/packages/analytics + cp -r "$WS/packages/analytics/." /app/packages/analytics/ + mkdir -p /app/web-app/node_modules/@harb + ln -sf /app/packages/analytics /app/web-app/node_modules/@harb/analytics + echo "@harb/analytics linked for webapp" + fi + echo "=== Starting webapp (pre-built image + source overlay) ===" cd /app/web-app # Explicitly set CI=true to disable Vue DevTools in vite.config.ts @@ -285,6 +294,15 @@ services: echo "@harb/ui-shared linked for landing" fi + # Overlay @harb/analytics shared package from workspace + if [ -d "$WS/packages/analytics" ]; then + mkdir -p /app/packages/analytics + cp -r "$WS/packages/analytics/." /app/packages/analytics/ + mkdir -p /app/landing/node_modules/@harb + ln -sf /app/packages/analytics /app/landing/node_modules/@harb/analytics + echo "@harb/analytics linked for landing" + fi + echo "=== Starting landing (pre-built image + source overlay) ===" cd /app/landing exec npm run dev -- --host 0.0.0.0 --port 5174 diff --git a/containers/Caddyfile b/containers/Caddyfile index e6b88dc..eca7688 100644 --- a/containers/Caddyfile +++ b/containers/Caddyfile @@ -18,5 +18,9 @@ uri strip_prefix /api/txn reverse_proxy txn-bot:43069 } + route /analytics* { + uri strip_prefix /analytics + reverse_proxy umami:3000 + } reverse_proxy landing:5174 } diff --git a/containers/init-umami-db.sh b/containers/init-umami-db.sh new file mode 100755 index 0000000..e7dff02 --- /dev/null +++ b/containers/init-umami-db.sh @@ -0,0 +1,16 @@ +#!/bin/bash +# Creates the umami database and user if they don't already exist. +# Mounted as a postgres init script via docker-compose volumes. +set -e + +psql -v ON_ERROR_STOP=1 --username "$POSTGRES_USER" --dbname "$POSTGRES_DB" <<-EOSQL + DO \$\$ + BEGIN + IF NOT EXISTS (SELECT FROM pg_catalog.pg_roles WHERE rolname = 'umami') THEN + CREATE ROLE umami WITH LOGIN PASSWORD 'umami_local'; + END IF; + END + \$\$; + SELECT 'CREATE DATABASE umami OWNER umami' + WHERE NOT EXISTS (SELECT FROM pg_database WHERE datname = 'umami')\gexec +EOSQL diff --git a/containers/landing-entrypoint.sh b/containers/landing-entrypoint.sh index efacad8..79f7d2f 100755 --- a/containers/landing-entrypoint.sh +++ b/containers/landing-entrypoint.sh @@ -26,4 +26,7 @@ if [[ -f "$CONTRACTS_ENV" ]]; then echo "[landing-entrypoint] Contract addresses loaded: KRK=${KRAIKEN:-unset} STAKE=${STAKE:-unset}" fi +export VITE_UMAMI_URL="${VITE_UMAMI_URL:-}" +export VITE_UMAMI_WEBSITE_ID="${VITE_UMAMI_WEBSITE_ID:-}" + exec npm run dev -- --host 0.0.0.0 --port 5174 diff --git a/containers/webapp-entrypoint.sh b/containers/webapp-entrypoint.sh index 5c612a1..67c277a 100755 --- a/containers/webapp-entrypoint.sh +++ b/containers/webapp-entrypoint.sh @@ -34,6 +34,8 @@ if [[ "${CI:-}" == "true" ]]; then export VITE_SWAP_ROUTER=${VITE_SWAP_ROUTER:-0x94cC0AaC535CCDB3C01d6787D6413C739ae12bc4} export VITE_PONDER_BASE_SEPOLIA_LOCAL_FORK=${VITE_PONDER_BASE_SEPOLIA_LOCAL_FORK:-/api/graphql} export VITE_TXNBOT_BASE_SEPOLIA_LOCAL_FORK=${VITE_TXNBOT_BASE_SEPOLIA_LOCAL_FORK:-/api/txn} + export VITE_UMAMI_URL="${VITE_UMAMI_URL:-}" + export VITE_UMAMI_WEBSITE_ID="${VITE_UMAMI_WEBSITE_ID:-}" echo "[webapp-ci] Environment configured:" echo " VITE_KRAIKEN_ADDRESS: ${VITE_KRAIKEN_ADDRESS}" @@ -81,5 +83,7 @@ export VITE_SWAP_ROUTER=$SWAP_ROUTER export VITE_PONDER_BASE_SEPOLIA_LOCAL_FORK=${VITE_PONDER_BASE_SEPOLIA_LOCAL_FORK:-/api/graphql} export VITE_TXNBOT_BASE_SEPOLIA_LOCAL_FORK=${VITE_TXNBOT_BASE_SEPOLIA_LOCAL_FORK:-/api/txn} export CHOKIDAR_USEPOLLING=${CHOKIDAR_USEPOLLING:-1} +export VITE_UMAMI_URL="${VITE_UMAMI_URL:-}" +export VITE_UMAMI_WEBSITE_ID="${VITE_UMAMI_WEBSITE_ID:-}" exec npm run dev -- --host 0.0.0.0 --port 5173 --base /app/ diff --git a/docker-compose.yml b/docker-compose.yml index b73a366..7fcee69 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -52,6 +52,7 @@ services: - POSTGRES_DB=ponder_local volumes: - postgres-data:/var/lib/postgresql/data + - ./containers/init-umami-db.sh:/docker-entrypoint-initdb.d/init-umami-db.sh:ro,z expose: - "5432" restart: unless-stopped @@ -133,6 +134,8 @@ services: - CHOKIDAR_USEPOLLING=1 - GIT_BRANCH=${GIT_BRANCH:-} - VITE_ENABLE_LOCAL_SWAP=true + - VITE_UMAMI_URL=${VITE_UMAMI_URL:-} + - VITE_UMAMI_WEBSITE_ID=${VITE_UMAMI_WEBSITE_ID:-} expose: - "5173" ports: @@ -167,6 +170,8 @@ services: - CHOKIDAR_USEPOLLING=1 - GIT_BRANCH=${GIT_BRANCH:-} - VITE_APP_URL=http://localhost:5173/app + - VITE_UMAMI_URL=${VITE_UMAMI_URL:-} + - VITE_UMAMI_WEBSITE_ID=${VITE_UMAMI_WEBSITE_ID:-} expose: - "5174" restart: unless-stopped @@ -228,6 +233,30 @@ services: retries: 3 start_period: 2s + umami: + image: ghcr.io/umami-software/umami:postgresql-latest + environment: + - DATABASE_URL=postgresql://umami:umami_local@postgres:5432/umami + - APP_SECRET=${UMAMI_APP_SECRET:-harb-analytics-secret} + - DISABLE_TELEMETRY=1 + expose: + - "3000" + ports: + - "127.0.0.1:3000:3000" + restart: unless-stopped + networks: + - harb-network + depends_on: + postgres: + condition: service_healthy + logging: *default-logging + healthcheck: + test: ["CMD", "wget", "--spider", "-q", "http://127.0.0.1:3000/api/heartbeat"] + interval: 5s + timeout: 3s + retries: 10 + start_period: 15s + otterscan: image: otterscan/otterscan:v2.6.0 environment: diff --git a/docs/ENVIRONMENT.md b/docs/ENVIRONMENT.md index 8b31b0e..d96b238 100644 --- a/docs/ENVIRONMENT.md +++ b/docs/ENVIRONMENT.md @@ -16,6 +16,7 @@ Docker Compose services (in startup order): | **webapp** | Staking app (Vue 3) | 5173 | HTTP response | | **txn-bot** | Automated `recenter()` and `payTax()` upkeep ([services/txnBot/](../services/txnBot/)) | 43069 | Process alive | | **caddy** | Reverse proxy / TLS | 80/443 | — | +| **umami** | Self-hosted analytics (Umami) | 3000 | HTTP /api/heartbeat | | **otterscan** | Block explorer | 5100 | — | ## txnBot Service @@ -119,6 +120,7 @@ docker inspect landing --format '{{.NetworkSettings.Networks.harb_harb-network.I - **Ponder GraphQL:** `http://localhost:42069/graphql` - **Anvil RPC:** `http://localhost:8545` - **txnBot status:** `http://localhost:43069/status` +- **Umami analytics:** `http://localhost:3000` (default login: `admin` / `umami`) ## Resource Notes @@ -140,6 +142,39 @@ For testing login: `lobsterDao`, `test123`, `lobster-x010syqe?412!` | `VITE_KRAIKEN_ADDRESS` | from `deployments-local.json` | via `contracts.env` + entrypoint | Override KRK token address. | | `VITE_STAKE_ADDRESS` | from `deployments-local.json` | via `contracts.env` + entrypoint | Override Stake contract address. | | `VITE_DEFAULT_CHAIN_ID` | auto-detected (31337 on localhost) | — | Force the default chain. | +| `VITE_UMAMI_URL` | unset | via env | Full URL to Umami `script.js` (e.g. `https://analytics.kraiken.org/script.js`). Omit to disable analytics. | +| `VITE_UMAMI_WEBSITE_ID` | unset | via env | Umami website ID (UUID). Required alongside `VITE_UMAMI_URL`. | + +## Analytics (Umami) + +Self-hosted [Umami](https://umami.is/) provides privacy-respecting funnel analytics with no third-party tracking. The `umami` Docker service shares the `postgres` instance (separate `umami` database created by `containers/init-umami-db.sh`). + +### Setup + +1. Start the stack — Umami comes up automatically. +2. Open `http://localhost:3000` and log in (default: `admin` / `umami`). Change the password on first login. +3. Add a website in Umami and copy the **Website ID** (UUID). +4. Set the env vars before starting landing/webapp: + ```bash + export VITE_UMAMI_URL=http://localhost:3000/script.js + export VITE_UMAMI_WEBSITE_ID= + ``` + For staging/production behind Caddy, use the `/analytics/script.js` path instead. + +### Tracked funnel events + +| Event | App | Trigger | +|-------|-----|---------| +| `cta_click` | landing | User clicks a CTA button (label in event data) | +| `wallet_connect` | web-app | Wallet connected for the first time | +| `swap_initiated` | web-app | User submits a buy or sell swap (direction in event data) | +| `stake_created` | web-app | Stake position successfully created | + +Page views are tracked automatically by the Umami script on every route change. + +### Production deployment + +On `harb-staging`, set `VITE_UMAMI_URL` and `VITE_UMAMI_WEBSITE_ID` in the environment and configure `UMAMI_APP_SECRET` to a strong random value. The Caddy route `/analytics*` proxies to the Umami container. ## Contract Addresses diff --git a/landing/index.html b/landing/index.html index 5e94391..ae4ffc8 100644 --- a/landing/index.html +++ b/landing/index.html @@ -5,7 +5,6 @@ KrAIken -
diff --git a/landing/package.json b/landing/package.json index e724ca8..c8266da 100644 --- a/landing/package.json +++ b/landing/package.json @@ -17,6 +17,7 @@ "prepare": "husky" }, "dependencies": { + "@harb/analytics": "*", "@harb/web3": "*", "@harb/ui-shared": "*", "@tanstack/vue-query": "^5.92.9", diff --git a/landing/src/main.ts b/landing/src/main.ts index cf3d3ae..16f9adb 100644 --- a/landing/src/main.ts +++ b/landing/src/main.ts @@ -5,9 +5,12 @@ import { WagmiPlugin } from '@wagmi/vue'; // that Wagmi uses internally for its reactive data hooks (useAccount, useConnect, etc.). import { VueQueryPlugin } from '@tanstack/vue-query'; import { createHarbConfig } from '@harb/web3'; +import { initAnalytics } from '@harb/analytics'; import App from './App.vue'; import router from './router'; +initAnalytics(import.meta.env.VITE_UMAMI_URL, import.meta.env.VITE_UMAMI_WEBSITE_ID); + const rpcUrl = import.meta.env.VITE_LOCAL_RPC_URL ?? '/api/rpc'; const config = createHarbConfig({ rpcUrl }); diff --git a/landing/src/views/HomeView.vue b/landing/src/views/HomeView.vue index 8b0be16..83b9108 100644 --- a/landing/src/views/HomeView.vue +++ b/landing/src/views/HomeView.vue @@ -10,7 +10,7 @@ $KRK has a price floor backed by real ETH. The protocol adapts automatically. You just hold.
- Get $KRK + Get $KRK
@@ -38,7 +38,7 @@
- Get $KRK + Get $KRK
@@ -51,7 +51,7 @@

Watch the protocol in real time. Supply dynamics, ETH reserves, position history — all live, no wallet needed.

- View Protocol + View Protocol
@@ -68,8 +68,8 @@
  • Hold. The protocol does the rest.
  • - Get $KRK - How It Works → + Get $KRK + How It Works →
    @@ -93,11 +93,17 @@ import LiveStats from '@/components/LiveStats.vue'; import SecurityInfo from '@/components/SecurityInfo.vue'; import WalletCard from '@/components/WalletCard.vue'; import { useMobile } from '@/composables/useMobile'; +import { trackCtaClick } from '@harb/analytics'; import { useRouter } from 'vue-router'; const isMobile = useMobile(); const router = useRouter(); +const navigateCta = (path: string, label: string) => { + trackCtaClick(label); + router.push(path); +}; + const openExternal = (url: string) => { window.open(url, '_blank', 'noopener'); }; diff --git a/package-lock.json b/package-lock.json index 488f411..78d1021 100644 --- a/package-lock.json +++ b/package-lock.json @@ -69,6 +69,7 @@ "name": "vue-kraiken", "version": "0.0.0", "dependencies": { + "@harb/analytics": "*", "@harb/ui-shared": "*", "@harb/web3": "*", "@tanstack/vue-query": "^5.92.9", @@ -279,7 +280,6 @@ "integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@babel/code-frame": "^7.29.0", "@babel/generator": "^7.29.0", @@ -938,7 +938,6 @@ } ], "license": "MIT", - "peer": true, "engines": { "node": ">=20.19.0" }, @@ -979,7 +978,6 @@ } ], "license": "MIT", - "peer": true, "engines": { "node": ">=20.19.0" } @@ -3360,6 +3358,10 @@ "graphql": "^0.8.0 || ^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" } }, + "node_modules/@harb/analytics": { + "resolved": "packages/analytics", + "link": true + }, "node_modules/@harb/ui-shared": { "resolved": "packages/ui-shared", "link": true @@ -4228,7 +4230,6 @@ "resolved": "https://registry.npmjs.org/@noble/ciphers/-/ciphers-1.2.1.tgz", "integrity": "sha512-rONPWMC7PeExE077uLE4oqWrZ1IvAfz3oH9LibVAcVCopJiA9R62uavnbEzdkVmJYI6M6Zgkbeb07+tWjlq2XA==", "license": "MIT", - "peer": true, "engines": { "node": "^14.21.3 || >=16" }, @@ -5291,7 +5292,6 @@ "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.0.tgz", "integrity": "sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==", "license": "MIT", - "peer": true, "engines": { "node": ">=10.0.0" }, @@ -5637,7 +5637,6 @@ "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.0.tgz", "integrity": "sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==", "license": "MIT", - "peer": true, "engines": { "node": ">=10.0.0" }, @@ -5935,7 +5934,6 @@ "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.0.tgz", "integrity": "sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==", "license": "MIT", - "peer": true, "engines": { "node": ">=10.0.0" }, @@ -6430,7 +6428,6 @@ "integrity": "sha512-RnO1SaiCFHn666wNz2QfZEFxvmiNRqhzaMXHXxXXKt+MEP7aajlPxUSMIQpKAaJfverpovEYqjBOXDq6dDcaOQ==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@typescript-eslint/utils": "^8.13.0", "eslint-visitor-keys": "^4.2.0", @@ -6673,6 +6670,66 @@ "node": ">=14.0.0" } }, + "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@emnapi/core": { + "version": "1.4.5", + "dev": true, + "inBundle": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/wasi-threads": "1.0.4", + "tslib": "^2.4.0" + } + }, + "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@emnapi/runtime": { + "version": "1.4.5", + "dev": true, + "inBundle": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@emnapi/wasi-threads": { + "version": "1.0.4", + "dev": true, + "inBundle": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@napi-rs/wasm-runtime": { + "version": "0.2.12", + "dev": true, + "inBundle": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "^1.4.3", + "@emnapi/runtime": "^1.4.3", + "@tybys/wasm-util": "^0.10.0" + } + }, + "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@tybys/wasm-util": { + "version": "0.10.0", + "dev": true, + "inBundle": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/tslib": { + "version": "2.8.0", + "dev": true, + "inBundle": true, + "license": "0BSD", + "optional": true + }, "node_modules/@tailwindcss/oxide-win32-arm64-msvc": { "version": "4.1.13", "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.1.13.tgz", @@ -6840,7 +6897,6 @@ "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.12.tgz", "integrity": "sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ==", "license": "MIT", - "peer": true, "dependencies": { "@types/ms": "*" } @@ -6888,7 +6944,6 @@ "resolved": "https://registry.npmjs.org/@types/lodash-es/-/lodash-es-4.17.12.tgz", "integrity": "sha512-0NgftHUcV4v34VhXm8QBSftKVXtbkBG3ViCjs6+eJ5a6y6Mi/jiFGPc1sC7QK+9BFhWrURE3EOggmWaSxL9OzQ==", "license": "MIT", - "peer": true, "dependencies": { "@types/lodash": "*" } @@ -6966,7 +7021,6 @@ "integrity": "sha512-IgSWvLobTDOjnaxAfDTIHaECbkNlAlKv2j5SjpB2v7QHKv1FIfjwMy8FsDbVfDX/KjmCmYICcw7uGaXLhtsLNg==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "8.56.0", "@typescript-eslint/types": "8.56.0", @@ -7874,7 +7928,6 @@ "resolved": "https://registry.npmjs.org/@wagmi/core/-/core-2.22.1.tgz", "integrity": "sha512-cG/xwQWsBEcKgRTkQVhH29cbpbs/TdcUJVFXCyri3ZknxhMyGv0YEjTcrNpRgt2SaswL1KrvslSNYKKo+5YEAg==", "license": "MIT", - "peer": true, "dependencies": { "eventemitter3": "5.0.1", "mipd": "0.0.7", @@ -8476,7 +8529,6 @@ "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.0.tgz", "integrity": "sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==", "license": "MIT", - "peer": true, "engines": { "node": ">=10.0.0" }, @@ -8679,7 +8731,6 @@ "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", "dev": true, "license": "MIT", - "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -9174,7 +9225,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "baseline-browser-mapping": "^2.9.0", "caniuse-lite": "^1.0.30001759", @@ -9238,7 +9288,6 @@ "integrity": "sha512-ZMANVnAixE6AWWnPzlW2KpUrxhm9woycYvPOo67jWHyFowASTEd9s+QN1EIMsSDtwhIxN4sWE1jotpuDUIgyIw==", "hasInstallScript": true, "license": "MIT", - "peer": true, "dependencies": { "node-gyp-build": "^4.3.0" }, @@ -9503,7 +9552,6 @@ "resolved": "https://registry.npmjs.org/chart.js/-/chart.js-4.5.1.tgz", "integrity": "sha512-GIjfiT9dbmHRiYi6Nl2yFCq7kkwdkp1W/lp2J99rX0yo9tgJGn3lKQATztIjb5tVtevcBtIdICNWqlq5+E8/Pw==", "license": "MIT", - "peer": true, "dependencies": { "@kurkle/color": "^0.3.0" }, @@ -9943,7 +9991,6 @@ "resolved": "https://registry.npmjs.org/cross-fetch/-/cross-fetch-4.1.0.tgz", "integrity": "sha512-uKm5PU+MHTootlWEY+mZ4vvXoCn4fLQxT9dSc1sXVMSFkINTJVN8cAQROpwcKm8bJ/c7rgZVIBWzH5T78sNZZw==", "license": "MIT", - "peer": true, "dependencies": { "node-fetch": "^2.7.0" } @@ -9980,7 +10027,6 @@ "resolved": "https://registry.npmjs.org/crossws/-/crossws-0.3.5.tgz", "integrity": "sha512-ojKiDvcmByhwa8YYqbQI/hg7MEU0NC03+pSdEq4ZUnZR9xXpwk7E43SMNGkn+JxJGPFtNvQ48+vV2p+P1ml5PA==", "license": "MIT", - "peer": true, "dependencies": { "uncrypto": "^0.1.3" } @@ -10452,7 +10498,6 @@ "resolved": "https://registry.npmjs.org/eciesjs/-/eciesjs-0.4.17.tgz", "integrity": "sha512-TOOURki4G7sD1wDCjj7NfLaXZZ49dFOeEb5y39IXpb8p0hRzVvfvzZHOi5JcT+PpyAbi/Y+lxPb8eTag2WYH8w==", "license": "MIT", - "peer": true, "dependencies": { "@ecies/ciphers": "^0.2.5", "@noble/ciphers": "^1.3.0", @@ -10869,7 +10914,6 @@ "integrity": "sha512-LEyamqS7W5HB3ujJyvi0HQK/dtVINZvd5mAAp9eT5S/ujByGjiZLCzPcHVzuXbpJDJF/cxwHlfceVUDZ2lnSTw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", @@ -10946,7 +10990,6 @@ "integrity": "sha512-f1J/tcbnrpgC8suPN5AtdJ5MQjuXbSU9pGRSSYAuF3SHoiYCOdEX6O22pLaRyLHXvDcOe+O5ENgc1owQ587agA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.4.0", "natural-compare": "^1.4.0", @@ -11350,8 +11393,7 @@ "version": "6.4.9", "resolved": "https://registry.npmjs.org/eventemitter2/-/eventemitter2-6.4.9.tgz", "integrity": "sha512-JEPTiaOt9f04oa6NOkc4aH+nVp5I3wEjpHbIPqfgCdD5v5bUzy7xQqwcVO2aDQgOWhI28da57HksMrzK9HlRxg==", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/eventemitter3": { "version": "5.0.1", @@ -11446,7 +11488,6 @@ "integrity": "sha512-DT9ck5YIRU+8GYzzU5kT3eHGA5iL+1Zd0EutOmTE9Dtk+Tvuzd23VBU+ec7HPNSTxXYO55gPV/hq4pSBJDjFpA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "accepts": "^2.0.0", "body-parser": "^2.2.0", @@ -12223,7 +12264,6 @@ "resolved": "https://registry.npmjs.org/graphql/-/graphql-16.12.0.tgz", "integrity": "sha512-DKKrynuQRne0PNpEbzuEdHlYOMksHSUI8Zc9Unei5gTsMNA2/vMpoMz/yKba50pejK56qj98qM0SjYxAKi13gQ==", "license": "MIT", - "peer": true, "engines": { "node": "^12.22.0 || ^14.16.0 || ^16.0.0 || >=17.0.0" } @@ -12305,7 +12345,6 @@ "integrity": "sha512-yoLRW+KRlDmnnROdAu7sX77VNLC0bsFoZyGQJLy1cF+X/SkLg/fWkRGrEEYQK8o2cafJ2wmEaMqMEZB3U3DYDg==", "devOptional": true, "license": "MIT", - "peer": true, "engines": { "node": ">=20" }, @@ -12359,7 +12398,6 @@ "integrity": "sha512-UVIHeVhxmxedbWPCfgS55Jg2rDfwf2BCKeylcPSqazLz5w3Kri7Q4xdBJubsr/+VUzFLh0VjIvh13RaDA2/Xug==", "devOptional": true, "license": "MIT", - "peer": true, "dependencies": { "webidl-conversions": "^7.0.0", "whatwg-mimetype": "^3.0.0" @@ -13382,7 +13420,6 @@ "integrity": "sha512-mjzqwWRD9Y1J1KUi7W97Gja1bwOOM5Ug0EZ6UDK3xS7j7mndrkwozHtSblfomlzyB4NepioNt+B2sOSzczVgtQ==", "devOptional": true, "license": "MIT", - "peer": true, "dependencies": { "@acemir/cssom": "^0.9.28", "@asamuzakjp/dom-selector": "^6.7.6", @@ -13685,7 +13722,6 @@ "integrity": "sha512-xi6IyHML+c9+Q3W0S4fCQJOym42pyurFiJUHEcEyHS0CeKzia4yZDEsLlqOFykxOdHpNy0NmvVO31vcSqAxJCg==", "devOptional": true, "license": "MPL-2.0", - "peer": true, "dependencies": { "detect-libc": "^2.0.3" }, @@ -14111,15 +14147,13 @@ "version": "4.17.23", "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.23.tgz", "integrity": "sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w==", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/lodash-es": { "version": "4.17.23", "resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.17.23.tgz", "integrity": "sha512-kVI48u3PZr38HdYz98UmfPnXl2DXrpdctLrFLCd3kOx1xUkOmpFPx7gCWWM5MPkL/fD8zb+Ph0QzjGFs4+hHWg==", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/lodash-unified": { "version": "1.0.3", @@ -16286,7 +16320,6 @@ "resolved": "https://registry.npmjs.org/react/-/react-19.1.1.tgz", "integrity": "sha512-w8nqGImo45dmMIfljjMwOGtbmC/mk4CMYhWIicdSflH91J9TyCyczcPFXJzrZ/ZXcgGRFeP6BU0BEJTw6tZdfQ==", "license": "MIT", - "peer": true, "engines": { "node": ">=0.10.0" } @@ -16311,7 +16344,6 @@ "integrity": "sha512-Dlq/5LAZgF0Gaz6yiqZCf6VCcZs1ghAJyrsu84Q/GT0gV+mCxbfmKNoGRKBYMJ8IEdGPqu49YWXD02GCknEDkw==", "devOptional": true, "license": "MIT", - "peer": true, "dependencies": { "scheduler": "^0.26.0" }, @@ -16437,7 +16469,6 @@ "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", "license": "MIT", - "peer": true, "dependencies": { "inherits": "^2.0.3", "string_decoder": "^1.1.1", @@ -16604,7 +16635,6 @@ "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.52.2.tgz", "integrity": "sha512-I25/2QgoROE1vYV+NQ1En9T9UFB9Cmfm2CJ83zZOlaDpvz29wGQSZXWKw7MiNXau7wYgB/T9fVIdIuEQ+KbiiA==", "license": "MIT", - "peer": true, "dependencies": { "@types/estree": "1.0.8" }, @@ -17185,7 +17215,6 @@ "resolved": "https://registry.npmjs.org/socket.io-client/-/socket.io-client-4.8.3.tgz", "integrity": "sha512-uP0bpjWrjQmUt5DTHq9RuoCBdFJF10cdX9X+a368j/Ft0wmaVgxlrjvK3kjvgCODOMMOz9lcaRzxmso0bTWZ/g==", "license": "MIT", - "peer": true, "dependencies": { "@socket.io/component-emitter": "~3.1.0", "debug": "~4.4.1", @@ -17570,8 +17599,7 @@ "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.13.tgz", "integrity": "sha512-i+zidfmTqtwquj4hMEwdjshYYgMbOrPzb9a0M3ZgNa0JMoZeFC6bxZvO8yr8ozS6ix2SDz0+mvryPeBs2TFE+w==", "dev": true, - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/tailwindcss-animate": { "version": "1.0.7", @@ -17909,7 +17937,6 @@ "integrity": "sha512-84MVSjMEHP+FQRPy3pX9sTVV/INIex71s9TL2Gm5FG/WG1SqXeKyZ0k7/blY/4FdOzI12CBy1vGc4og/eus0fw==", "devOptional": true, "license": "Apache-2.0", - "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -18281,7 +18308,6 @@ "integrity": "sha512-Z6czzLq4u8fPOyx7TU6X3dvUZVvoJmxSQ+IcrlmagKhilxlhZgxPK6C5Jqbkw1IDUmFTM+cz9QDnnLTwDz/2gQ==", "hasInstallScript": true, "license": "MIT", - "peer": true, "dependencies": { "node-gyp-build": "^4.3.0" }, @@ -18322,7 +18348,6 @@ "resolved": "https://registry.npmjs.org/valtio/-/valtio-1.13.2.tgz", "integrity": "sha512-Qik0o+DSy741TmkqmRfjq+0xpZBXi/Y6+fXZLn0xNF1z/waFMbE3rkivv5Zcf9RrMUp6zswf2J7sbh2KBlba5A==", "license": "MIT", - "peer": true, "dependencies": { "derive-valtio": "0.1.0", "proxy-compare": "2.6.0", @@ -18374,7 +18399,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "@noble/curves": "1.9.1", "@noble/hashes": "1.8.0", @@ -18522,7 +18546,6 @@ "resolved": "https://registry.npmjs.org/vite/-/vite-6.4.1.tgz", "integrity": "sha512-+Oxm7q9hDoLMyJOYfUYBuHQo+dkAloi33apOPP56pzj+vsdJDzr+j1NISE5pyaAuKL4A3UD34qd0lx5+kfKp2g==", "license": "MIT", - "peer": true, "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.4.4", @@ -18721,7 +18744,6 @@ "resolved": "https://registry.npmjs.org/vitest/-/vitest-3.2.4.tgz", "integrity": "sha512-LUCP5ev3GURDysTWiP47wRRUpLKMOfPh+yKTx3kVIEiu5KOMeqzpnYNsKyOoVrULivR8tLcks4+lga33Whn90A==", "license": "MIT", - "peer": true, "dependencies": { "@types/chai": "^5.2.2", "@vitest/expect": "3.2.4", @@ -18801,7 +18823,6 @@ "resolved": "https://registry.npmjs.org/vue/-/vue-3.5.28.tgz", "integrity": "sha512-BRdrNfeoccSoIZeIhyPBfvWSLFP4q8J3u8Ju8Ug5vu3LdD+yTM13Sg4sKtljxozbnuMu1NB1X5HBHRYUzFocKg==", "license": "MIT", - "peer": true, "dependencies": { "@vue/compiler-dom": "3.5.28", "@vue/compiler-sfc": "3.5.28", @@ -18830,7 +18851,6 @@ "integrity": "sha512-Vxi9pJdbN3ZnVGLODVtZ7y4Y2kzAAE2Cm0CZ3ZDRvydVYxZ6VrnBhLikBsRS+dpwj4Jv4UCv21PTEwF5rQ9WXg==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "debug": "^4.4.0", "eslint-scope": "^8.2.0 || ^9.0.0", @@ -19101,7 +19121,6 @@ "version": "8.17.1", "resolved": "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz", "integrity": "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==", - "peer": true, "engines": { "node": ">=10.0.0" }, @@ -19289,7 +19308,6 @@ "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", "devOptional": true, "license": "MIT", - "peer": true, "funding": { "url": "https://github.com/sponsors/colinhacks" } @@ -19333,6 +19351,10 @@ } } }, + "packages/analytics": { + "name": "@harb/analytics", + "version": "0.1.0" + }, "packages/ui-shared": { "name": "@harb/ui-shared", "version": "0.1.0", @@ -19363,6 +19385,7 @@ "name": "harb-staking", "version": "0.0.0", "dependencies": { + "@harb/analytics": "*", "@harb/utils": "*", "@harb/web3": "*", "@tanstack/vue-query": "^5.64.2", diff --git a/packages/analytics/package.json b/packages/analytics/package.json new file mode 100644 index 0000000..a74009f --- /dev/null +++ b/packages/analytics/package.json @@ -0,0 +1,8 @@ +{ + "name": "@harb/analytics", + "version": "0.1.0", + "private": true, + "type": "module", + "main": "src/index.ts", + "types": "src/index.ts" +} diff --git a/packages/analytics/src/index.ts b/packages/analytics/src/index.ts new file mode 100644 index 0000000..3be50a0 --- /dev/null +++ b/packages/analytics/src/index.ts @@ -0,0 +1,69 @@ +/** + * Lightweight analytics wrapper for self-hosted Umami. + * + * Call `initAnalytics()` once at app startup to inject the tracker + * script. All other helpers are safe to call at any time — if the + * tracker hasn't loaded (ad-blocker, missing config, dev without + * Umami running) every call is a silent no-op. + */ + +interface UmamiTracker { + track: (name: string, data?: Record) => void; +} + +declare global { + interface Window { + umami?: UmamiTracker; + } +} + +/** + * Inject the Umami tracker script. + * @param scriptUrl — full URL to `script.js`, e.g. `https://analytics.kraiken.org/script.js` + * @param websiteId — Umami website ID (UUID) + */ +export function initAnalytics(scriptUrl: string | undefined, websiteId: string | undefined): void { + if (!scriptUrl || !websiteId) return; + if (typeof document === 'undefined') return; + if (document.querySelector(`script[data-website-id="${websiteId}"]`)) return; + + const el = document.createElement('script'); + el.defer = true; + el.src = scriptUrl; + el.dataset.websiteId = websiteId; + document.head.appendChild(el); +} + +function getTracker(): UmamiTracker | undefined { + return typeof window !== 'undefined' ? window.umami : undefined; +} + +/** Fire a named event with optional properties. */ +export function trackEvent( + name: string, + data?: Record, +): void { + getTracker()?.track(name, data); +} + +/* ── Funnel events ─────────────────────────────────────────────── */ + +/** Landing page CTA clicked (e.g. "Get $KRK"). */ +export function trackCtaClick(label: string): void { + trackEvent('cta_click', { label }); +} + +/** Wallet successfully connected. */ +export function trackWalletConnect(): void { + trackEvent('wallet_connect'); +} + +/** Swap (buy/sell) initiated — user submitted the transaction. */ +export function trackSwapInitiated(direction: 'buy' | 'sell'): void { + trackEvent('swap_initiated', { direction }); +} + +/** Stake position created (snatch completed). */ +export function trackStakeCreated(): void { + trackEvent('stake_created'); +} diff --git a/web-app/package.json b/web-app/package.json index ddc06e7..56207f7 100644 --- a/web-app/package.json +++ b/web-app/package.json @@ -33,6 +33,7 @@ "vue-router": "^4.2.5", "vue-tippy": "^6.6.0", "vue-toastification": "^2.0.0-rc.5", + "@harb/analytics": "*", "@harb/web3": "*", "@harb/utils": "*" }, diff --git a/web-app/src/composables/useStake.ts b/web-app/src/composables/useStake.ts index 55225fe..3193b35 100644 --- a/web-app/src/composables/useStake.ts +++ b/web-app/src/composables/useStake.ts @@ -11,6 +11,7 @@ import { createPermitObject, getSignatureRSV } from '@/utils/blockchain'; import { weiToNumber, compactNumber } from 'kraiken-lib/format'; import { getTaxRateOptionByIndex } from '@/composables/useAdjustTaxRates'; import { useContractToast } from './useContractToast'; +import { trackStakeCreated } from '@harb/analytics'; const wallet = useWallet(); const contractToast = useContractToast(); const wagmiConfig: Config = config; @@ -129,6 +130,7 @@ export function useStake() { const eventArgs: PositionCreatedArgs = topics.args; const amount = compactNumber(weiToNumber(eventArgs.harbergDeposit, wallet.balance.decimals)); + trackStakeCreated(); contractToast.showSuccessToast(amount, 'Success!', 'You Staked', 'Check your positions on the
    Staker Dashboard', '$KRK'); waiting.value = false; diff --git a/web-app/src/composables/useSwapKrk.ts b/web-app/src/composables/useSwapKrk.ts index b46651e..7f19848 100644 --- a/web-app/src/composables/useSwapKrk.ts +++ b/web-app/src/composables/useSwapKrk.ts @@ -7,6 +7,7 @@ import { config as wagmiConfig } from '@/wagmi'; import { getChain, DEFAULT_CHAIN_ID } from '@/config'; import { useWallet } from '@/composables/useWallet'; import { getErrorMessage, ensureAddress } from '@harb/utils'; +import { trackSwapInitiated } from '@harb/analytics'; export const WETH_ABI = [ { @@ -217,6 +218,7 @@ export function useSwapKrk() { }); } + trackSwapInitiated('buy'); await writeContract(wagmiConfig, { abi: SWAP_ROUTER_ABI, address: router, @@ -327,6 +329,7 @@ export function useSwapKrk() { } sellPhase.value = 'selling'; + trackSwapInitiated('sell'); await writeContract(wagmiConfig, { abi: SWAP_ROUTER_ABI, address: router, diff --git a/web-app/src/composables/useWallet.ts b/web-app/src/composables/useWallet.ts index c501f48..609fc77 100644 --- a/web-app/src/composables/useWallet.ts +++ b/web-app/src/composables/useWallet.ts @@ -9,6 +9,7 @@ import { setStakeContract } from '@/contracts/stake'; import { chainsData, DEFAULT_CHAIN_ID } from '@/config'; import { createPublicClient, custom, formatUnits, type EIP1193Provider, type PublicClient, type Transport, type WalletClient } from 'viem'; import { getWalletPublicClient, setWalletPublicClient } from '@/services/walletRpc'; +import { trackWalletConnect } from '@harb/analytics'; const balance = ref({ value: 0n, @@ -118,6 +119,7 @@ export function useWallet() { }; } else if (account.value.address !== data.address || account.value.chainId !== data.chainId) { logger.info(`Account changed!:`, data.address); + if (!account.value.address && data.address) trackWalletConnect(); account.value = data; await syncWalletPublicClient(data); await loadBalance(); diff --git a/web-app/src/main.ts b/web-app/src/main.ts index 898b9df..acd9498 100644 --- a/web-app/src/main.ts +++ b/web-app/src/main.ts @@ -2,6 +2,7 @@ import { WagmiPlugin } from '@wagmi/vue'; import { QueryClient, VueQueryPlugin } from '@tanstack/vue-query'; import { createApp } from 'vue'; import { config } from './wagmi'; +import { initAnalytics } from '@harb/analytics'; import ClickOutSide from '@/directives/ClickOutsideDirective'; import router from './router'; @@ -9,6 +10,9 @@ import App from './App.vue'; import './assets/styles/main.sass'; import Toast from 'vue-toastification'; import 'vue-toastification/dist/index.css'; + +initAnalytics(import.meta.env.VITE_UMAMI_URL, import.meta.env.VITE_UMAMI_WEBSITE_ID); + const queryClient = new QueryClient(); const app = createApp(App);