Browse Source

增加数据统计功能,用GA4

zxlie 9 months ago
parent
commit
d11b989b78
6 changed files with 578 additions and 0 deletions
  1. 58 0
      PRIVACY_POLICY.md
  2. 41 0
      apps/background/background.js
  3. 305 0
      apps/background/statistics.js
  4. 78 0
      server/GA4使用说明.md
  5. 74 0
      server/ga4-proxy.js
  6. 22 0
      server/package.json

+ 58 - 0
PRIVACY_POLICY.md

@@ -0,0 +1,58 @@
+# FeHelper 隐私政策
+
+最后更新日期:2023年10月10日
+
+## 引言
+
+FeHelper("我们"、"我们的"或"本扩展")尊重您的隐私。本隐私政策旨在告知您关于我们如何收集、使用、存储和保护您通过使用我们的Chrome浏览器扩展所提供的信息。
+
+## 我们收集的信息
+
+### 使用数据
+
+我们收集关于您如何使用FeHelper扩展的匿名数据,包括:
+
+- 扩展的安装和更新事件
+- 每日活跃使用情况
+- 使用的具体工具功能(如JSON格式化、二维码生成等)
+- 扩展版本信息
+
+### 用户标识符
+
+为了统计唯一用户数,我们为每个安装的扩展实例生成一个随机唯一标识符(UUID)。这个标识符是匿名的,不包含任何可以直接识别您个人身份的信息。
+
+## 我们如何使用收集的信息
+
+我们收集的信息用于以下目的:
+
+- 改进扩展功能和用户体验
+- 了解哪些功能最受欢迎,以便优先开发和改进
+- 识别并修复可能的问题或错误
+- 了解用户的使用模式以优化性能
+
+## 数据分析服务
+
+我们使用Google Analytics 4(GA4)来收集和分析上述使用数据。GA4受Google隐私政策的约束,您可以在[此处](https://policies.google.com/privacy)查看。
+
+## 数据分享和披露
+
+我们不会出售、出租或以其他方式分享您的个人信息,除非:
+
+- 法律要求我们这样做
+- 为了保护我们的权利、财产或安全
+- 在获得您的同意的情况下
+
+## 数据安全
+
+我们采取合理的安全措施来保护所收集的信息,防止未经授权的访问、使用或披露。
+
+## 隐私政策的变更
+
+我们可能会更新本隐私政策。如有重大变更,我们将通过扩展内通知或更新日期来通知您。
+
+## 联系我们
+
+如果您对本隐私政策有任何疑问,请通过以下方式联系我们:
+
+- 电子邮件:[您的电子邮件地址]
+- 网站:[您的网站地址] 

+ 41 - 0
apps/background/background.js

@@ -10,6 +10,7 @@ import Menu from './menu.js';
 import Awesome from './awesome.js';
 import InjectTools from './inject-tools.js';
 import Monkey from './monkey.js';
+import Statistics from './statistics.js';
 
 
 let BgPageInstance = (function () {
@@ -238,6 +239,9 @@ let BgPageInstance = (function () {
         chrome.DynamicToolRunner({
             tool: MSG_TYPE.JSON_FORMAT
         });
+        
+        // 记录工具使用
+        Statistics.recordToolUsage(MSG_TYPE.JSON_FORMAT);
     };
 
     /**
@@ -369,14 +373,22 @@ let BgPageInstance = (function () {
             // 截屏
             else if (request.type === MSG_TYPE.CAPTURE_VISIBLE_PAGE) {
                 _captureVisibleTab(callback);
+                // 记录工具使用
+                Statistics.recordToolUsage('screenshot');
             }
             // 直接处理content-script.js中的截图请求
             else if (request.type === 'fh-screenshot-capture-visible') {
                 _captureVisibleTab(callback);
+                // 记录工具使用
+                Statistics.recordToolUsage('screenshot');
             }
             // 打开动态工具页面
             else if (request.type === MSG_TYPE.OPEN_DYNAMIC_TOOL) {
                 chrome.DynamicToolRunner(request);
+                // 记录工具使用
+                if (request.page) {
+                    Statistics.recordToolUsage(request.page);
+                }
                 callback && callback();
             }
             // 打开其他页面
@@ -384,6 +396,10 @@ let BgPageInstance = (function () {
                 chrome.DynamicToolRunner({
                     tool: request.page
                 });
+                // 记录工具使用
+                if (request.page) {
+                    Statistics.recordToolUsage(request.page);
+                }
                 callback && callback();
             }
             // 任何事件,都可以通过这个钩子来完成
@@ -405,6 +421,8 @@ let BgPageInstance = (function () {
                                 noPage: true
                             });
                         }
+                        // 记录工具使用
+                        Statistics.recordToolUsage('screenshot');
                         break;
                     case 'request-jsonformat-options':
                         Awesome.StorageMgr.get(request.params).then(result => {
@@ -430,9 +448,13 @@ let BgPageInstance = (function () {
                                 callback && callback(!show);
                             });
                         });
+                        // 记录工具使用
+                        Statistics.recordToolUsage('json-format');
                         return true; // 这个返回true是非常重要的!!!要不然callback会拿不到结果
                     case 'code-beautify':
                         _codeBeautify(request.params);
+                        // 记录工具使用
+                        Statistics.recordToolUsage('code-beautify');
                         break;
                     case 'close-beautify':
                         Awesome.StorageMgr.set('JS_CSS_PAGE_BEAUTIFY',0);
@@ -443,6 +465,8 @@ let BgPageInstance = (function () {
                             tool: 'qr-code',
                             query: `mode=decode`
                         });
+                        // 记录工具使用
+                        Statistics.recordToolUsage('qr-code');
                         break;
                     case 'request-page-content':
                         request.params = FeJson[request.tabId];
@@ -453,18 +477,26 @@ let BgPageInstance = (function () {
                             tool: 'page-timing',
                             withContent: request.wpoInfo
                         });
+                        // 记录工具使用
+                        Statistics.recordToolUsage('page-timing');
                         break;
                     case 'color-picker-capture':
                         _colorPickerCapture(request.params);
+                        // 记录工具使用
+                        Statistics.recordToolUsage('color-picker');
                         break;
                     case 'add-screen-shot-by-pages':
                         _addScreenShotByPages(request.params,callback);
+                        // 记录工具使用
+                        Statistics.recordToolUsage('screenshot');
                         return true;
                     case 'page-screenshot-done':
                         _showScreenShotResult(request.params);
                         break;
                     case 'request-monkey-start':
                         Monkey.start(request.params);
+                        // 记录工具使用
+                        Statistics.recordToolUsage('page-monkey');
                         break;
                     case 'inject-content-css':
                         _injectContentCss(sender.tab.id,request.tool,!!request.devTool);
@@ -502,9 +534,13 @@ let BgPageInstance = (function () {
             switch (reason) {
                 case 'install':
                     chrome.runtime.openOptionsPage();
+                    // 记录新安装用户
+                    Statistics.recordInstallation();
                     break;
                 case 'update':
                     _animateTips('+++1');
+                    // 记录更新安装
+                    Statistics.recordUpdate(previousVersion);
                     if (previousVersion === '2019.12.2415') {
                         notifyText({
                             message: '历尽千辛万苦,FeHelper已升级到最新版本,可以到插件设置页去安装旧版功能了!',
@@ -553,6 +589,8 @@ let BgPageInstance = (function () {
         chrome.contextMenus.onClicked.addListener((info, tab) => {
             if (info.menuItemId === 'fehelper-screenshot-page') {
                 _triggerScreenshotTool(tab.id);
+                // 记录工具使用
+                Statistics.recordToolUsage('screenshot');
             }
         });
         
@@ -563,6 +601,9 @@ let BgPageInstance = (function () {
             contexts: ['page']
         });
         
+        // 初始化统计功能
+        Statistics.init();
+        
         Menu.rebuild();
         // 定期清理冗余的垃圾
         setTimeout(() => {

+ 305 - 0
apps/background/statistics.js

@@ -0,0 +1,305 @@
+/**
+ * FeHelper数据统计模块 - 使用GA4实现
+ * @author fehelper
+ */
+
+import Awesome from './awesome.js';
+
+// GA4测量ID - 需要替换为您自己的GA4测量ID
+const GA4_MEASUREMENT_ID = 'G-1NWRCJRT01';
+const GA4_API_SECRET = 'wHIo3W6uRRCvhZ18hwOmiA';
+
+// 用户ID存储键名
+const USER_ID_KEY = 'FH_USER_ID';
+// 上次使用日期存储键名
+const LAST_ACTIVE_DATE_KEY = 'FH_LAST_ACTIVE_DATE';
+// 用户日常使用数据存储键名
+const USER_USAGE_DATA_KEY = 'FH_USER_USAGE_DATA';
+
+let Statistics = (function() {
+    
+    // 用户唯一标识
+    let userId = '';
+    
+    // 今天的日期字符串 YYYY-MM-DD
+    let todayStr = new Date().toISOString().split('T')[0];
+    
+    // 本地存储的使用数据
+    let usageData = {
+        dailyUsage: {}, // 按日期存储的使用记录
+        tools: {}       // 各工具的使用次数
+    };
+    
+    /**
+     * 生成唯一的用户ID
+     * @returns {string} 用户ID
+     */
+    const generateUserId = () => {
+        return 'fh_' + Date.now() + '_' + Math.random().toString(36).substr(2, 9);
+    };
+    
+    /**
+     * 获取或创建用户ID
+     * @returns {Promise<string>} 用户ID
+     */
+    const getUserId = async () => {
+        if (userId) return userId;
+        
+        try {
+            const result = await Awesome.StorageMgr.get(USER_ID_KEY);
+            if (result) {
+                userId = result;
+            } else {
+                userId = generateUserId();
+                await Awesome.StorageMgr.set(USER_ID_KEY, userId);
+            }
+            return userId;
+        } catch (error) {
+            console.error('获取用户ID失败:', error);
+            return generateUserId(); // 失败时生成临时ID
+        }
+    };
+    
+    /**
+     * 加载本地存储的使用数据
+     * @returns {Promise<void>}
+     */
+    const loadUsageData = async () => {
+        try {
+            const data = await Awesome.StorageMgr.get(USER_USAGE_DATA_KEY);
+            if (data) {
+                usageData = JSON.parse(data);
+            }
+        } catch (error) {
+            console.error('加载使用数据失败:', error);
+        }
+    };
+    
+    /**
+     * 保存使用数据到本地存储
+     * @returns {Promise<void>}
+     */
+    const saveUsageData = async () => {
+        try {
+            await Awesome.StorageMgr.set(USER_USAGE_DATA_KEY, JSON.stringify(usageData));
+        } catch (error) {
+            console.error('保存使用数据失败:', error);
+        }
+    };
+    
+    /**
+     * 使用GA4发送事件数据
+     * @param {string} eventName - 事件名称
+     * @param {Object} params - 事件参数
+     */
+    const sendToGA4 = async (eventName, params = {}) => {
+        // 获取设备和浏览器信息
+        const manifest = chrome.runtime.getManifest();
+        
+        // 确保获取用户ID
+        const uid = await getUserId();
+        
+        // 构建GA4所需参数
+        const gaParams = {
+            client_id: uid,
+            user_id: uid,
+            non_personalized_ads: true,
+            ...params
+        };
+        
+        // GA4主URL
+        const mainURL = `https://www.google-analytics.com/mp/collect?measurement_id=${GA4_MEASUREMENT_ID}&api_secret=${GA4_API_SECRET}`;
+        // 国内备用URL (可以使用自己的代理转发)
+        const backupURL = `https://chrome.fehelper.com/mp/collect?measurement_id=${GA4_MEASUREMENT_ID}&api_secret=${GA4_API_SECRET}`;
+        
+        // 准备发送的数据
+        const data = {
+            client_id: uid,
+            user_id: uid,
+            events: [
+                {
+                    name: eventName,
+                    params: {
+                        extension_version: manifest.version,
+                        ...gaParams
+                    }
+                }
+            ]
+        };
+        
+        try {
+            // 主要尝试直接发送到GA
+            fetch(mainURL, {
+                method: 'POST',
+                body: JSON.stringify(data),
+                keepalive: true,
+                headers: {
+                    'Content-Type': 'application/json'
+                }
+            }).catch(error => {
+                // 如果主要GA服务器失败,尝试备用URL
+                if (backupURL) {
+                    fetch(backupURL, {
+                        method: 'POST',
+                        body: JSON.stringify(data),
+                        keepalive: true,
+                        headers: {
+                            'Content-Type': 'application/json'
+                        }
+                    }).catch(e => console.log('备用GA4统计服务器发送失败:', e));
+                }
+            });
+        } catch (error) {
+            console.log('GA4统计发送失败:', error);
+        }
+    };
+    
+    /**
+     * 记录每日活跃用户
+     * @returns {Promise<void>}
+     */
+    const recordDailyActiveUser = async () => {
+        try {
+            // 获取上次活跃日期
+            const lastActiveDate = await Awesome.StorageMgr.get(LAST_ACTIVE_DATE_KEY);
+            
+            // 如果今天还没有记录,则记录今天的活跃
+            if (lastActiveDate !== todayStr) {
+                await Awesome.StorageMgr.set(LAST_ACTIVE_DATE_KEY, todayStr);
+                
+                // 确保该日期的记录存在
+                if (!usageData.dailyUsage[todayStr]) {
+                    usageData.dailyUsage[todayStr] = {
+                        date: todayStr,
+                        tools: {}
+                    };
+                }
+                
+                // 发送每日活跃记录到GA4
+                sendToGA4('daily_active_user', {
+                    date: todayStr
+                });
+            }
+        } catch (error) {
+            console.error('记录日活跃用户失败:', error);
+        }
+    };
+    
+    /**
+     * 记录插件安装事件
+     */
+    const recordInstallation = async () => {
+        sendToGA4('extension_installed');
+    };
+    
+    /**
+     * 记录插件更新事件
+     * @param {string} previousVersion - 更新前的版本
+     */
+    const recordUpdate = async (previousVersion) => {
+        sendToGA4('extension_updated', {
+            previous_version: previousVersion
+        });
+    };
+    
+    /**
+     * 记录工具使用情况
+     * @param {string} toolName - 工具名称
+     */
+    const recordToolUsage = async (toolName) => {
+        // 确保今天的记录存在
+        if (!usageData.dailyUsage[todayStr]) {
+            usageData.dailyUsage[todayStr] = {
+                date: todayStr,
+                tools: {}
+            };
+        }
+        
+        // 增加工具使用计数
+        if (!usageData.tools[toolName]) {
+            usageData.tools[toolName] = 0;
+        }
+        usageData.tools[toolName]++;
+        
+        // 增加今天该工具的使用计数
+        if (!usageData.dailyUsage[todayStr].tools[toolName]) {
+            usageData.dailyUsage[todayStr].tools[toolName] = 0;
+        }
+        usageData.dailyUsage[todayStr].tools[toolName]++;
+        
+        // 保存使用数据
+        await saveUsageData();
+        
+        // 发送工具使用记录到GA4
+        sendToGA4('tool_used', {
+            tool_name: toolName,
+            date: todayStr
+        });
+    };
+    
+    /**
+     * 定期发送使用摘要数据
+     */
+    const scheduleSyncStats = () => {
+        // 每周发送一次摘要数据
+        const ONE_WEEK = 7 * 24 * 60 * 60 * 1000;
+        setInterval(async () => {
+            // 发送工具使用排名
+            const toolRanking = Object.entries(usageData.tools)
+                .sort((a, b) => b[1] - a[1])
+                .slice(0, 5)
+                .map(([name, count]) => ({name, count}));
+            
+            sendToGA4('usage_summary', {
+                top_tools: JSON.stringify(toolRanking)
+            });
+            
+            // 清理过旧的日期数据(保留30天数据)
+            const now = new Date();
+            const thirtyDaysAgo = new Date(now.getTime() - 30 * 24 * 60 * 60 * 1000);
+            const thirtyDaysAgoStr = thirtyDaysAgo.toISOString().split('T')[0];
+            
+            Object.keys(usageData.dailyUsage).forEach(date => {
+                if (date < thirtyDaysAgoStr) {
+                    delete usageData.dailyUsage[date];
+                }
+            });
+            
+            // 保存清理后的数据
+            await saveUsageData();
+        }, ONE_WEEK);
+    };
+    
+    /**
+     * 初始化GA4测量代码
+     */
+    const initGA4 = async () => {
+        // 获取用户ID
+        const uid = await getUserId();
+        
+        // 发送初始化事件
+        sendToGA4('extension_loaded', {
+            extension_version: chrome.runtime.getManifest().version
+        });
+    };
+    
+    /**
+     * 初始化统计模块
+     */
+    const init = async () => {
+        await getUserId();
+        await loadUsageData();
+        await recordDailyActiveUser();
+        await initGA4();
+        scheduleSyncStats();
+    };
+    
+    return {
+        init,
+        recordInstallation,
+        recordUpdate,
+        recordToolUsage
+    };
+})();
+
+export default Statistics; 

+ 78 - 0
server/GA4使用说明.md

@@ -0,0 +1,78 @@
+# FeHelper插件GA4数据统计使用指南
+
+本文档将指导您如何配置和使用Google Analytics 4 (GA4)来跟踪FeHelper浏览器扩展的使用情况。
+
+## 1. 创建GA4账户和配置
+
+### 1.1 创建GA4属性
+1. 访问[Google Analytics](https://analytics.google.com/)并登录您的Google账户
+2. 创建一个新的GA4属性(如果没有的话)
+3. 记录下您的测量ID(Measurement ID),通常格式为`G-XXXXXXXXXX`
+
+### 1.2 获取API密钥
+1. 在GA4管理界面中,进入`管理` > `数据流` > 选择您的网站/应用数据流
+2. 在数据流详情页面中,找到`测量ID`和`API密钥`部分
+3. 创建一个新的API密钥,设置一个容易记住的名称(如"FeHelper扩展")
+4. 记录下生成的API密钥,这将用于授权数据发送
+
+## 2. 配置FeHelper扩展使用GA4
+
+在`apps/background/statistics.js`文件中,更新以下配置:
+
+```javascript
+// GA4测量ID - 替换为您自己的GA4测量ID
+const GA4_MEASUREMENT_ID = 'G-XXXXXXXXXX';
+const GA4_API_SECRET = 'YOUR_API_SECRET';
+```
+
+请将`G-XXXXXXXXXX`替换为您的GA4测量ID,将`YOUR_API_SECRET`替换为您的API密钥。
+
+## 3. 解决中国大陆访问GA的问题
+
+由于中国大陆地区访问Google服务可能会受到限制,我们提供了一个代理服务器解决方案:
+
+### 3.1 部署GA4代理服务器
+1. 在您可以访问的服务器(如阿里云、腾讯云等)上部署`server/ga4-proxy.js`文件
+2. 安装依赖:`npm install`
+3. 启动服务:`npm start`
+
+### 3.2 配置FeHelper使用代理服务器
+在`apps/background/statistics.js`文件中,更新代理服务器URL:
+
+```javascript
+// 国内备用URL (替换为您的代理服务器地址)
+const backupURL = `https://your-ga4-proxy.com/mp/collect?measurement_id=${GA4_MEASUREMENT_ID}&api_secret=${GA4_API_SECRET}`;
+```
+
+请将`your-ga4-proxy.com`替换为您的代理服务器实际域名或IP地址。
+
+## 4. 跟踪的事件类型
+
+本实现会跟踪以下类型的事件:
+
+| 事件名称 | 说明 | 参数 |
+|---------|------|------|
+| extension_loaded | 扩展启动 | extension_version |
+| daily_active_user | 每日活跃用户 | date |
+| extension_installed | 扩展安装 | - |
+| extension_updated | 扩展更新 | previous_version |
+| tool_used | 工具使用 | tool_name, date |
+| usage_summary | 使用摘要 | top_tools |
+
+## 5. 在GA4中查看数据
+
+1. 登录GA4控制台
+2. 浏览`实时`报告查看当前活跃用户
+3. 在`报告`>`参与度`>`事件`中查看各类事件数据
+4. 您可以创建自定义报告和探索查看特定数据
+
+## 6. 隐私保护
+
+为了保护用户隐私,我们收集的数据都是匿名的,不包含任何可以直接识别用户个人身份的信息。在隐私政策中,我们明确说明了所收集数据的类型和用途。
+
+## 7. 注意事项
+
+- GA4有免费使用额度限制,但对于一般规模的浏览器扩展通常足够
+- 如果您的扩展用户数量非常大,可能需要考虑升级到付费版本
+- 请确保在隐私政策中说明您收集的数据类型和用途
+- 代理服务器需要定期维护,确保其正常运行 

+ 74 - 0
server/ga4-proxy.js

@@ -0,0 +1,74 @@
+/**
+ * FeHelper GA4代理服务器
+ * 用于转发统计数据到GA4服务器,解决国内访问GA服务器被拦截问题
+ */
+
+const express = require('express');
+const fetch = require('node-fetch');
+const bodyParser = require('body-parser');
+const app = express();
+const PORT = process.env.PORT || 3001;
+
+// 中间件
+app.use(bodyParser.json());
+app.use((req, res, next) => {
+    res.header('Access-Control-Allow-Origin', '*');
+    res.header('Access-Control-Allow-Headers', 'Origin, X-Requested-With, Content-Type, Accept');
+    if (req.method === 'OPTIONS') {
+        res.header('Access-Control-Allow-Methods', 'GET, POST');
+        return res.status(200).json({});
+    }
+    next();
+});
+
+// 代理GA4的收集端点
+app.post('/mp/collect', async (req, res) => {
+    try {
+        const measurementId = req.query.measurement_id;
+        const apiSecret = req.query.api_secret;
+        
+        if (!measurementId || !apiSecret) {
+            return res.status(400).json({ error: '缺少必要参数' });
+        }
+        
+        const gaUrl = `https://www.google-analytics.com/mp/collect?measurement_id=${measurementId}&api_secret=${apiSecret}`;
+        
+        // 记录请求数据但不保存敏感信息
+        console.log(`[${new Date().toISOString()}] 代理请求到GA4: ${measurementId}`);
+        
+        // 转发请求到GA4
+        const gaResponse = await fetch(gaUrl, {
+            method: 'POST',
+            headers: {
+                'Content-Type': 'application/json'
+            },
+            body: JSON.stringify(req.body)
+        });
+        
+        // 如果GA4返回成功,返回成功响应
+        if (gaResponse.ok) {
+            return res.status(200).json({ success: true });
+        } else {
+            // 如果GA4返回错误,返回错误响应
+            const errorData = await gaResponse.text();
+            return res.status(gaResponse.status).json({ 
+                error: '转发到GA4失败', 
+                status: gaResponse.status,
+                details: errorData
+            });
+        }
+    } catch (error) {
+        console.error('代理请求失败:', error);
+        res.status(500).json({ error: '代理服务器内部错误' });
+    }
+});
+
+// 健康检查端点
+app.get('/health', (req, res) => {
+    res.status(200).json({ status: 'ok' });
+});
+
+// 启动服务器
+app.listen(PORT, () => {
+    console.log(`FeHelper GA4代理服务器运行在 http://localhost:${PORT}`);
+}); 

+ 22 - 0
server/package.json

@@ -0,0 +1,22 @@
+{
+  "name": "fehelper-statistics-server",
+  "version": "1.0.0",
+  "description": "FeHelper浏览器扩展的统计数据服务器",
+  "main": "ga4-proxy.js",
+  "scripts": {
+    "start": "node ga4-proxy.js",
+    "dev": "nodemon ga4-proxy.js"
+  },
+  "dependencies": {
+    "express": "^4.18.2",
+    "body-parser": "^1.20.2",
+    "node-fetch": "^2.6.9"
+  },
+  "devDependencies": {
+    "nodemon": "^2.0.22"
+  },
+  "engines": {
+    "node": ">=14.0.0"
+  },
+  "private": true
+}