diff --git a/src/analytics.ts b/src/analytics.ts index 2d9a6cb..456838c 100644 --- a/src/analytics.ts +++ b/src/analytics.ts @@ -2,6 +2,8 @@ import * as Sentry from '@sentry/browser' import { Middleware } from 'driver/connect.js' import { IN_PRODUCTION_MODE, VERSION } from 'env' import { platform } from 'platforms' +import { atomicAsyncFunction } from 'utils/general' +import { storageHelper, storageKeys } from 'utils/storageHelper' const PUBLIC_KEY = 'd22ec5c9cc874539a51c78388c12e3b0' const PROJECT_ID = '1406497' @@ -69,12 +71,43 @@ export const withErrorLog: Middleware = function withErrorLog(method, args) { ] } -export function raiseError( +// 1. Only cache errors for current version, so that future errors can still be exposed +// - Run migration to clean on every update + +// 2. Only cache the top 2 levels of stack, e.g. +// ``` +// Error: cannot get current branch +// at Module.getCurrentBranch (chrome-extension://______id______/index.js:1:1)" +// ``` +// So that different initial callees would not result in multiple records +const MAX_STACK_LEVEL = 2 +const hasTheErrorBeenReported = atomicAsyncFunction(async function hasTheErrorBeenReported( + error: Error, +) { + const message = error.stack?.split('\n').slice(0, MAX_STACK_LEVEL).join('\n') + if (!message) return true // ignore errors that has no stack + + type ErrorCache = string + const cache: ErrorCache[] = + (await storageHelper.get(storageKeys.raiseErrorCache))?.[storageKeys.raiseErrorCache] || [] + const has = cache.includes(message) + + if (!has) { + cache.push(message) + await storageHelper.set({ [storageKeys.raiseErrorCache]: cache }) + } + + return has +}) + +export async function raiseError( error: Error, extra?: { [key: string]: any }, ) { + if (await hasTheErrorBeenReported(error)) return + if (!IN_PRODUCTION_MODE || platform.isEnterprise()) { // ignore errors from enterprise to get less noise on Sentry console.error(error) diff --git a/src/utils/config/helper.ts b/src/utils/config/helper.ts index e541087..1dc332e 100644 --- a/src/utils/config/helper.ts +++ b/src/utils/config/helper.ts @@ -1,6 +1,6 @@ import { SearchMode } from 'components/searchModes' import { platformName } from 'platforms' -import { storageHelper } from 'utils/storageHelper' +import { Storage, storageHelper } from 'utils/storageHelper' import { migrateConfig } from './migrations' export type Config = { @@ -83,7 +83,7 @@ function applyDefaultConfigs(configs: Partial) { }, {} as Config) } -export type VersionedConfig = Record & { configVersion: string } +export type VersionedConfig = Record & Storage export const configRef: Partial = {} const updateConfigRef = async (config: Partial) => { diff --git a/src/utils/config/migrations/1.3.4.ts b/src/utils/config/migrations/1.3.4.ts index 9fd8cd9..7131416 100644 --- a/src/utils/config/migrations/1.3.4.ts +++ b/src/utils/config/migrations/1.3.4.ts @@ -1,12 +1,11 @@ import { storageHelper } from 'utils/storageHelper' import { Migration } from '.' -import { Storage } from '../../storageHelper' import { Config, VersionedConfig } from '../helper' export const migration: Migration = { version: '1.3.4', async migrate(version) { - const config: any | void = await storageHelper.get & Storage>([ + const config: any | void = await storageHelper.get>([ 'configVersion', 'platform_undefined', 'platform_GitHub', diff --git a/src/utils/config/migrations/clearRaiseErrorCache.ts b/src/utils/config/migrations/clearRaiseErrorCache.ts new file mode 100644 index 0000000..a467457 --- /dev/null +++ b/src/utils/config/migrations/clearRaiseErrorCache.ts @@ -0,0 +1,13 @@ +import { storageHelper } from 'utils/storageHelper' +import { Migration, onConfigOutdated } from '.' +import { version } from '../../../../package.json' + +// Run every time a new version is released. +export const migration: Migration = { + version, + async migrate(version) { + await onConfigOutdated(version, async () => { + await storageHelper.set({ raiseErrorCache: [] }) + }) + }, +} diff --git a/src/utils/config/migrations/index.ts b/src/utils/config/migrations/index.ts index 276c197..545c7b8 100644 --- a/src/utils/config/migrations/index.ts +++ b/src/utils/config/migrations/index.ts @@ -1,10 +1,12 @@ -import { storageHelper } from 'utils/storageHelper' +import { storageHelper, storageKeys } from 'utils/storageHelper' +import { version } from '../../../../package.json' import { Storage } from '../../storageHelper' import { migration as v1v0v1 } from './1.0.1' import { migration as v1v3v4 } from './1.3.4' import { migration as v2v6v0 } from './2.6.0' import { migration as v3v0v0 } from './3.0.0' import { migration as v3v5v0 } from './3.5.0' +import { migration as clearRaiseErrorCache } from './clearRaiseErrorCache' export type Migration = { version: string @@ -13,21 +15,29 @@ export type Migration = { export async function migrateConfig() { const migrations: Migration[] = [v1v0v1, v1v3v4, v2v6v0, v3v0v0, v3v5v0] + migrations.push(clearRaiseErrorCache) // Make sure this is run after other version-specific migrations for (const { version, migrate } of migrations) { await migrate(version) } + + await storageHelper.set({ [storageKeys.configVersion]: version }) } export async function onConfigOutdated( - configVersion: string, + migrationConfigVersion: string, runIfOutdated: (config: T) => Async, ) { const config = await storageHelper.get() - if (config && config.configVersion < configVersion) { - const { configVersion: $configVersion, ...restConfig } = config - await runIfOutdated(restConfig as T) - await storageHelper.set({ configVersion }) + if (config) { + const { + [storageKeys.configVersion]: savedConfigVersion, + [storageKeys.raiseErrorCache]: __, + ...restConfig + } = config + if (savedConfigVersion < migrationConfigVersion) { + await runIfOutdated(restConfig as T) + } } } diff --git a/src/utils/storageHelper.ts b/src/utils/storageHelper.ts index e3dc40a..a8fe9bd 100644 --- a/src/utils/storageHelper.ts +++ b/src/utils/storageHelper.ts @@ -1,8 +1,15 @@ const localStorage = browser.storage.local +const keys = { + configVersion: 'configVersion', + raiseErrorCache: 'raiseErrorCache', +} as const + +export const storageKeys = keys + export type Storage = { - // save root level `configVersion` for easier future migrating - [key in EnumString<'configVersion'>]: string + // save root level keys for easier future migrating + [key in EnumString]: string // separate different platform configs to simplify interactions with browser storage API // e.g. @@ -12,7 +19,7 @@ export type Storage = { async function get< T extends { [key: string]: any - } + }, >(mapping: string | string[] | null = null): Promise { try { return (await localStorage.get(mapping || undefined)) as T