This commit is contained in:
geekgeekrun
2024-10-21 00:58:12 +08:00
parent 46d0dac0e2
commit 48c65cb50a
10 changed files with 226 additions and 2 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View 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')

View 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
View File

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