Merge pull request 'fix: feat: basic analytics funnel tracking for launch readiness (#1101)' (#1142) from fix/issue-1101 into master

This commit is contained in:
johba 2026-03-23 16:36:02 +01:00
commit 209e0c798e
19 changed files with 287 additions and 57 deletions

View file

@ -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

View file

@ -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
}

16
containers/init-umami-db.sh Executable file
View file

@ -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

View file

@ -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

View file

@ -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/

View file

@ -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:

View file

@ -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=<your-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

View file

@ -5,7 +5,6 @@
<link rel="icon" href="/favicon.ico">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>KrAIken</title>
<script defer src="https://cloud.umami.is/script.js" data-website-id="6e2869f2-02a5-4346-9b3e-3c27e24ea5d3"></script>
</head>
<body>
<div id="app"></div>

View file

@ -17,6 +17,7 @@
"prepare": "husky"
},
"dependencies": {
"@harb/analytics": "*",
"@harb/web3": "*",
"@harb/ui-shared": "*",
"@tanstack/vue-query": "^5.92.9",

View file

@ -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 });

View file

@ -10,7 +10,7 @@
$KRK has a price floor backed by real ETH. The protocol adapts automatically. You just hold.
</div>
<div class="header-cta">
<KButton @click="router.push('/app/get-krk')">Get $KRK</KButton>
<KButton @click="navigateCta('/app/get-krk', 'hero_get_krk')">Get $KRK</KButton>
</div>
<div class="blur-effect"></div>
</div>
@ -38,7 +38,7 @@
</div>
</div>
<div class="centered-cta">
<KButton @click="router.push('/app/get-krk')">Get $KRK</KButton>
<KButton @click="navigateCta('/app/get-krk', 'how_it_works_get_krk')">Get $KRK</KButton>
</div>
</section>
<section class="protocol-health-section">
@ -51,7 +51,7 @@
<p>
Watch the protocol in real time. Supply dynamics, ETH reserves, position history all live, no wallet needed.
</p>
<KButton @click="router.push('/app')">View Protocol</KButton>
<KButton @click="navigateCta('/app', 'view_protocol')">View Protocol</KButton>
</template>
</LeftRightComponent>
</section>
@ -68,8 +68,8 @@
<li>Hold. The protocol does the rest.</li>
</ol>
<div class="button-group">
<KButton @click="router.push('/app/get-krk')">Get $KRK</KButton>
<KButton @click="router.push('/docs/how-it-works')">How It Works </KButton>
<KButton @click="navigateCta('/app/get-krk', 'getting_started_get_krk')">Get $KRK</KButton>
<KButton @click="navigateCta('/docs/how-it-works', 'how_it_works_docs')">How It Works </KButton>
</div>
</template>
</LeftRightComponent>
@ -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');
};

125
package-lock.json generated
View file

@ -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",

View file

@ -0,0 +1,8 @@
{
"name": "@harb/analytics",
"version": "0.1.0",
"private": true,
"type": "module",
"main": "src/index.ts",
"types": "src/index.ts"
}

View file

@ -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<string, string | number | boolean>) => 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<string, string | number | boolean>,
): 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');
}

View file

@ -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": "*"
},

View file

@ -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<br /> Staker Dashboard', '$KRK');
waiting.value = false;

View file

@ -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,

View file

@ -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<GetBalanceReturnType>({
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();

View file

@ -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);