Tunnel Traffic Analytics¶
Full description of how visit statistics for tunnel sites (slug.dix.su) are collected, stored, and displayed.
For Users¶
Plan Availability¶
| Plan | Analytics | Traffic quota |
|---|---|---|
| SIMPLE | No | Usage / limit only |
| VIP | No | Usage / limit only |
| PRO | Yes — full detail | + display |
| PERS | Yes — full detail | + display |
| GLAVA | Yes | Unlimited |
Data appears from the moment you upgrade to PRO/PERS — no retroactive history for previous days.
What Is Shown¶
Daily breakdown (Analytics tab, date selector):
| Metric | Description |
|---|---|
| Traffic (bytes) | Inbound and outbound volume for the day |
| Requests | Total HTTP request count (including bots and static assets) |
| Unique visitors | Estimated unique people (deduplication by IP+UA) |
| Top pages | Most visited paths on your tunnel site |
| Traffic sources | UTM parameters + referrer domains |
| Geography | Visitors' countries and cities |
Monthly summary (Usage tab): total bytes and requests for the current month, no per-day breakdown — available on all plans.
Unique Visitors¶
Uniqueness is determined by SHA-256(daily_salt | IP | User-Agent). The salt rotates daily and is never persisted — meaning the raw IP cannot be reconstructed from the database even with direct access. This is the same approach used by Plausible Analytics.
This is an approximate metric: multiple people behind the same NAT (corporate network, mobile carrier) may be counted as one visitor.
Bot Filtering¶
| Request type | Traffic (bytes/requests) | Page visits | Unique | Geo |
|---|---|---|---|---|
| Real users | ✅ | ✅ | ✅ | ✅ |
| Bots (Googlebot, Yandex, Facebook, etc.) | ✅ | ❌ | ❌ | ❌ |
| Static assets (css/js/img/fonts) | ✅ | ❌ | ❌ | ❌ |
Bot traffic is included in bytes and request counts — they generate real load and consume the monthly quota.
UTM Tags and Traffic Sources¶
Supported parameters: utm_source, utm_medium, utm_campaign, utm_content, utm_term, ref.
The domain from the Referer header is automatically recorded as referrer_domain — capturing organic visits from search engines and social networks without UTM tags. Internal referrals (same tunnel host) are excluded.
Retention and Export¶
- Data is retained for 90 days, then automatically deleted (daily at 00:10 UTC).
- Excel export: custom date range up to 365 days back. Includes all 4 metrics (traffic, paths, parameters, geo).
What Is NOT Collected¶
- Request/response body content
- Authorization headers, cookies, tokens
- In E2E mode — nothing at all (traffic is encrypted to the device; the server sees only ciphertext)
For Technical Specialists¶
Architecture: Hot Path via ETS¶
Direct PostgreSQL INSERTs on every request would be a bottleneck at thousands of requests per second. Instead:
HTTP request to slug.dix.su
↓
proxy_controller.ex
↓
MetricsCollector.record_request/6 ← atomic ETS increment, nanoseconds
↓ (every 60 sec)
flush_to_db/0
↓
PostgreSQL UPSERT × 4 tables
MetricsCollector is a GenServer with four in-RAM ETS tables:
| ETS table | Purpose |
|---|---|
:tunnel_metrics |
Traffic/path/geo/UTM counters for the current flush interval |
:tunnel_unique_fingerprints |
Visitor deduplication — hashes for today and yesterday |
:tunnel_visitor_salt |
Daily salts (only today and yesterday retained) |
:public_role_cache |
User role cache (TTL 10 min) — avoids DB lookup on every request |
Flush is best effort: if PostgreSQL is unavailable, up to 60s of accumulated data is lost. The proxy does not degrade.
PostgreSQL Schema¶
Four tables, all with composite primary keys and ON CONFLICT DO UPDATE:
-- Daily traffic summary
tunnel_traffic_daily (
slug TEXT, date DATE,
bytes_in BIGINT, bytes_out BIGINT,
request_count BIGINT, unique_visitors BIGINT,
PRIMARY KEY (slug, date)
)
-- Top paths per day
tunnel_path_visits_daily (
slug TEXT, date DATE, path TEXT,
visit_count BIGINT,
PRIMARY KEY (slug, date, path)
)
-- UTM parameters and referrer_domain
tunnel_param_visits_daily (
slug TEXT, date DATE, param_name TEXT, param_value TEXT,
visit_count BIGINT,
PRIMARY KEY (slug, date, param_name, param_value)
)
-- Geo visits
tunnel_geo_visits_daily (
slug TEXT, date DATE, country TEXT, city TEXT,
visit_count BIGINT,
PRIMARY KEY (slug, date, country, city)
)
Migrations: V29__Tunnel_analytics.sql, V30__Tunnel_unique_visitors.sql, V31__Tunnel_geo_visits.sql.
Geolocation¶
Country and city are resolved from the IP address via the offline MaxMind GeoLite2 database (DixuProxy.GeoIp). No external API calls in the hot path. If the country cannot be resolved (localhost, VPN, database not loaded), no geo row is written.
Unique Visitors: Implementation¶
# MetricsCollector.ex — deduplication
defp track_unique_visitor(slug, date, ip, user_agent) do
salt = daily_salt(date)
fingerprint = :crypto.hash(:sha256, [salt, "|", ip, "|", user_agent])
if :ets.insert_new(@uniq_table, {{slug, date, fingerprint}, true}) do
bump({:unique, slug, date}, 1)
end
end
Salt rotation: on first access for a date, strong_rand_bytes(16) is generated and stored in @salt_table. Fingerprints and salts older than yesterday are cleaned up every 30 flush cycles (~30 min).
API Endpoints¶
All endpoints are authenticated (session-based), accessible only by the tunnel owner.
| Method | Path | Minimum plan | Description |
|---|---|---|---|
| GET | /api/analytics/my?date=YYYY-MM-DD&page=N&size=N |
PRO | Daily analytics with paginated top paths |
| GET | /api/analytics/monthly |
All | Cumulative traffic for the current month |
| GET | /api/billing/traffic |
All | Quota usage (bytes) with limit and reset date |
| GET | /api/analytics/my/export?from=YYYY-MM-DD&to=YYYY-MM-DD |
PRO | Download Excel (up to 365 days) |
Sample /api/analytics/my response (PRO+):
{
"bytesIn": 1234567,
"bytesOut": 9876543,
"requestCount": 4200,
"uniqueVisitors": 37,
"topPaths": {
"rows": [{"path": "/", "visitCount": 120}],
"total": 42
},
"topParams": [
{"paramName": "utm_source", "paramValue": "telegram", "visitCount": 55},
{"paramName": "referrer_domain", "paramValue": "google.com", "visitCount": 18}
],
"topGeo": [
{"country": "Russia", "city": "Moscow", "visitCount": 30}
]
}
Admin Access¶
Via AdminListsController:
| Path | Description |
|---|---|
GET /admin/analytics/list?date=&page=&size= |
Paginated tunnel list with daily metrics |
GET /admin/analytics/top-paths?slug=&date= |
Top paths for a specific tunnel |
GET /admin/analytics/geo?slug=&date= |
Geo statistics for a specific tunnel |
Available only to GLAVA-role sessions (Spring Security session).
Data Cleanup¶
AnalyticsCleanupScheduler — Spring @Scheduled(cron = "0 10 0 * * *"):
DELETE FROM tunnel_traffic_daily WHERE date < CURRENT_DATE - INTERVAL '90 days';
DELETE FROM tunnel_path_visits_daily WHERE date < CURRENT_DATE - INTERVAL '90 days';
DELETE FROM tunnel_param_visits_daily WHERE date < CURRENT_DATE - INTERVAL '90 days';
DELETE FROM tunnel_geo_visits_daily WHERE date < CURRENT_DATE - INTERVAL '90 days';
E2E Tunnels and Analytics¶
For E2E tunnels (e2e-<token>.dix.su), MetricsCollector.record_request is not called — the proxy acts as a blind TCP relay. Instead, e2e_traffic_counter.ex records bytes only (in/out) in a separate table for billing purposes. No paths, geo, or UTM for E2E.
Traffic Limits and Warnings¶
MetricsCollector.check_public_limit/1 is called before every tunnel request:
- Sums ETS counters for the current month + cached DB data (TTL 5 min)
- At ≥ 80% quota: sends an email warning (at most once per day)
- At ≥ 100%: freezes the tunnel until the 1st of the next month
Scaling (Future)¶
Plan from SCALING.md:
- Read replica for
AnalyticsController— analytics SELECTs skip the master - Table partitioning for
tunnel_path_visits_daily/tunnel_geo_visits_dailybydate— cheap DROP PARTITION instead of DELETE - Event streaming (Kafka/Kinesis) — replace direct UPSERTs in dixu_proxy, buffer for traffic spikes
See Also¶
- Billing & Plans — monthly traffic quotas by plan
- Security — rate limiting, DDoS protection
- Scaling — analytics infrastructure growth plan