mirror of
https://github.com/geekgeekrun/geekgeekrun.git
synced 2026-06-07 08:30:21 +08:00
add gtag
This commit is contained in:
@@ -38,7 +38,8 @@
|
||||
"@geekgeekrun/utils": "workspace:*",
|
||||
"JSONStream": "^1.3.5",
|
||||
"diff": "^7.0.0",
|
||||
"electron-updater": "^6.1.7"
|
||||
"electron-updater": "^6.1.7",
|
||||
"node-machine-id": "^1.1.12"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@electron-toolkit/eslint-config": "^1.0.2",
|
||||
|
||||
@@ -3,6 +3,7 @@ import { checkAndDownloadPuppeteerExecutable } from './utils/puppeteer-executabl
|
||||
import * as fs from 'fs'
|
||||
import { pipeWriteRegardlessError } from '../utils/pipe'
|
||||
import { sleep } from '@geekgeekrun/utils/sleep.mjs'
|
||||
import gtag from '../../utils/gtag'
|
||||
|
||||
export enum DOWNLOAD_ERROR_EXIT_CODE {
|
||||
DOWNLOAD_ERROR = 80
|
||||
@@ -83,9 +84,11 @@ export const checkAndDownloadDependenciesForInit = async () => {
|
||||
})
|
||||
|
||||
await promiseWithResolver.promise
|
||||
gtag('browser_download_finished')
|
||||
app.exit()
|
||||
} catch (err) {
|
||||
console.error(err)
|
||||
gtag('browser_download_error')
|
||||
pipeWriteRegardlessError(
|
||||
pipe,
|
||||
JSON.stringify({
|
||||
|
||||
@@ -11,6 +11,7 @@ import {
|
||||
import { getExecutableFileVersion } from '@geekgeekrun/utils/windows-only/file.mjs'
|
||||
import createCheckAndLocateExistedChromiumExecutableWorker from './worker/find-and-locate-existed-chromium-executable?nodeWorker&url'
|
||||
import { type Worker, isMainThread } from 'node:worker_threads'
|
||||
import gtag from '../../../../utils/gtag'
|
||||
|
||||
const getPuppeteerManagerModule = async () => {
|
||||
let puppeteerManager
|
||||
@@ -82,6 +83,7 @@ export const checkAndDownloadPuppeteerExecutable = async (
|
||||
const puppeteerManager = await getPuppeteerManagerModule()
|
||||
let installedBrowser: InstalledBrowser
|
||||
if (!(await checkCachedPuppeteerExecutable())) {
|
||||
gtag('need_download_browser')
|
||||
try {
|
||||
await options.confirmContinuePromise
|
||||
} catch {
|
||||
@@ -100,6 +102,7 @@ export const checkAndDownloadPuppeteerExecutable = async (
|
||||
downloadProgressCallback: options.downloadProgressCallback
|
||||
})
|
||||
} else {
|
||||
gtag('use_installed_browser')
|
||||
installedBrowser = (
|
||||
await puppeteerManager.getInstalledBrowsers({
|
||||
cacheDir
|
||||
|
||||
@@ -6,6 +6,7 @@ import fs, { WriteStream } from 'node:fs'
|
||||
import { pipeWriteRegardlessError } from '../utils/pipe'
|
||||
import * as JSONStream from 'JSONStream'
|
||||
import { initPowerSaveBlocker } from './power-saver-blocker'
|
||||
import gtag from '../../utils/gtag'
|
||||
|
||||
const rerunInterval = (() => {
|
||||
let v = Number(process.env.MAIN_BOSSGEEKGO_RERUN_INTERVAL)
|
||||
@@ -115,4 +116,6 @@ export function runAutoChatWithDaemon() {
|
||||
type: 'AUTO_START_CHAT_DAEMON_PROCESS_STARTUP'
|
||||
})
|
||||
)
|
||||
|
||||
gtag('run_auto_chat_with_boss_daemon_ready')
|
||||
}
|
||||
|
||||
@@ -14,6 +14,7 @@ import { AUTO_CHAT_ERROR_EXIT_CODE } from '../../../common/enums/auto-start-chat
|
||||
import * as JSONStream from 'JSONStream'
|
||||
|
||||
import SqlitePluginModule from '@geekgeekrun/sqlite-plugin'
|
||||
import gtag from '../../utils/gtag'
|
||||
const { default: SqlitePlugin } = SqlitePluginModule
|
||||
|
||||
const rerunInterval = (() => {
|
||||
@@ -94,6 +95,8 @@ const runAutoChat = async () => {
|
||||
}
|
||||
initPlugins(hooks)
|
||||
await hooks.daemonInitialized.promise()
|
||||
|
||||
gtag('run_auto_chat_with_boss_main_ready')
|
||||
pipeWriteRegardlessError(
|
||||
pipe,
|
||||
JSON.stringify({
|
||||
|
||||
@@ -24,6 +24,7 @@ import { Target } from 'puppeteer'
|
||||
import { pipeWriteRegardlessError } from '../utils/pipe'
|
||||
import * as JSONStream from 'JSONStream'
|
||||
import { ChatStartupFrom } from '@geekgeekrun/sqlite-plugin/dist/entity/ChatStartupLog'
|
||||
import gtag from '../../utils/gtag'
|
||||
|
||||
const __dirname = url.fileURLToPath(new URL('.', import.meta.url))
|
||||
const isRunFromUi = Boolean(process.env.MAIN_BOSSGEEKGO_UI_RUN_MODE)
|
||||
@@ -225,7 +226,7 @@ export async function launchBossSite() {
|
||||
})
|
||||
)
|
||||
//#endregion
|
||||
|
||||
gtag('launch_boss_site_ready')
|
||||
browser.on('targetcreated', (target) => {
|
||||
attachRequestsListener(target)
|
||||
})
|
||||
|
||||
@@ -3,6 +3,7 @@ import { electronApp, optimizer } from '@electron-toolkit/utils'
|
||||
import { createMainWindow } from '../../window/mainWindow'
|
||||
import './app-menu'
|
||||
import initIpc from './ipc'
|
||||
import gtag from '../../utils/gtag'
|
||||
export function openSettingWindow() {
|
||||
// TODO: singleton lock; how can we check if there is another process should run as singleton with arguments?
|
||||
if (!app.requestSingleInstanceLock()) {
|
||||
@@ -37,6 +38,8 @@ export function openSettingWindow() {
|
||||
// dock icon is clicked and there are no other windows open.
|
||||
if (BrowserWindow.getAllWindows().length === 0) createMainWindow()
|
||||
})
|
||||
|
||||
gtag('ui_ready')
|
||||
})
|
||||
|
||||
// Quit when all windows are closed, except on macOS. There, it's common
|
||||
|
||||
101
packages/ui/src/main/utils/gtag/Analytics.ts
Normal file
101
packages/ui/src/main/utils/gtag/Analytics.ts
Normal file
@@ -0,0 +1,101 @@
|
||||
import { machineId } from 'node-machine-id'
|
||||
|
||||
const GA_ENDPOINT = 'https://www.google-analytics.com/mp/collect'
|
||||
const GA_DEBUG_ENDPOINT = 'https://www.google-analytics.com/debug/mp/collect'
|
||||
|
||||
// Get via https://developers.google.com/analytics/devguides/collection/protocol/ga4/sending-events?client_type=gtag#recommended_parameters_for_reports
|
||||
const MEASUREMENT_ID = '<measurement_id>'
|
||||
const API_SECRET = '<api_secret>'
|
||||
const DEFAULT_ENGAGEMENT_TIME_MSEC = 100
|
||||
|
||||
// Duration of inactivity after which a new session is created
|
||||
const SESSION_EXPIRATION_IN_MIN = 30
|
||||
|
||||
let sessionData: null | {
|
||||
session_id: string
|
||||
timestamp: number
|
||||
} = null
|
||||
|
||||
class Analytics {
|
||||
private debug: boolean
|
||||
constructor(debug = false) {
|
||||
this.debug = debug
|
||||
}
|
||||
|
||||
// Returns the client id, or creates a new one if one doesn't exist.
|
||||
// Stores client id in local storage to keep the same client id as long as
|
||||
// the extension is installed.
|
||||
async getOrCreateClientId() {
|
||||
return machineId()
|
||||
}
|
||||
|
||||
// Returns the current session id, or creates a new one if one doesn't exist or
|
||||
// the previous one has expired.
|
||||
async getOrCreateSessionId() {
|
||||
// Use storage.session because it is only in memory
|
||||
const currentTimeInMs = Date.now()
|
||||
// Check if session exists and is still valid
|
||||
if (sessionData && sessionData.timestamp) {
|
||||
// Calculate how long ago the session was last updated
|
||||
const durationInMin = (currentTimeInMs - sessionData.timestamp) / 60000
|
||||
// Check if last update lays past the session expiration threshold
|
||||
if (durationInMin > SESSION_EXPIRATION_IN_MIN) {
|
||||
// Clear old session id to start a new session
|
||||
sessionData = null
|
||||
} else {
|
||||
// Update timestamp to keep session alive
|
||||
sessionData.timestamp = currentTimeInMs
|
||||
}
|
||||
}
|
||||
if (!sessionData) {
|
||||
// Create and store a new session
|
||||
sessionData = {
|
||||
session_id: currentTimeInMs.toString(),
|
||||
timestamp: currentTimeInMs
|
||||
}
|
||||
}
|
||||
return sessionData.session_id
|
||||
}
|
||||
|
||||
// Fires an event with optional params. Event names must only include letters and underscores.
|
||||
async fireEvent(
|
||||
name,
|
||||
params: {
|
||||
session_id?: string
|
||||
engagement_time_msec?: number
|
||||
} = {}
|
||||
) {
|
||||
// Configure session id and engagement time if not present, for more details see:
|
||||
// https://developers.google.com/analytics/devguides/collection/protocol/ga4/sending-events?client_type=gtag#recommended_parameters_for_reports
|
||||
if (!params.session_id) params.session_id = await this.getOrCreateSessionId()
|
||||
|
||||
if (!params.engagement_time_msec) params.engagement_time_msec = DEFAULT_ENGAGEMENT_TIME_MSEC
|
||||
|
||||
try {
|
||||
const response = await fetch(
|
||||
`${
|
||||
this.debug ? GA_DEBUG_ENDPOINT : GA_ENDPOINT
|
||||
}?measurement_id=${MEASUREMENT_ID}&api_secret=${API_SECRET}`,
|
||||
{
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
client_id: await this.getOrCreateClientId(),
|
||||
events: [
|
||||
{
|
||||
name,
|
||||
params
|
||||
}
|
||||
]
|
||||
})
|
||||
}
|
||||
)
|
||||
if (!this.debug) return
|
||||
const res = await response.text()
|
||||
console.log('gtag res', res)
|
||||
} catch (e) {
|
||||
console.error('Google Analytics request failed with an exception', e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default new Analytics(process.env.NODE_ENV === 'development')
|
||||
99
packages/ui/src/main/utils/gtag/index.ts
Normal file
99
packages/ui/src/main/utils/gtag/index.ts
Normal file
@@ -0,0 +1,99 @@
|
||||
import buildInfo from '../../../common/build-info.json'
|
||||
|
||||
type LowercaseLetter =
|
||||
| 'a'
|
||||
| 'b'
|
||||
| 'c'
|
||||
| 'd'
|
||||
| 'e'
|
||||
| 'f'
|
||||
| 'g'
|
||||
| 'h'
|
||||
| 'i'
|
||||
| 'j'
|
||||
| 'k'
|
||||
| 'l'
|
||||
| 'm'
|
||||
| 'n'
|
||||
| 'o'
|
||||
| 'p'
|
||||
| 'q'
|
||||
| 'r'
|
||||
| 's'
|
||||
| 't'
|
||||
| 'u'
|
||||
| 'v'
|
||||
| 'w'
|
||||
| 'x'
|
||||
| 'y'
|
||||
| 'z'
|
||||
type Digit = '0' | '1' | '2' | '3' | '4' | '5' | '6' | '7' | '8' | '9'
|
||||
type Underscore = '_'
|
||||
|
||||
type ValidChar = LowercaseLetter | Digit | Underscore
|
||||
|
||||
type StartsWithLowercase<S extends string> = S extends `${LowercaseLetter}${string}` ? S : never
|
||||
type EndsWithLowercaseOrDigit<S extends string> = S extends `${string}${LowercaseLetter | Digit}`
|
||||
? S
|
||||
: never
|
||||
type ContainsOnlyValidChars<S extends string> = S extends `${ValidChar}${infer Rest}`
|
||||
? ContainsOnlyValidChars<Rest>
|
||||
: S extends ''
|
||||
? S
|
||||
: never
|
||||
|
||||
type ValidString<S extends string> =
|
||||
StartsWithLowercase<S> extends never
|
||||
? never
|
||||
: EndsWithLowercaseOrDigit<S> extends never
|
||||
? never
|
||||
: ContainsOnlyValidChars<S> extends never
|
||||
? never
|
||||
: S
|
||||
|
||||
function getCommonParams() {
|
||||
return {
|
||||
app_version: buildInfo.version,
|
||||
app_build_hash: buildInfo.buildHash,
|
||||
t: Number(new Date())
|
||||
}
|
||||
}
|
||||
|
||||
export default async function gtag<T extends string>(
|
||||
name: ValidString<T>,
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
params: Record<string, any> = {}
|
||||
) {
|
||||
params = {
|
||||
...getCommonParams(),
|
||||
...params
|
||||
}
|
||||
// ServiceWorker环境下直接调用上报函数
|
||||
const reporter = (await import('./Analytics')).default
|
||||
return reporter.fireEvent(name, params)
|
||||
}
|
||||
|
||||
// Fire a page view event.
|
||||
export function gtagPageView(
|
||||
page_title = document?.title ?? '',
|
||||
page_location = location.href,
|
||||
additionalParams = {}
|
||||
) {
|
||||
return gtag('page_view', {
|
||||
page_location,
|
||||
page_title,
|
||||
...getCommonParams(),
|
||||
...additionalParams
|
||||
})
|
||||
}
|
||||
|
||||
// Fire an error event.
|
||||
export function gtagApplicationError(error: string, additionalParams = {}) {
|
||||
// Note: 'error' is a reserved event name and cannot be used
|
||||
// see https://developers.google.com/analytics/devguides/collection/protocol/ga4/reference?client_type=gtag#reserved_names
|
||||
return gtag('application_error', {
|
||||
error,
|
||||
...getCommonParams(),
|
||||
...additionalParams
|
||||
})
|
||||
}
|
||||
7
pnpm-lock.yaml
generated
7
pnpm-lock.yaml
generated
@@ -126,6 +126,9 @@ importers:
|
||||
electron-updater:
|
||||
specifier: ^6.1.7
|
||||
version: 6.1.7
|
||||
node-machine-id:
|
||||
specifier: ^1.1.12
|
||||
version: 1.1.12
|
||||
devDependencies:
|
||||
'@electron-toolkit/eslint-config':
|
||||
specifier: ^1.0.2
|
||||
@@ -4621,6 +4624,10 @@ packages:
|
||||
dev: false
|
||||
optional: true
|
||||
|
||||
/node-machine-id@1.1.12:
|
||||
resolution: {integrity: sha512-QNABxbrPa3qEIfrE6GOJ7BYIuignnJw7iQ2YPbc3Nla1HzRJjXzZOiikfF8m7eAMfichLt3M4VgLOetqgDmgGQ==}
|
||||
dev: false
|
||||
|
||||
/node-releases@2.0.14:
|
||||
resolution: {integrity: sha512-y10wOWt8yZpqXmOgRo77WaHEmhYQYGNA6y421PKsKYWEK8aW+cqAphborZDhqfyKrbZEN92CN1X2KbafY2s7Yw==}
|
||||
dev: true
|
||||
|
||||
Reference in New Issue
Block a user