feat: add experience improvement module and system config storage (#1248)

- Add experience module for product telemetry
- Add system_config table for persistent settings
- Update privacy policy documentation
This commit is contained in:
tinkle-community
2025-12-19 13:57:08 +08:00
committed by GitHub
parent c81e6b0094
commit 97f58b49f4
12 changed files with 409 additions and 4 deletions

View File

@@ -1,6 +1,7 @@
package config
import (
"nofx/experience"
"os"
"strconv"
"strings"
@@ -22,14 +23,20 @@ type Config struct {
// TransportEncryption enables browser-side encryption for API keys
// Requires HTTPS or localhost. Set to false for HTTP access via IP.
TransportEncryption bool
// Experience improvement (anonymous usage statistics)
// Helps us understand product usage and improve the experience
// Set EXPERIENCE_IMPROVEMENT=false to disable
ExperienceImprovement bool
}
// Init initializes global configuration (from .env)
func Init() {
cfg := &Config{
APIServerPort: 8080,
RegistrationEnabled: true,
MaxUsers: 1, // Default: only 1 user allowed
APIServerPort: 8080,
RegistrationEnabled: true,
MaxUsers: 1, // Default: only 1 user allowed
ExperienceImprovement: true, // Default: enabled to help improve the product
}
// Load from environment variables
@@ -62,7 +69,16 @@ func Init() {
cfg.TransportEncryption = strings.ToLower(v) == "true"
}
// Experience improvement: anonymous usage statistics
// Default enabled, set EXPERIENCE_IMPROVEMENT=false to disable
if v := os.Getenv("EXPERIENCE_IMPROVEMENT"); v != "" {
cfg.ExperienceImprovement = strings.ToLower(v) != "false"
}
global = cfg
// Initialize experience improvement (installation ID will be set after database init)
experience.Init(cfg.ExperienceImprovement, "")
}
// Get returns the global configuration

View File

@@ -62,6 +62,32 @@ C. Note on Local Encryption
We are aware that the "Software" provides functionality to encrypt user-entered API keys and private keys. We clarify here that this encryption process is performed and managed entirely on your own device (locally). This data is never transmitted to us or any third party after encryption. This encryption feature is designed to protect your data from unauthorized access to your local device, not to share it with us.
D. Experience Improvement Program (Optional)
To help us improve the product experience, the "Software" sends **anonymous usage statistics** by default. This feature is completely optional and you can disable it at any time.
**Data Types Collected:**
- Exchange type (e.g., Binance, Bybit, etc., excluding your account information)
- Trade type (open/close position)
- Trade amount (USD value)
- Trading pair (e.g., BTCUSDT)
- Anonymous identifiers (used for counting active numbers, not linked to personal identity):
- Installation ID: Identifies each independently deployed software instance
- User ID: Identifies user accounts within the software (only for counting active users)
- Trader ID: Identifies trading strategies created by users (only for counting active strategies)
**We explicitly do NOT collect:**
- Your API keys, private keys, or any credentials
- Your account addresses, usernames, or identity information
- Specific trade prices, times, or order details
- Any information that could reverse-identify personal identity through the above anonymous IDs
**How to Disable:**
Set `EXPERIENCE_IMPROVEMENT=false` in your environment variables to completely disable this feature.
**Purpose of Data:**
These anonymous statistics are only used to understand overall product usage and help us optimize features and improve user experience.
IV. Data Sharing, Retention, and Security (Website Data)

View File

@@ -62,6 +62,32 @@ C. ローカル暗号化に関する注記
当社は、「ソフトウェア」がユーザーが入力したAPIキーと秘密鍵を暗号化する機能を提供していることを認識しています。ここで明確にします。この暗号化プロセスは完全にお客様自身のデバイス上で(ローカルで)実行および管理されます。これらのデータは、暗号化後に当社またはサードパーティに送信されることは決してありません。この暗号化機能は、お客様のローカルデバイスへの不正アクセスからデータを保護するためであり、当社と共有するためではありません。
D. エクスペリエンス向上プログラム(オプション)
製品体験の向上を支援するために、「ソフトウェア」はデフォルトで**匿名の使用統計データ**を送信します。この機能は完全にオプションであり、いつでも無効にすることができます。
**収集されるデータの種類:**
- 取引所タイプBinance、Bybitなど、アカウント情報を含まない
- 取引タイプ(ポジションの開閉)
- 取引金額USD値
- 取引ペアBTCUSDTなど
- 匿名識別子(アクティブ数のカウントに使用、個人情報とは関連付けられません):
- インストールID独立して展開された各ソフトウェアインスタンスを識別
- ユーザーIDソフトウェア内のユーザーアカウントを識別アクティブユーザー数のカウントのみ
- トレーダーIDユーザーが作成した取引戦略を識別アクティブ戦略数のカウントのみ
**当社が明確に収集しないもの:**
- APIキー、秘密鍵、または資格情報
- アカウントアドレス、ユーザー名、または身元情報
- 具体的な取引価格、時間、または注文詳細
- 上記の匿名IDを通じて個人を特定できる情報
**無効にする方法:**
環境変数で `EXPERIENCE_IMPROVEMENT=false` を設定すると、この機能を完全に無効にできます。
**データの目的:**
これらの匿名統計は、製品の全体的な使用状況を理解し、機能の最適化とユーザー体験の向上に役立てるためにのみ使用されます。
IV. データの共有、保持、およびセキュリティ(ウェブサイトデータ)

View File

@@ -62,6 +62,32 @@ C. Примечание о локальном шифровании
Мы знаем, что «Программное обеспечение» предоставляет функцию шифрования введенных пользователем API-ключей и приватных ключей. Мы уточняем здесь, что этот процесс шифрования полностью выполняется и управляется на вашем собственном устройстве (локально). Эти данные никогда не передаются нам или любой третьей стороне после шифрования. Эта функция шифрования предназначена для защиты ваших данных от несанкционированного доступа к вашему локальному устройству, а не для обмена ими с нами.
D. Программа улучшения опыта (Опционально)
Чтобы помочь нам улучшить продукт, «Программное обеспечение» по умолчанию отправляет **анонимную статистику использования**. Эта функция полностью опциональна, и вы можете отключить её в любое время.
**Типы собираемых данных:**
- Тип биржи (например, Binance, Bybit и т.д., без информации о вашем аккаунте)
- Тип сделки (открытие/закрытие позиции)
- Сумма сделки (в USD)
- Торговая пара (например, BTCUSDT)
- Анонимные идентификаторы (используются для подсчета активных пользователей, не связаны с личной информацией):
- ID установки: Идентифицирует каждый независимо развернутый экземпляр программного обеспечения
- ID пользователя: Идентифицирует учетные записи пользователей в программном обеспечении (только для подсчета активных пользователей)
- ID трейдера: Идентифицирует торговые стратегии, созданные пользователями (только для подсчета активных стратегий)
**Мы явно НЕ собираем:**
- Ваши API-ключи, приватные ключи или любые учетные данные
- Адреса ваших аккаунтов, имена пользователей или идентификационную информацию
- Конкретные цены сделок, время или детали заказов
- Любую информацию, которая может идентифицировать личность через вышеуказанные анонимные ID
**Как отключить:**
Установите `EXPERIENCE_IMPROVEMENT=false` в переменных окружения, чтобы полностью отключить эту функцию.
**Цель сбора данных:**
Эта анонимная статистика используется только для понимания общего использования продукта и помогает нам оптимизировать функции и улучшить пользовательский опыт.
IV. Обмен данными, хранение и безопасность (Данные веб-сайта)

View File

@@ -62,6 +62,32 @@ C. Примітка про локальне шифрування
Ми знаємо, що «Програмне забезпечення» надає функцію шифрування введених користувачем API-ключів і приватних ключів. Ми уточнюємо тут, що цей процес шифрування повністю виконується та керується на вашому власному пристрої (локально). Ці дані ніколи не передаються нам або будь-якій третій стороні після шифрування. Ця функція шифрування призначена для захисту ваших даних від несанкціонованого доступу до вашого локального пристрою, а не для обміну ними з нами.
D. Програма покращення досвіду (Опціонально)
Щоб допомогти нам покращити продукт, «Програмне забезпечення» за замовчуванням надсилає **анонімну статистику використання**. Ця функція повністю опціональна, і ви можете вимкнути її в будь-який час.
**Типи даних, що збираються:**
- Тип біржі (наприклад, Binance, Bybit тощо, без інформації про ваш обліковий запис)
- Тип угоди (відкриття/закриття позиції)
- Сума угоди (в USD)
- Торгова пара (наприклад, BTCUSDT)
- Анонімні ідентифікатори (використовуються для підрахунку активних користувачів, не пов'язані з особистою інформацією):
- ID установки: Ідентифікує кожен незалежно розгорнутий екземпляр програмного забезпечення
- ID користувача: Ідентифікує облікові записи користувачів у програмному забезпеченні (тільки для підрахунку активних користувачів)
- ID трейдера: Ідентифікує торгові стратегії, створені користувачами (тільки для підрахунку активних стратегій)
**Ми явно НЕ збираємо:**
- Ваші API-ключі, приватні ключі або будь-які облікові дані
- Адреси ваших облікових записів, імена користувачів або ідентифікаційну інформацію
- Конкретні ціни угод, час або деталі замовлень
- Будь-яку інформацію, яка може ідентифікувати особу через вищезазначені анонімні ID
**Як вимкнути:**
Встановіть `EXPERIENCE_IMPROVEMENT=false` у змінних середовища, щоб повністю вимкнути цю функцію.
**Мета збору даних:**
Ця анонімна статистика використовується тільки для розуміння загального використання продукту і допомагає нам оптимізувати функції та покращити досвід користувача.
IV. Обмін даними, зберігання та безпека (Дані веб-сайту)

View File

@@ -62,7 +62,33 @@ B. 明确的不收集列表
C. 关于本地加密的说明
我们知悉软件提供了对用户输入的 API 密钥和私钥进行加密的功能。我们在此澄清,此加密过程完全在您自己的设备上(本地)进行和管理。这些数据在加密后绝不会被传输给我们或任何第三方。该加密功能是为了保护您的数据免受对您本地设备的未授权访问,而不是为了与我们共享。
我们知悉"软件"提供了对用户输入的 API 密钥和私钥进行加密的功能。我们在此澄清,此加密过程完全在您自己的设备上(本地)进行和管理。这些数据在加密后绝不会被传输给我们或任何第三方。该加密功能是为了保护您的数据免受对您本地设备的未授权访问,而不是为了与我们共享。
D. 体验改进计划(可选)
为了帮助我们改进产品体验,"软件"默认会发送**匿名的使用统计数据**。此功能完全可选,您可以随时关闭。
**收集的数据类型:**
- 交易所类型(如 Binance、Bybit 等,不包含您的账户信息)
- 交易类型(开仓/平仓)
- 交易金额USD 数值)
- 交易币种(如 BTCUSDT
- 匿名标识符(用于统计活跃数量,不关联个人身份):
- 安装实例 ID标识每个独立部署的软件实例
- 用户 ID标识软件内的用户账号仅用于统计活跃用户数
- 交易者 ID标识用户创建的交易策略仅用于统计活跃策略数
**我们明确不收集:**
- 您的 API 密钥、私钥或任何凭证
- 您的账户地址、用户名或身份信息
- 具体的交易价格、时间或订单详情
- 任何可通过上述匿名 ID 反向识别个人身份的信息
**如何关闭:**
在环境变量中设置 `EXPERIENCE_IMPROVEMENT=false` 即可完全禁用此功能。
**数据用途:**
这些匿名统计数据仅用于了解产品整体使用情况,帮助我们优化功能和改进用户体验。
四、 数据共享、保留和安全(网站数据)

188
experience/experience.go Normal file
View File

@@ -0,0 +1,188 @@
// Package experience handles product telemetry
package experience
import (
"bytes"
"encoding/json"
"net/http"
"sync"
"time"
)
const (
telemetryEndpoint = "https://www.google-analytics.com/mp/collect"
tid = "G-14J8SY6F0J"
tk = "sgPLmshGTPiF-X57rzEIKA"
)
var (
client *Client
clientOnce sync.Once
httpClient = &http.Client{Timeout: 5 * time.Second}
)
type Client struct {
enabled bool
installationID string
mu sync.RWMutex
}
type TradeEvent struct {
Exchange string
TradeType string
Symbol string
AmountUSD float64
Leverage int
UserID string
TraderID string
}
type telemetryPayload struct {
ClientID string `json:"client_id"`
Events []telemetryEvent `json:"events"`
}
type telemetryEvent struct {
Name string `json:"name"`
Params map[string]interface{} `json:"params"`
}
func Init(enabled bool, installationID string) {
clientOnce.Do(func() {
client = &Client{
enabled: enabled,
installationID: installationID,
}
})
}
func SetInstallationID(id string) {
if client == nil {
return
}
client.mu.Lock()
defer client.mu.Unlock()
client.installationID = id
}
func GetInstallationID() string {
if client == nil {
return ""
}
client.mu.RLock()
defer client.mu.RUnlock()
return client.installationID
}
func SetEnabled(enabled bool) {
if client == nil {
return
}
client.mu.Lock()
defer client.mu.Unlock()
client.enabled = enabled
}
func IsEnabled() bool {
if client == nil {
return false
}
client.mu.RLock()
defer client.mu.RUnlock()
return client.enabled
}
func TrackTrade(event TradeEvent) {
if client == nil || !IsEnabled() {
return
}
// Send asynchronously to not block trading
go func() {
_ = sendTradeEvent(event)
}()
}
// sendTradeEvent sends the trade event to GA4
func sendTradeEvent(event TradeEvent) error {
client.mu.RLock()
installationID := client.installationID
client.mu.RUnlock()
payload := telemetryPayload{
ClientID: installationID,
Events: []telemetryEvent{
{
Name: "trade",
Params: map[string]interface{}{
"exchange": event.Exchange,
"trade_type": event.TradeType,
"symbol": event.Symbol,
"amount_usd": event.AmountUSD,
"leverage": event.Leverage,
"installation_id": installationID, // For counting active installations
"user_id": event.UserID, // For counting active users
"trader_id": event.TraderID, // For counting active traders
"engagement_time_msec": 1, // Required by GA4
},
},
},
}
jsonData, err := json.Marshal(payload)
if err != nil {
return err
}
url := telemetryEndpoint + "?measurement_id=" + tid + "&api_secret=" + tk
req, err := http.NewRequest("POST", url, bytes.NewBuffer(jsonData))
if err != nil {
return err
}
req.Header.Set("Content-Type", "application/json")
resp, err := httpClient.Do(req)
if err != nil {
return err
}
defer resp.Body.Close()
return nil
}
func TrackStartup(version string) {
if client == nil || !IsEnabled() {
return
}
go func() {
client.mu.RLock()
installationID := client.installationID
client.mu.RUnlock()
payload := telemetryPayload{
ClientID: installationID,
Events: []telemetryEvent{
{
Name: "app_startup",
Params: map[string]interface{}{
"version": version,
"installation_id": installationID,
"engagement_time_msec": 1,
},
},
},
}
jsonData, _ := json.Marshal(payload)
url := telemetryEndpoint + "?measurement_id=" + tid + "&api_secret=" + tk
req, _ := http.NewRequest("POST", url, bytes.NewBuffer(jsonData))
if req != nil {
req.Header.Set("Content-Type", "application/json")
resp, err := httpClient.Do(req)
if err == nil {
resp.Body.Close()
}
}
}()
}

BIN
img.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 492 KiB

BIN
img_1.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 360 KiB

29
main.go
View File

@@ -6,6 +6,7 @@ import (
"nofx/backtest"
"nofx/config"
"nofx/crypto"
"nofx/experience"
"nofx/logger"
"nofx/manager"
"nofx/market"
@@ -18,6 +19,7 @@ import (
"syscall"
"time"
"github.com/google/uuid"
"github.com/joho/godotenv"
)
@@ -58,6 +60,9 @@ func main() {
defer st.Close()
backtest.UseDatabase(st.DB())
// Initialize installation ID for experience improvement (anonymous statistics)
initInstallationID(st)
// Initialize encryption service
logger.Info("🔐 Initializing encryption service...")
cryptoService, err := crypto.NewCryptoService()
@@ -173,3 +178,27 @@ func newSharedMCPClient() mcp.AIClient {
}
return mcp.NewDeepSeekClient()
}
// initInstallationID initializes the anonymous installation ID for experience improvement
// This ID is persisted in database and used for anonymous usage statistics
func initInstallationID(st *store.Store) {
const key = "installation_id"
// Try to load from database
installationID, err := st.GetSystemConfig(key)
if err != nil {
logger.Warnf("⚠️ Failed to load installation ID: %v", err)
}
// Generate new ID if not exists
if installationID == "" {
installationID = uuid.New().String()
if err := st.SetSystemConfig(key, installationID); err != nil {
logger.Warnf("⚠️ Failed to save installation ID: %v", err)
}
logger.Infof("📊 Generated new installation ID: %s", installationID[:8]+"...")
}
// Set installation ID in experience module
experience.SetInstallationID(installationID)
}

View File

@@ -115,6 +115,16 @@ func (s *Store) SetCryptoFuncs(encrypt, decrypt func(string) string) {
// initTables initializes all database tables
func (s *Store) initTables() error {
// Initialize system config table first
if _, err := s.db.Exec(`
CREATE TABLE IF NOT EXISTS system_config (
key TEXT PRIMARY KEY,
value TEXT NOT NULL
)
`); err != nil {
return fmt.Errorf("failed to create system_config table: %w", err)
}
// Initialize in dependency order
if err := s.User().initTables(); err != nil {
return fmt.Errorf("failed to initialize user tables: %w", err)
@@ -278,6 +288,25 @@ func (s *Store) DB() *sql.DB {
return s.db
}
// GetSystemConfig gets a system configuration value by key
func (s *Store) GetSystemConfig(key string) (string, error) {
var value string
err := s.db.QueryRow(`SELECT value FROM system_config WHERE key = ?`, key).Scan(&value)
if err == sql.ErrNoRows {
return "", nil
}
return value, err
}
// SetSystemConfig sets a system configuration value
func (s *Store) SetSystemConfig(key, value string) error {
_, err := s.db.Exec(`
INSERT INTO system_config (key, value) VALUES (?, ?)
ON CONFLICT(key) DO UPDATE SET value = excluded.value
`, key, value)
return err
}
// Transaction executes transaction
func (s *Store) Transaction(fn func(tx *sql.Tx) error) error {
tx, err := s.db.Begin()

View File

@@ -5,6 +5,7 @@ import (
"fmt"
"math"
"nofx/decision"
"nofx/experience"
"nofx/logger"
"nofx/market"
"nofx/mcp"
@@ -1710,6 +1711,18 @@ func (at *AutoTrader) recordAndConfirmOrder(orderResult map[string]interface{},
// Record position change with actual fill data
at.recordPositionChange(orderID, symbol, positionSide, action, actualQty, actualPrice, leverage, entryPrice, fee)
// Send anonymous trade statistics for experience improvement (async, non-blocking)
// This helps us understand overall product usage across all deployments
experience.TrackTrade(experience.TradeEvent{
Exchange: at.exchange,
TradeType: action,
Symbol: symbol,
AmountUSD: actualPrice * actualQty,
Leverage: leverage,
UserID: at.userID,
TraderID: at.id,
})
}
// recordPositionChange records position change (create record on open, update record on close)