From 05aea64c3e1420a5794e749fec7c5d943563dfa7 Mon Sep 17 00:00:00 2001 From: Aaron Klinker Date: Sat, 27 May 2023 16:40:30 -0500 Subject: [PATCH 1/7] defineAnalytics setup --- packages/analytics/README.md | 16 +++ packages/analytics/package.json | 69 ++++++++++ packages/analytics/src/clients/index.ts | 3 + packages/analytics/src/clients/types.ts | 68 ++++++++++ .../analytics/src/defineExtensionAnalytics.ts | 45 +++++++ packages/analytics/src/index.ts | 3 + packages/analytics/src/types.ts | 76 +++++++++++ .../analytics/src/utils/createAnalytics.ts | 119 ++++++++++++++++++ .../src/utils/trackStandardEvents.ts | 18 +++ packages/analytics/tsconfig.json | 7 ++ pnpm-lock.yaml | 114 ++++++++--------- 11 files changed, 479 insertions(+), 59 deletions(-) create mode 100644 packages/analytics/README.md create mode 100644 packages/analytics/package.json create mode 100644 packages/analytics/src/clients/index.ts create mode 100644 packages/analytics/src/clients/types.ts create mode 100644 packages/analytics/src/defineExtensionAnalytics.ts create mode 100644 packages/analytics/src/index.ts create mode 100644 packages/analytics/src/types.ts create mode 100644 packages/analytics/src/utils/createAnalytics.ts create mode 100644 packages/analytics/src/utils/trackStandardEvents.ts create mode 100644 packages/analytics/tsconfig.json diff --git a/packages/analytics/README.md b/packages/analytics/README.md new file mode 100644 index 0000000..84973e0 --- /dev/null +++ b/packages/analytics/README.md @@ -0,0 +1,16 @@ +# `@webext-core/analytics` + +A complete analytics solution for web extensions that supports a variety of analytics services. + +- [x] Event tracking +- [x] Page views + +Supports tracking analytics from background scripts, service workers, content scripts, and HTML pages. + +```bash +pnpm i @webext-core/analytics +``` + +## Get Started + +See [documentation](https://webext-core.aklinker1.io/guide/analytics/) to get started! diff --git a/packages/analytics/package.json b/packages/analytics/package.json new file mode 100644 index 0000000..1a2b02b --- /dev/null +++ b/packages/analytics/package.json @@ -0,0 +1,69 @@ +{ + "name": "@webext-core/analytics", + "version": "0.1.0", + "description": "Analytics client for web extensions", + "license": "MIT", + "keywords": [ + "web-extension", + "browser-extension", + "chrome-extension", + "webext", + "web-ext", + "chrome", + "firefox", + "safari", + "browser", + "extension", + "analytics", + "google", + "ga4", + "measurement", + "protocol", + "umami" + ], + "homepage": "https://github.com/aklinker1/webext-core/tree/main/packages/analytics", + "repository": { + "type": "git", + "url": "https://github.com/aklinker1/webext-core", + "directory": "packages/analytics" + }, + "type": "module", + "publishConfig": { + "access": "public" + }, + "files": [ + "lib" + ], + "main": "lib/index.cjs", + "module": "lib/index.js", + "types": "lib/index.d.ts", + "exports": { + ".": { + "import": "./lib/index.js", + "require": "./lib/index.cjs", + "types": "./lib/index.d.ts" + } + }, + "scripts": { + "build": "tsup src/index.ts --clean --out-dir lib --dts --format esm,cjs,iife --global-name webExtCoreAnalytics", + "build:dependencies": "cd ../.. && turbo run build --filter=@webext-core/analytics^...", + "test": "vitest", + "test:coverage": "vitest run --coverage", + "compile": "tsc --noEmit" + }, + "dependencies": { + "@webext-core/proxy-service": "workspace:*", + "@webext-core/storage": "workspace:*", + "ua-parser-js": "^1.0.35", + "webextension-polyfill": "^0.10.0" + }, + "devDependencies": { + "@types/ua-parser-js": "^0.7.36", + "@vitest/coverage-c8": "^0.24.5", + "jsdom": "^20.0.3", + "tsconfig": "workspace:*", + "tsup": "^6.4.0", + "typescript": "^4.8.4", + "vitest": "^0.24.5" + } +} diff --git a/packages/analytics/src/clients/index.ts b/packages/analytics/src/clients/index.ts new file mode 100644 index 0000000..772f0bc --- /dev/null +++ b/packages/analytics/src/clients/index.ts @@ -0,0 +1,3 @@ +export * from './types'; +export * from './defineGoogleAnalyticsClient'; +export * from './defineUmamiClient'; diff --git a/packages/analytics/src/clients/types.ts b/packages/analytics/src/clients/types.ts new file mode 100644 index 0000000..3e16000 --- /dev/null +++ b/packages/analytics/src/clients/types.ts @@ -0,0 +1,68 @@ +/** + * Client responsible for making the API calls to the Analytics service. Clients execute in the + * background script so they aren't effected by CORS. + */ +export interface ExtensionAnalyticsClient { + /** + * Reports an event to the analytics service. Called once for every event. When implementing a + * client, you may choose to batch multiple events into a single network call. + */ + uploadEvent: (options: TrackEventOptions) => Promise; + /** + * If the service supports it, upload page views separate from events. If the service doesn't + * support it, every event also gets the page details, and can be reported inside `trackEvent`. + */ + uploadPageView?: (options: TrackPageViewOptions) => Promise; +} + +export interface TrackBaseOptions { + /** + * ID of the user reporting the event. + */ + userId: string | undefined; + /** + * JS context the event was reported from. + */ + context: string | undefined; + /** + * Screen resolution. + */ + screen: string | undefined; + /** + * The session ID is the timestamp in MS since epoch that the session was started. + */ + sessionId: number | undefined; + /** + * The MS since epoch that the event was reported at. + */ + timestamp: number; + /** + * The operating system name from the `navigator.userAgent` + */ + os: string | undefined; + /** + * The operating system version from the `navigator.userAgent` + */ + osVersion: string | undefined; + /** + * The browser name from the `navigator.userAgent` + */ + browser: string | undefined; + /** + * The browser version from the `navigator.userAgent` + */ + browserVersion: string | undefined; + /** + * Language returned from [`browser.i18n.getUILanguage`](https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/API/I18n/getUILanguage). + */ + language: string | undefined; +} + +export interface TrackPageViewOptions extends TrackBaseOptions { + page?: string; +} + +export interface TrackEventOptions extends TrackPageViewOptions { + action: string; + properties: Record; +} diff --git a/packages/analytics/src/defineExtensionAnalytics.ts b/packages/analytics/src/defineExtensionAnalytics.ts new file mode 100644 index 0000000..e8aaf20 --- /dev/null +++ b/packages/analytics/src/defineExtensionAnalytics.ts @@ -0,0 +1,45 @@ +import { defineProxyService } from '@webext-core/proxy-service'; +import { createExtensionAnalytics } from './utils/createAnalytics'; +import { ExtensionAnalyticsClient } from './clients'; +import { ExtensionAnalytics, ExtensionAnalyticsConfig } from './types'; +import { trackStandardEvents } from './utils/trackStandardEvents'; + +/** + * Create extension analytics with the given configuration. Internally, it uses + * `@webext-core/proxy-service` to execute all HTTP requests in the background. + * + * @example + * // analytics.ts + * export const [registerAnalytics, getAnalytics] = defineExtensionAnalytics({ + * client: createUmamiClient({ ... }), + * isEnabled: () => localExtStorage.getItem("opted-in"), + * }) + */ +export function defineExtensionAnalytics(config: ExtensionAnalyticsConfig) { + const [registerClient, getClient] = defineProxyService( + '@webext-core/analytics-client', + (client: ExtensionAnalyticsClient) => client, + ); + + let singletonAnalytics: ExtensionAnalytics | undefined; + + return [ + function registerAnalytics(): ExtensionAnalytics { + registerClient(config.client); + + const analytics = createExtensionAnalytics(config); + if (!config.disableStandardEvents) trackStandardEvents(analytics); + + return (singletonAnalytics = analytics); + }, + + function getAnalytics(): ExtensionAnalytics { + if (singletonAnalytics) return singletonAnalytics; + + const client = getClient(); + const analytics = createExtensionAnalytics({ ...config, client }); + + return (singletonAnalytics = analytics); + }, + ]; +} diff --git a/packages/analytics/src/index.ts b/packages/analytics/src/index.ts new file mode 100644 index 0000000..1215cec --- /dev/null +++ b/packages/analytics/src/index.ts @@ -0,0 +1,3 @@ +export * from './clients'; +export * from './types'; +export * from './defineExtensionAnalytics'; diff --git a/packages/analytics/src/types.ts b/packages/analytics/src/types.ts new file mode 100644 index 0000000..4ae608d --- /dev/null +++ b/packages/analytics/src/types.ts @@ -0,0 +1,76 @@ +import { ExtensionAnalyticsClient } from './clients'; + +export interface ExtensionAnalytics { + /** + * Sets the current context, this should only be done once at the beginning of each entry point. + * The context is which HTML page is open, which content script is being ran, if it's the + * background page, etc. Will be reported as a property of all events and be apart of page views. + */ + init: (context: string) => void; + /** + * Reports an event with an optional set of properties in key-value format. + */ + trackEvent: (action: string, properties?: Record) => void; + /** + * Tracks a page view. The `pathname` parameter should be the `location.pathname`. Depending on + * the client, this may not send an HTTP request. It might just be stored in memory and included + * in the `trackEvent` properties automatically. + */ + trackPageView: (pathname: string) => void; +} + +export interface ExtensionAnalyticsConfig { + /** + * The client to report analytics to. Use `defineUmamiClient`, `defineGoogleAnalyticsClient`, or + * implement your own `ExtensionAnalyticsClient`. + */ + client: ExtensionAnalyticsClient; + /** + * Before an event is uploaded, this function is executed to see if the user has agreed to + * collecting their data. Return `false` if they have opted-out, and `true` if they have opted-in. + */ + isEnabled: () => boolean | Promise; + /** + * Returns the user id that may be reported with events and page views. If this method is not + * provided, a random UUID will be generated and stored in storage. If the storage permission is + * missing, the user ID will not be included in any reports. + */ + getUserId?: () => string | Promise; + /** + * By default, the analytics application will track some standard events: `extension_installed`, + * `extension_updated`, etc. By setting this to `false`, these events will not be tracked + * automatically; you'll need to track them manually. + * + * @default true + */ + disableStandardEvents?: boolean; + /** + * By default, all events are uploaded. Define this function to sample events on the client side. + * Return `true` if an event should be uploaded. Return false to skip uploading the event. + * + * @param action The event's action that was reported. + * @param sessionRandom A random decimal between 0-1 that can be used to decide if an event should be sampled. + * + * @default () => true + * + * @example + * const DEFAULT_SESSION_SAMPLE_RATE = 0.25; // 25% + * const DEFAULT_ACTION_SAMPLE_RATE = 0.5; // 50% + * + * const ACTION_SAMPLE_RATES = { + * button_clicked: 0.1, // 10% + * extension_installed: 1.0, // 100% + * } + * + * function isEventSampled(action: string, sessionRandom: number): boolean { + * // Only accept events from 25% of sessions + * if (sessionRandom > 0.25) return false; + * + * // Custom sample rates for individual events, defaulting to 50% + * const actionSampleRate = ACTION_SAMPLE_RATES[action] ?? 0.5; + * const actionRandom = Math.random(); + * return actionRandom <= actionSampleRate; + * } + */ + isEventSampled?: (action: string, sessionRandom: number) => boolean | Promise; +} diff --git a/packages/analytics/src/utils/createAnalytics.ts b/packages/analytics/src/utils/createAnalytics.ts new file mode 100644 index 0000000..614eb2b --- /dev/null +++ b/packages/analytics/src/utils/createAnalytics.ts @@ -0,0 +1,119 @@ +import { localExtStorage } from '@webext-core/storage'; +import browser from 'webextension-polyfill'; +import { ExtensionAnalytics, ExtensionAnalyticsConfig } from '../types'; +import { TrackBaseOptions } from '../clients'; +import UAParser from 'ua-parser-js'; + +export function createExtensionAnalytics(config: ExtensionAnalyticsConfig): ExtensionAnalytics { + const { client } = config; + let currentContext: string | undefined; + let currentPage: string | undefined; + const sessionId = Date.now(); + const sessionRandom = Math.random(); + + const isEnabled = async (isSampled?: () => boolean | Promise): Promise => { + const sampled = await (isSampled ?? (() => true))(); + if (!sampled) return false; + + return await isEnabled(); + }; + + const ua = UAParser(navigator.userAgent); + + const getUserId = async (): Promise => { + if (config.getUserId) return await config.getUserId(); + if (browser.storage == null) return undefined; + + const userId: string | null = await localExtStorage.getItem('@webext-core/analytics/userId'); + return userId ?? undefined; + }; + + const getBaseOptions = async (): Promise> => { + return { + browser: ua.browser.name, + browserVersion: ua.browser.version, + language: browser.i18n?.getUILanguage?.() ?? globalThis.navigator?.language, + os: ua.os.name, + osVersion: ua.os.version, + screen: globalThis.screen + ? `${globalThis.screen.width}x${globalThis.screen.height}` + : undefined, + sessionId, + userId: await getUserId(), + }; + }; + + const trackEventAsync = async ( + timestamp: number, + context: string | undefined, + page: string | undefined, + action: string, + properties: Record | undefined, + ) => { + const isEventSampled = config.isEventSampled; + const enabled = await isEnabled( + isEventSampled ? () => isEventSampled(action, sessionRandom) : undefined, + ); + if (!enabled) return; + + const baseOptions = await getBaseOptions(); + + await client + .uploadEvent({ + timestamp, + action, + properties: properties ?? {}, + context, + page, + ...baseOptions, + }) + // ignore network errors + .catch(); + }; + + const trackPageViewAsync = async ( + timestamp: number, + context: string | undefined, + page: string | undefined, + ) => { + if (client.uploadPageView == null) return; + + const enabled = await isEnabled(); + if (!enabled) return; + + const baseOptions = await getBaseOptions(); + await client + .uploadPageView?.({ + timestamp, + context, + page, + ...baseOptions, + }) + // ignore network errors + .catch(); + }; + + return { + init(context) { + currentContext = context; + }, + + trackEvent(action, properties) { + const timestamp = Date.now(); + + // Grab variables synchronously + const context = currentContext; + const page = currentPage; + void trackEventAsync(timestamp, context, page, action, properties); + }, + + trackPageView(page) { + const timestamp = Date.now(); + currentPage = page; + + // Grab variables synchronously + const context = currentContext; + void trackPageViewAsync(timestamp, context, page); + }, + }; +} diff --git a/packages/analytics/src/utils/trackStandardEvents.ts b/packages/analytics/src/utils/trackStandardEvents.ts new file mode 100644 index 0000000..186b6a8 --- /dev/null +++ b/packages/analytics/src/utils/trackStandardEvents.ts @@ -0,0 +1,18 @@ +import browser from 'webextension-polyfill'; +import { ExtensionAnalytics } from '../types'; + +export function trackStandardEvents(analytics: ExtensionAnalytics): void { + // Install events + browser.runtime.onInstalled.addListener(({ reason }) => { + const { version, version_name } = browser.runtime.getManifest(); + if (reason === 'install') { + analytics.trackEvent('extension_installed', { version, version_name }); + } else if (reason === 'update') { + analytics.trackEvent('extension_updated', { version, version_name }); + } else { + analytics.trackEvent('browser_updated', { version, version_name }); + } + }); + + // ... +} diff --git a/packages/analytics/tsconfig.json b/packages/analytics/tsconfig.json new file mode 100644 index 0000000..df70d6a --- /dev/null +++ b/packages/analytics/tsconfig.json @@ -0,0 +1,7 @@ +{ + "extends": "tsconfig/ts-library.json", + "compilerOptions": { + "lib": ["ESNext", "DOM"] + }, + "exclude": ["lib"] +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index f956833..db58701 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -39,6 +39,33 @@ importers: vitest: 0.29.2 webextension-polyfill: 0.10.0 + packages/analytics: + specifiers: + '@types/ua-parser-js': ^0.7.36 + '@vitest/coverage-c8': ^0.24.5 + '@webext-core/proxy-service': workspace:* + '@webext-core/storage': workspace:* + jsdom: ^20.0.3 + tsconfig: workspace:* + tsup: ^6.4.0 + typescript: ^4.8.4 + ua-parser-js: ^1.0.35 + vitest: ^0.24.5 + webextension-polyfill: ^0.10.0 + dependencies: + '@webext-core/proxy-service': link:../proxy-service + '@webext-core/storage': link:../storage + ua-parser-js: 1.0.35 + webextension-polyfill: 0.10.0 + devDependencies: + '@types/ua-parser-js': 0.7.36 + '@vitest/coverage-c8': 0.24.5_jsdom@20.0.3 + jsdom: 20.0.3 + tsconfig: link:../tsconfig + tsup: 6.4.0_typescript@4.8.4 + typescript: 4.8.4 + vitest: 0.24.5_jsdom@20.0.3 + packages/fake-browser: specifiers: '@types/lodash.merge': ^4.6.7 @@ -1073,10 +1100,6 @@ packages: '@types/chai': 4.3.4 dev: true - /@types/chai/4.3.3: - resolution: {integrity: sha512-hC7OMnszpxhZPduX+m+nrx+uFoLkWOMiR4oa/AZF3MuSETYTZmFfJAHqZEM8MVlvfG7BEUcgvtwoCTxBp6hm3g==} - dev: true - /@types/chai/4.3.4: resolution: {integrity: sha512-KnRanxnpfpjUTqTCXslZSEdLfXExwgNxYPdiO2WGUj8+HDjFi8R3k5RVKPeSCzLjCcshCAtVO2QBbVuAV4kTnw==} dev: true @@ -1119,6 +1142,10 @@ packages: resolution: {integrity: sha512-KufADq8uQqo1pYKVIYzfKbJfBAc0sOeXqGbFaSpv8MRmC/zXgowNZmFcbngndGk922QDmOASEXUZCaY48gs4cg==} dev: true + /@types/ua-parser-js/0.7.36: + resolution: {integrity: sha512-N1rW+njavs70y2cApeIw1vLMYXRwfBy+7trgavGuuTfOd7j1Yh7QTRc/yqsPl6ncokt72ZXuxEU0PiCp9bSwNQ==} + dev: true + /@types/web-bluetooth/0.0.17: resolution: {integrity: sha512-4p9vcSmxAayx72yn70joFoL44c9MO/0+iVEBIQXe3v2h2SiAsEIo/G5v6ObFWvNKRFjbrVadNf9LqEEZeQPzdA==} dev: true @@ -1418,7 +1445,7 @@ packages: /acorn-globals/7.0.1: resolution: {integrity: sha512-umOSDSDrfHbTNPuNpC2NSnnA3LUrqpevPb4T9jRx4MagXNS0rs+gwiTcAvqCRmsD6utzsrzNt+ebm00SNWiC3Q==} dependencies: - acorn: 8.8.1 + acorn: 8.8.2 acorn-walk: 8.2.0 dev: true @@ -1896,19 +1923,6 @@ packages: resolution: {integrity: sha512-4tYFyifaFfGacoiObjJegolkwSU4xQNGbVgUiNYVUxbQ2x2lUsFvY4hVgVzGiIe6WLOPqycWXA40l+PWsxthUw==} dev: true - /chai/4.3.6: - resolution: {integrity: sha512-bbcp3YfHCUzMOvKqsztczerVgBKSsEijCySNlHHbX3VG1nskvqjz5Rfso1gGwD6w6oOV3eI60pKuMOV5MV7p3Q==} - engines: {node: '>=4'} - dependencies: - assertion-error: 1.1.0 - check-error: 1.0.2 - deep-eql: 3.0.1 - get-func-name: 2.0.0 - loupe: 2.3.5 - pathval: 1.1.1 - type-detect: 4.0.8 - dev: true - /chai/4.3.7: resolution: {integrity: sha512-HLnAzZ2iupm25PlN0xFreAlBA5zaBSv3og0DdeGA4Ar6h6rJ3A0rolRUKJhSF2V10GZKDgWF/VmAEsNWjCRB+A==} engines: {node: '>=4'} @@ -2307,13 +2321,6 @@ packages: mimic-response: 3.1.0 dev: true - /deep-eql/3.0.1: - resolution: {integrity: sha512-+QeIQyN5ZuO+3Uk5DYh6/1eKO0m0YmJFGNmFHGACpf1ClL1nmlV/p4gNgbl2pJGxgXb4faqo6UE+M5ACEMyVcw==} - engines: {node: '>=0.12'} - dependencies: - type-detect: 4.0.8 - dev: true - /deep-eql/4.1.3: resolution: {integrity: sha512-WaEtAOpRA1MQ0eohqZjpGD8zdI0Ovsm8mmFhaDN8dvDZzyoUMcYDnf5Y6iu7HTXxf8JDS23qWa4a+hKCDyOPzw==} engines: {node: '>=6'} @@ -3743,7 +3750,7 @@ packages: optional: true dependencies: abab: 2.0.6 - acorn: 8.8.1 + acorn: 8.8.2 acorn-globals: 7.0.1 cssom: 0.5.0 cssstyle: 2.3.0 @@ -4056,12 +4063,6 @@ packages: wrap-ansi: 8.1.0 dev: true - /loupe/2.3.5: - resolution: {integrity: sha512-KNGVjhsXDxvY/cYE8GNi7SBaJSfJIT+/+/8GlprqBXpoU6cSR7/RT7OBJOsoYtyxq0L3q6oIcO8tX7dbEEXr3A==} - dependencies: - get-func-name: 2.0.0 - dev: true - /loupe/2.3.6: resolution: {integrity: sha512-RaPMZKiMy8/JruncMU5Bt6na1eftNoo++R4Y+N2FrxkDVTrGvcyzFTsaGif4QTeKESheMGegbhw6iUAq+5A8zA==} dependencies: @@ -5007,14 +5008,6 @@ packages: fsevents: 2.3.2 dev: true - /rollup/3.2.5: - resolution: {integrity: sha512-/Ha7HhVVofduy+RKWOQJrxe4Qb3xyZo+chcpYiD8SoQa4AG7llhupUtyfKSSrdBM2mWJjhM8wZwmbY23NmlIYw==} - engines: {node: '>=14.18.0', npm: '>=8.0.0'} - hasBin: true - optionalDependencies: - fsevents: 2.3.2 - dev: true - /rollup/3.21.6: resolution: {integrity: sha512-SXIICxvxQxR3D4dp/3LDHZIJPC8a4anKMHd4E3Jiz2/JnY+2bEjqrOokAauc5ShGVNFHlEFjBXAXlaxkJqIqSg==} engines: {node: '>=14.18.0', npm: '>=8.0.0'} @@ -5363,7 +5356,7 @@ packages: /strip-literal/0.4.2: resolution: {integrity: sha512-pv48ybn4iE1O9RLgCAN0iU4Xv7RlBTiit6DKmMiErbs9x1wH6vXBs45tWc0H5wUIF6TLTrKweqkmYF/iraQKNw==} dependencies: - acorn: 8.8.1 + acorn: 8.8.2 dev: true /strip-literal/1.0.1: @@ -5413,7 +5406,7 @@ packages: engines: {node: '>=8'} dependencies: '@istanbuljs/schema': 0.1.3 - glob: 7.1.6 + glob: 7.2.0 minimatch: 3.1.2 dev: true @@ -5448,11 +5441,6 @@ packages: resolution: {integrity: sha512-hGYWYBMPr7p4g5IarQE7XhlyWveh1EKhy4wUBS1LrHXCKYgvz+4/jCqgmJqZxxldesn05vccrtME2RLLZNW7iA==} dev: true - /tinypool/0.3.0: - resolution: {integrity: sha512-NX5KeqHOBZU6Bc0xj9Vr5Szbb1j8tUHIeD18s41aDJaPeC5QTdEhK0SpdpUrZlj2nv5cctNcSjaKNanXlfcVEQ==} - engines: {node: '>=14.0.0'} - dev: true - /tinypool/0.3.1: resolution: {integrity: sha512-zLA1ZXlstbU2rlpA4CIeVaqvWq41MTWqLY3FfsAXgC8+f7Pk7zroaJQxDgxn1xNudKW6Kmj4808rPFShUlIRmQ==} engines: {node: '>=14.0.0'} @@ -5565,7 +5553,7 @@ packages: joycon: 3.1.1 postcss-load-config: 3.1.4 resolve-from: 5.0.0 - rollup: 3.2.5 + rollup: 3.21.6 source-map: 0.8.0-beta.0 sucrase: 3.28.0 tree-kill: 1.2.2 @@ -5712,6 +5700,10 @@ packages: hasBin: true dev: false + /ua-parser-js/1.0.35: + resolution: {integrity: sha512-fKnGuqmTBnIE+/KXSzCn4db8RTigUzw1AN0DmdU6hJovUTbYJKyqj+8Mt1c4VfRDnOVJnENmfYkIPZ946UrSAA==} + dev: false + /ufo/1.1.1: resolution: {integrity: sha512-MvlCc4GHrmZdAllBc0iUDowff36Q9Ndw/UzqmEKyrfSzokTd9ZCy1i+IIk5hrYKkjoYVQyNbrw7/F8XJ2rEwTg==} dev: true @@ -5929,17 +5921,20 @@ packages: - utf-8-validate dev: true - /vite/3.2.2: - resolution: {integrity: sha512-pLrhatFFOWO9kS19bQ658CnRYzv0WLbsPih6R+iFeEEhDOuYgYCX2rztUViMz/uy/V8cLCJvLFeiOK7RJEzHcw==} + /vite/3.2.4: + resolution: {integrity: sha512-Z2X6SRAffOUYTa+sLy3NQ7nlHFU100xwanq1WDwqaiFiCe+25zdxP1TfCS5ojPV2oDDcXudHIoPnI1Z/66B7Yw==} engines: {node: ^14.18.0 || >=16.0.0} hasBin: true peerDependencies: + '@types/node': '>= 14' less: '*' sass: '*' stylus: '*' sugarss: '*' terser: ^5.4.0 peerDependenciesMeta: + '@types/node': + optional: true less: optional: true sass: @@ -5959,7 +5954,7 @@ packages: fsevents: 2.3.2 dev: true - /vite/3.2.4: + /vite/3.2.4_@types+node@18.11.9: resolution: {integrity: sha512-Z2X6SRAffOUYTa+sLy3NQ7nlHFU100xwanq1WDwqaiFiCe+25zdxP1TfCS5ojPV2oDDcXudHIoPnI1Z/66B7Yw==} engines: {node: ^14.18.0 || >=16.0.0} hasBin: true @@ -5984,6 +5979,7 @@ packages: terser: optional: true dependencies: + '@types/node': 18.11.9 esbuild: 0.15.13 postcss: 8.4.18 resolve: 1.22.1 @@ -6176,17 +6172,17 @@ packages: jsdom: optional: true dependencies: - '@types/chai': 4.3.3 + '@types/chai': 4.3.4 '@types/chai-subset': 1.3.3 '@types/node': 18.11.9 - chai: 4.3.6 + chai: 4.3.7 debug: 4.3.4 local-pkg: 0.4.2 strip-literal: 0.4.2 tinybench: 2.3.1 - tinypool: 0.3.0 + tinypool: 0.3.1 tinyspy: 1.0.2 - vite: 3.2.2 + vite: 3.2.4_@types+node@18.11.9 transitivePeerDependencies: - less - sass @@ -6218,18 +6214,18 @@ packages: jsdom: optional: true dependencies: - '@types/chai': 4.3.3 + '@types/chai': 4.3.4 '@types/chai-subset': 1.3.3 '@types/node': 18.11.9 - chai: 4.3.6 + chai: 4.3.7 debug: 4.3.4 jsdom: 20.0.3 local-pkg: 0.4.2 strip-literal: 0.4.2 tinybench: 2.3.1 - tinypool: 0.3.0 + tinypool: 0.3.1 tinyspy: 1.0.2 - vite: 3.2.2 + vite: 3.2.4_@types+node@18.11.9 transitivePeerDependencies: - less - sass From 00e618e4db5d665d3fbb12dce4a7378881af66b2 Mon Sep 17 00:00:00 2001 From: Aaron Klinker Date: Sat, 27 May 2023 16:40:42 -0500 Subject: [PATCH 2/7] Docs setup --- docs/.vitepress/config.ts | 19 ++ docs/.vitepress/plugins/typescript-docs.ts | 5 +- docs/api/analytics.md | 342 +++++++++++++++++++++ 3 files changed, 365 insertions(+), 1 deletion(-) create mode 100644 docs/api/analytics.md diff --git a/docs/.vitepress/config.ts b/docs/.vitepress/config.ts index 385310e..c610e44 100644 --- a/docs/.vitepress/config.ts +++ b/docs/.vitepress/config.ts @@ -8,6 +8,7 @@ const ogUrl = 'https://webext-core.aklinker1.io'; const packageDirnames = [ 'storage', 'messaging', + 'analytics', 'job-scheduler', 'match-patterns', 'proxy-service', @@ -86,6 +87,24 @@ const packagePages = { link: '/guide/match-patterns/', }, ], + analytics: [ + { + text: 'Get Started', + link: '/guide/analytics/', + }, + { + text: 'Google Analytics', + link: '/guide/analytics/google-analytics', + }, + { + text: 'Umami', + link: '/guide/analytics/umami', + }, + { + text: 'Custom Analytics Service', + link: '/guide/analytics/custom', + }, + ], }; const packagesItemGroup = packageDirnames.map(dirname => ({ diff --git a/docs/.vitepress/plugins/typescript-docs.ts b/docs/.vitepress/plugins/typescript-docs.ts index 7fe75c7..a35c8ec 100644 --- a/docs/.vitepress/plugins/typescript-docs.ts +++ b/docs/.vitepress/plugins/typescript-docs.ts @@ -166,7 +166,10 @@ async function removeWatchListeners(ctx: Ctx) { */ async function getPackages(): Promise { const all = await fs.readdir('packages'); - return all.filter(folderName => !folderName.endsWith('-demo') && folderName !== 'tsconfig'); + return all.filter( + folderName => + !folderName.endsWith('-demo') && folderName !== 'tsconfig' && !folderName.startsWith('.'), + ); } async function generateAll( diff --git a/docs/api/analytics.md b/docs/api/analytics.md new file mode 100644 index 0000000..7be26e0 --- /dev/null +++ b/docs/api/analytics.md @@ -0,0 +1,342 @@ + + +# API Reference - `analytics` + +> [`@webext-core/analytics`](/guide/analytics/) + +## `Array` + +```ts +interface Array { + length: number; + toString(): string; + toLocaleString(): string; + pop(): T | undefined; + push(...items: T[]): number; + concat(...items: ConcatArray[]): T[]; + concat(...items: (T | ConcatArray)[]): T[]; + join(separator?: string): string; + reverse(): T[]; + shift(): T | undefined; + slice(start?: number, end?: number): T[]; + sort(compareFn?: (a: T, b: T) => number): this; + splice(start: number, deleteCount?: number): T[]; + splice(start: number, deleteCount: number, ...items: T[]): T[]; + unshift(...items: T[]): number; + indexOf(searchElement: T, fromIndex?: number): number; + lastIndexOf(searchElement: T, fromIndex?: number): number; + every( + predicate: (value: T, index: number, array: T[]) => value is S, + thisArg?: any + ): this is S[]; + every( + predicate: (value: T, index: number, array: T[]) => unknown, + thisArg?: any + ): boolean; + some( + predicate: (value: T, index: number, array: T[]) => unknown, + thisArg?: any + ): boolean; + forEach( + callbackfn: (value: T, index: number, array: T[]) => void, + thisArg?: any + ): void; + map( + callbackfn: (value: T, index: number, array: T[]) => U, + thisArg?: any + ): U[]; + filter( + predicate: (value: T, index: number, array: T[]) => value is S, + thisArg?: any + ): S[]; + filter( + predicate: (value: T, index: number, array: T[]) => unknown, + thisArg?: any + ): T[]; + reduce( + callbackfn: ( + previousValue: T, + currentValue: T, + currentIndex: number, + array: T[] + ) => T + ): T; + reduce( + callbackfn: ( + previousValue: T, + currentValue: T, + currentIndex: number, + array: T[] + ) => T, + initialValue: T + ): T; + reduce( + callbackfn: ( + previousValue: U, + currentValue: T, + currentIndex: number, + array: T[] + ) => U, + initialValue: U + ): U; + reduceRight( + callbackfn: ( + previousValue: T, + currentValue: T, + currentIndex: number, + array: T[] + ) => T + ): T; + reduceRight( + callbackfn: ( + previousValue: T, + currentValue: T, + currentIndex: number, + array: T[] + ) => T, + initialValue: T + ): T; + reduceRight( + callbackfn: ( + previousValue: U, + currentValue: T, + currentIndex: number, + array: T[] + ) => U, + initialValue: U + ): U; + [n: number]: T; +} + +var Array: ArrayConstructor; + +interface Array { + find( + predicate: (this: void, value: T, index: number, obj: T[]) => value is S, + thisArg?: any + ): S | undefined; + find( + predicate: (value: T, index: number, obj: T[]) => unknown, + thisArg?: any + ): T | undefined; + findIndex( + predicate: (value: T, index: number, obj: T[]) => unknown, + thisArg?: any + ): number; + fill(value: T, start?: number, end?: number): this; + copyWithin(target: number, start: number, end?: number): this; +} + +interface Array { + [Symbol.iterator](): IterableIterator; + entries(): IterableIterator<[number, T]>; + keys(): IterableIterator; + values(): IterableIterator; +} + +interface Array { + [Symbol.unscopables](): { + copyWithin: boolean; + entries: boolean; + fill: boolean; + find: boolean; + findIndex: boolean; + keys: boolean; + values: boolean; + }; +} + +interface Array { + includes(searchElement: T, fromIndex?: number): boolean; +} + +interface Array { + flatMap( + callback: ( + this: This, + value: T, + index: number, + array: T[] + ) => U | ReadonlyArray, + thisArg?: This + ): U[]; + flat(this: A, depth?: D): FlatArray[]; +} + +interface Array { + at(index: number): T | undefined; +} +``` + +### Properties + +- ***`length:number`*** + +## `defineExtensionAnalytics` + +```ts +function defineExtensionAnalytics(config: ExtensionAnalyticsConfig) { + // ... +} +``` + +Create extension analytics with the given configuration. Internally, it uses +`@webext-core/proxy-service` to execute all HTTP requests in the background. + +### Examples + +```ts +// analytics.ts +export const [registerAnalytics, getAnalytics] = defineExtensionAnalytics({ + client: createUmamiClient({ ... }), + isEnabled: () => localExtStorage.getItem("opted-in"), +}) +``` + +## `ExtensionAnalytics` + +```ts +interface ExtensionAnalytics { + init: (context: string) => void; + trackEvent: (action: string, properties?: Record) => void; + trackPageView: (pathname: string) => void; +} +``` + +### Properties + +- ***`init: (context: string) => void`***
Sets the current context, this should only be done once at the beginning of each entry point. +The context is which HTML page is open, which content script is being ran, if it's the +background page, etc. Will be reported as a property of all events and be apart of page views. + +- ***`trackEvent: (action: string, properties?: Record) => void`***
Reports an event with an optional set of properties in key-value format. + +- ***`trackPageView: (pathname: string) => void`***
Tracks a page view. The `pathname` parameter should be the `location.pathname`. Depending on +the client, this may not send an HTTP request. It might just be stored in memory and included +in the `trackEvent` properties automatically. + +## `ExtensionAnalyticsClient` + +```ts +interface ExtensionAnalyticsClient { + uploadEvent: (options: TrackEventOptions) => Promise; + uploadPageView?: (options: TrackPageViewOptions) => Promise; +} +``` + +Client responsible for making the API calls to the Analytics service. Clients execute in the +background script so they aren't effected by CORS. + +### Properties + +- ***`uploadEvent: (options: TrackEventOptions) => Promise`***
Reports an event to the analytics service. Called once for every event. When implementing a +client, you may choose to batch multiple events into a single network call. + +- ***`uploadPageView?: (options: TrackPageViewOptions) => Promise`***
If the service supports it, upload page views separate from events. If the service doesn't +support it, every event also gets the page details, and can be reported inside `trackEvent`. + +## `ExtensionAnalyticsConfig` + +```ts +interface ExtensionAnalyticsConfig { + client: ExtensionAnalyticsClient; + isEnabled: () => boolean | Promise; + getUserId?: () => string | Promise; + disableStandardEvents?: boolean; + isEventSampled?: ( + action: string, + sessionRandom: number + ) => boolean | Promise; +} +``` + +### Properties + +- ***`client: ExtensionAnalyticsClient`***
The client to report analytics to. Use `defineUmamiClient`, `defineGoogleAnalyticsClient`, or +implement your own `ExtensionAnalyticsClient`. + +- ***`isEnabled: () => boolean | Promise`***
Before an event is uploaded, this function is executed to see if the user has agreed to +collecting their data. Return `false` if they have opted-out, and `true` if they have opted-in. + +- ***`getUserId?: () => string | Promise`***
Returns the user id that may be reported with events and page views. If this method is not +provided, a random UUID will be generated and stored in storage. If the storage permission is +missing, the user ID will not be included in any reports. + +- ***`disableStandardEvents?: boolean`*** (default: `true`)
By default, the analytics application will track some standard events: `extension_installed`, +`extension_updated`, etc. By setting this to `false`, these events will not be tracked +automatically; you'll need to track them manually. + +- ***`isEventSampled?: (action: string, sessionRandom: number) => boolean | Promise`*** (default: `() => true`)
By default, all events are uploaded. Define this function to sample events on the client side. +Return `true` if an event should be uploaded. Return false to skip uploading the event. + +## `TrackBaseOptions` + +```ts +interface TrackBaseOptions { + userId: string | undefined; + context: string | undefined; + screen: string | undefined; + sessionId: number | undefined; + timestamp: number; + os: string | undefined; + osVersion: string | undefined; + browser: string | undefined; + browserVersion: string | undefined; + language: string | undefined; +} +``` + +### Properties + +- ***`userId: string | undefined`***
ID of the user reporting the event. + +- ***`context: string | undefined`***
JS context the event was reported from. + +- ***`screen: string | undefined`***
Screen resolution. + +- ***`sessionId: number | undefined`***
The session ID is the timestamp in MS since epoch that the session was started. + +- ***`timestamp: number`***
The MS since epoch that the event was reported at. + +- ***`os: string | undefined`***
The operating system name from the `navigator.userAgent` + +- ***`osVersion: string | undefined`***
The operating system version from the `navigator.userAgent` + +- ***`browser: string | undefined`***
The browser name from the `navigator.userAgent` + +- ***`browserVersion: string | undefined`***
The browser version from the `navigator.userAgent` + +- ***`language: string | undefined`***
Language returned from [`browser.i18n.getUILanguage`](https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/API/I18n/getUILanguage). + +## `TrackEventOptions` + +```ts +interface TrackEventOptions extends TrackPageViewOptions { + action: string; + properties: Record; +} +``` + +### Properties + +- ***`action: string`*** + +- ***`properties: Record`*** + +## `TrackPageViewOptions` + +```ts +interface TrackPageViewOptions extends TrackBaseOptions { + page?: string; +} +``` + +### Properties + +- ***`page?: string`*** + +

+ +--- + +_API reference generated by [`plugins/typescript-docs.ts`](https://github.com/aklinker1/webext-core/blob/main/docs/.vitepress/plugins/typescript-docs.ts)_ \ No newline at end of file From 03f9a268a6159aedcdbfc5df0a90d5f89c7c0f3d Mon Sep 17 00:00:00 2001 From: Aaron Klinker Date: Sat, 27 May 2023 19:30:28 -0500 Subject: [PATCH 3/7] Add Umami and GA4 clients --- docs/.vitepress/plugins/typescript-docs.ts | 11 +- docs/api/analytics.md | 230 ++++++------------ packages/analytics/package.json | 1 - .../clients/createGoogleAnalyticsClient.ts | 97 ++++++++ .../src/clients/createUmamiClient.ts | 79 ++++++ packages/analytics/src/clients/index.ts | 4 +- packages/analytics/src/clients/types.ts | 12 +- packages/analytics/src/types.ts | 6 - .../analytics/src/utils/createAnalytics.ts | 12 +- pnpm-lock.yaml | 2 - 10 files changed, 266 insertions(+), 188 deletions(-) create mode 100644 packages/analytics/src/clients/createGoogleAnalyticsClient.ts create mode 100644 packages/analytics/src/clients/createUmamiClient.ts diff --git a/docs/.vitepress/plugins/typescript-docs.ts b/docs/.vitepress/plugins/typescript-docs.ts index a35c8ec..caa1103 100644 --- a/docs/.vitepress/plugins/typescript-docs.ts +++ b/docs/.vitepress/plugins/typescript-docs.ts @@ -127,6 +127,7 @@ type SymbolLinks = { [symbolName: string]: string }; const EXTERNAL_SYMBOLS: SymbolLinks = { Promise: 'https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise', + Array: 'https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array', 'Storage.StorageArea': 'https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/API/storage/StorageArea', StorageArea: @@ -506,7 +507,15 @@ function getTypeDeclarations(project: Project, symbol: Symbol): string[] { }`, ); }) - .map(text => prettier.format(text, { printWidth: 80, parser: 'typescript' }).trimEnd()); + .map(text => { + try { + return prettier.format(text, { printWidth: 80, parser: 'typescript' }).trimEnd(); + } catch (err) { + console.log('Symbol:', symbol.getName()); + console.log(text); + throw err; + } + }); } function warn(message: string) { diff --git a/docs/api/analytics.md b/docs/api/analytics.md index 7be26e0..403d745 100644 --- a/docs/api/analytics.md +++ b/docs/api/analytics.md @@ -4,173 +4,47 @@ > [`@webext-core/analytics`](/guide/analytics/) -## `Array` +## `createGoogleAnalyticsClient` ```ts -interface Array { - length: number; - toString(): string; - toLocaleString(): string; - pop(): T | undefined; - push(...items: T[]): number; - concat(...items: ConcatArray[]): T[]; - concat(...items: (T | ConcatArray)[]): T[]; - join(separator?: string): string; - reverse(): T[]; - shift(): T | undefined; - slice(start?: number, end?: number): T[]; - sort(compareFn?: (a: T, b: T) => number): this; - splice(start: number, deleteCount?: number): T[]; - splice(start: number, deleteCount: number, ...items: T[]): T[]; - unshift(...items: T[]): number; - indexOf(searchElement: T, fromIndex?: number): number; - lastIndexOf(searchElement: T, fromIndex?: number): number; - every( - predicate: (value: T, index: number, array: T[]) => value is S, - thisArg?: any - ): this is S[]; - every( - predicate: (value: T, index: number, array: T[]) => unknown, - thisArg?: any - ): boolean; - some( - predicate: (value: T, index: number, array: T[]) => unknown, - thisArg?: any - ): boolean; - forEach( - callbackfn: (value: T, index: number, array: T[]) => void, - thisArg?: any - ): void; - map( - callbackfn: (value: T, index: number, array: T[]) => U, - thisArg?: any - ): U[]; - filter( - predicate: (value: T, index: number, array: T[]) => value is S, - thisArg?: any - ): S[]; - filter( - predicate: (value: T, index: number, array: T[]) => unknown, - thisArg?: any - ): T[]; - reduce( - callbackfn: ( - previousValue: T, - currentValue: T, - currentIndex: number, - array: T[] - ) => T - ): T; - reduce( - callbackfn: ( - previousValue: T, - currentValue: T, - currentIndex: number, - array: T[] - ) => T, - initialValue: T - ): T; - reduce( - callbackfn: ( - previousValue: U, - currentValue: T, - currentIndex: number, - array: T[] - ) => U, - initialValue: U - ): U; - reduceRight( - callbackfn: ( - previousValue: T, - currentValue: T, - currentIndex: number, - array: T[] - ) => T - ): T; - reduceRight( - callbackfn: ( - previousValue: T, - currentValue: T, - currentIndex: number, - array: T[] - ) => T, - initialValue: T - ): T; - reduceRight( - callbackfn: ( - previousValue: U, - currentValue: T, - currentIndex: number, - array: T[] - ) => U, - initialValue: U - ): U; - [n: number]: T; +function createGoogleAnalyticsClient( + config: GoogleAnalyticsConfig +): ExtensionAnalyticsClient { + // ... } +``` -var Array: ArrayConstructor; - -interface Array { - find( - predicate: (this: void, value: T, index: number, obj: T[]) => value is S, - thisArg?: any - ): S | undefined; - find( - predicate: (value: T, index: number, obj: T[]) => unknown, - thisArg?: any - ): T | undefined; - findIndex( - predicate: (value: T, index: number, obj: T[]) => unknown, - thisArg?: any - ): number; - fill(value: T, start?: number, end?: number): this; - copyWithin(target: number, start: number, end?: number): this; -} +Returns a client for reporting analytics to Google Analytics 4 through the +[Measurement Protocol](https://developers.google.com/analytics/devguides/collection/protocol/ga4). -interface Array { - [Symbol.iterator](): IterableIterator; - entries(): IterableIterator<[number, T]>; - keys(): IterableIterator; - values(): IterableIterator; -} +It is worth noting that the measurment protocol restricts the reporting of some events, user +properties, and event parameters. [See the docs](https://developers.google.com/analytics/devguides/collection/protocol/ga4/reference?client_type=firebase#reserved_names) +for more information. That means that this client WILL NOT provide the same amount of stats as +your standard web, gtag setup. -interface Array { - [Symbol.unscopables](): { - copyWithin: boolean; - entries: boolean; - fill: boolean; - find: boolean; - findIndex: boolean; - keys: boolean; - values: boolean; - }; -} +The client will: -interface Array { - includes(searchElement: T, fromIndex?: number): boolean; -} +- Upload a single event per network request +- Send the `context` and `page` as event parameterss +- Does not upload anything for `trackPageView` - the `page_view` event is one of the restricted events for the MP API -interface Array { - flatMap( - callback: ( - this: This, - value: T, - index: number, - array: T[] - ) => U | ReadonlyArray, - thisArg?: This - ): U[]; - flat(this: A, depth?: D): FlatArray[]; -} +## `createUmamiClient` -interface Array { - at(index: number): T | undefined; +```ts +function createUmamiClient(config: UmamiConfig): ExtensionAnalyticsClient { + // ... } ``` -### Properties +Umami is a privacy focused alternative to google analytics. -- ***`length:number`*** +> https://umami.is/ + +The Umami client returned by this function: + +- Uploads a single event at a time +- Sends the `context` string as the `hostname` parameter +- Does not upload anything for `trackPageView` - pages are apart of events. ## `defineExtensionAnalytics` @@ -241,7 +115,6 @@ support it, every event also gets the page details, and can be reported inside ` interface ExtensionAnalyticsConfig { client: ExtensionAnalyticsClient; isEnabled: () => boolean | Promise; - getUserId?: () => string | Promise; disableStandardEvents?: boolean; isEventSampled?: ( action: string, @@ -258,10 +131,6 @@ implement your own `ExtensionAnalyticsClient`. - ***`isEnabled: () => boolean | Promise`***
Before an event is uploaded, this function is executed to see if the user has agreed to collecting their data. Return `false` if they have opted-out, and `true` if they have opted-in. -- ***`getUserId?: () => string | Promise`***
Returns the user id that may be reported with events and page views. If this method is not -provided, a random UUID will be generated and stored in storage. If the storage permission is -missing, the user ID will not be included in any reports. - - ***`disableStandardEvents?: boolean`*** (default: `true`)
By default, the analytics application will track some standard events: `extension_installed`, `extension_updated`, etc. By setting this to `false`, these events will not be tracked automatically; you'll need to track them manually. @@ -269,11 +138,30 @@ automatically; you'll need to track them manually. - ***`isEventSampled?: (action: string, sessionRandom: number) => boolean | Promise`*** (default: `() => true`)
By default, all events are uploaded. Define this function to sample events on the client side. Return `true` if an event should be uploaded. Return false to skip uploading the event. +## `GoogleAnalyticsConfig` + +```ts + +``` + +### Properties + +- ***``***
Used for the `measurement_id` query parameter. + +- ***``***
Used for the `api_secret` query parameter. + +- ***``***
Return value used for the `user_id` field in the request body. + +- ***``***
Return value used for the `client_id` field in the request body. + +- ***``***
Set to true to enable debug mode - requests will go to the `/debug/mp/collect` endpoint instead of the regular `/mp/collect` endpoint. + +- ***``***
Used for `non_personalized_ads` in the request body. + ## `TrackBaseOptions` ```ts interface TrackBaseOptions { - userId: string | undefined; context: string | undefined; screen: string | undefined; sessionId: number | undefined; @@ -283,13 +171,13 @@ interface TrackBaseOptions { browser: string | undefined; browserVersion: string | undefined; language: string | undefined; + referrer: string | undefined; + title: string | undefined; } ``` ### Properties -- ***`userId: string | undefined`***
ID of the user reporting the event. - - ***`context: string | undefined`***
JS context the event was reported from. - ***`screen: string | undefined`***
Screen resolution. @@ -308,6 +196,10 @@ interface TrackBaseOptions { - ***`language: string | undefined`***
Language returned from [`browser.i18n.getUILanguage`](https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/API/I18n/getUILanguage). +- ***`referrer: string | undefined`***
From `document.referrer` if available. + +- ***`title: string | undefined`***
From `document.title` if available. + ## `TrackEventOptions` ```ts @@ -335,6 +227,20 @@ interface TrackPageViewOptions extends TrackBaseOptions { - ***`page?: string`*** +## `UmamiConfig` + +```ts + +``` + +Used to pass config into `defineUmamiClient`. + +### Properties + +- ***``***
See [Umami's documentation for more details](https://umami.is/docs/collect-data). + +- ***``***
URL to your Umami instance (`https://stats.aklinker1.io`, `https://analytics.umami.is/share/LGazGOecbDtaIwDr/umami.is`, etc). Include the path up until the `/api` +

--- diff --git a/packages/analytics/package.json b/packages/analytics/package.json index 1a2b02b..b012524 100644 --- a/packages/analytics/package.json +++ b/packages/analytics/package.json @@ -53,7 +53,6 @@ }, "dependencies": { "@webext-core/proxy-service": "workspace:*", - "@webext-core/storage": "workspace:*", "ua-parser-js": "^1.0.35", "webextension-polyfill": "^0.10.0" }, diff --git a/packages/analytics/src/clients/createGoogleAnalyticsClient.ts b/packages/analytics/src/clients/createGoogleAnalyticsClient.ts new file mode 100644 index 0000000..e7ad28f --- /dev/null +++ b/packages/analytics/src/clients/createGoogleAnalyticsClient.ts @@ -0,0 +1,97 @@ +import { ExtensionAnalyticsClient } from './types'; + +/** + * Returns a client for reporting analytics to Google Analytics 4 through the + * [Measurement Protocol](https://developers.google.com/analytics/devguides/collection/protocol/ga4). + * + * It is worth noting that the measurment protocol restricts the reporting of some events, user + * properties, and event parameters. [See the docs](https://developers.google.com/analytics/devguides/collection/protocol/ga4/reference?client_type=firebase#reserved_names) + * for more information. That means that this client WILL NOT provide the same amount of stats as + * your standard web, gtag setup. + * + * The client will: + * + * - Upload a single event per network request + * - Send the `context` and `page` as event parameterss + * - Does not upload anything for `trackPageView` - the `page_view` event is one of the restricted events for the MP API + */ +export function createGoogleAnalyticsClient( + config: GoogleAnalyticsConfig, +): ExtensionAnalyticsClient { + const prodUrl = 'https://www.google-analytics.com/mp/collect'; + const debugUrl = 'https://www.google-analytics.com/debug/mp/collect'; + + return { + async uploadEvent(options) { + const url = new URL(config.debug ? debugUrl : prodUrl); + url.searchParams.set('measurement_id', config.measurementId); + url.searchParams.set('api_secret', config.apiSecret); + const body: RequestBody = { + client_id: await config.getClientId(), + user_id: await config.getUserId?.(), + non_personalized_ads: config.nonPersonalizedAds, + timestamp_micros: options.timestamp * 1000, + events: [ + { + name: options.action, + params: { + page: options.page, + context: options.context, + engagement_time_msec: options.sessionId + ? String(Date.now() - options.sessionId) + : undefined, + session_id: options.sessionId ? String(options.sessionId) : undefined, + }, + }, + ], + }; + await fetch(url.href, { + method: 'POST', + body: JSON.stringify(body), + }); + }, + }; +} + +export interface GoogleAnalyticsConfig { + /** + * Used for the `measurement_id` query parameter. + */ + measurementId: string; + /** + * Used for the `api_secret` query parameter. + */ + apiSecret: string; + /** + * Return value used for the `user_id` field in the request body. + */ + getUserId?: () => string | Promise; + /** + * Return value used for the `client_id` field in the request body. + */ + getClientId: () => string | Promise; + /** + * Set to true to enable debug mode - requests will go to the `/debug/mp/collect` endpoint instead of the regular `/mp/collect` endpoint. + */ + debug?: boolean; + /** + * Used for `non_personalized_ads` in the request body. + */ + nonPersonalizedAds?: boolean; +} + +interface RequestBody { + client_id: string; + user_id?: string; + timestamp_micros?: number; + user_properties?: Record; + non_personalized_ads?: boolean; + events: Array<{ + name: string; + params: { + engagement_time_msec?: string; + session_id?: string; + [param: string]: any; + }; + }>; +} diff --git a/packages/analytics/src/clients/createUmamiClient.ts b/packages/analytics/src/clients/createUmamiClient.ts new file mode 100644 index 0000000..7393836 --- /dev/null +++ b/packages/analytics/src/clients/createUmamiClient.ts @@ -0,0 +1,79 @@ +import { ExtensionAnalyticsClient } from './types'; + +/** + * Umami is a privacy focused alternative to google analytics. + * + * > https://umami.is/ + * + * The Umami client returned by this function: + * + * - Uploads a single event at a time + * - Sends the `context` string as the `hostname` parameter + * - Does not upload anything for `trackPageView` - pages are apart of events. + */ +export function createUmamiClient(config: UmamiConfig): ExtensionAnalyticsClient { + const baseUrl = config.url.endsWith('/') + ? config.url.substring(config.url.length - 1) + : config.url; + const sendUrl = `${baseUrl}/api/send`; + + return { + async uploadEvent(options) { + const body: RequestBody = { + type: 'event', + payload: { + name: options.action, + hostname: options.context ?? '', + language: options.language ?? '', + referrer: options.referrer ?? '', + screen: options.screen ?? '', + title: options.title ?? '', + url: options.page ?? '/', + website: config.websiteId, + data: options.properties, + }, + }; + + await fetch(sendUrl, { + method: 'POST', + body: JSON.stringify(body), + headers: { + 'Content-Type': 'application/json', + 'User-Agent': navigator.userAgent, + }, + }); + }, + }; +} + +/** + * Used to pass config into `defineUmamiClient`. + */ +export interface UmamiConfig { + /** + * See [Umami's documentation for more details](https://umami.is/docs/collect-data). + */ + websiteId: string; + /** + * URL to your Umami instance (`https://stats.aklinker1.io`, `https://analytics.umami.is/share/LGazGOecbDtaIwDr/umami.is`, etc). Include the path up until the `/api` + */ + url: string; +} + +/** + * See https://umami.is/docs/sending-stats + */ +interface RequestBody { + type: 'event'; + payload: { + hostname: string; + language: string; + referrer: string; + screen: string; + title: string; + url: string; + website: string; + name: string; + data?: Record; + }; +} diff --git a/packages/analytics/src/clients/index.ts b/packages/analytics/src/clients/index.ts index 772f0bc..7d64841 100644 --- a/packages/analytics/src/clients/index.ts +++ b/packages/analytics/src/clients/index.ts @@ -1,3 +1,3 @@ export * from './types'; -export * from './defineGoogleAnalyticsClient'; -export * from './defineUmamiClient'; +export * from './createGoogleAnalyticsClient'; +export * from './createUmamiClient'; diff --git a/packages/analytics/src/clients/types.ts b/packages/analytics/src/clients/types.ts index 3e16000..e048063 100644 --- a/packages/analytics/src/clients/types.ts +++ b/packages/analytics/src/clients/types.ts @@ -16,10 +16,6 @@ export interface ExtensionAnalyticsClient { } export interface TrackBaseOptions { - /** - * ID of the user reporting the event. - */ - userId: string | undefined; /** * JS context the event was reported from. */ @@ -56,6 +52,14 @@ export interface TrackBaseOptions { * Language returned from [`browser.i18n.getUILanguage`](https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/API/I18n/getUILanguage). */ language: string | undefined; + /** + * From `document.referrer` if available. + */ + referrer: string | undefined; + /** + * From `document.title` if available. + */ + title: string | undefined; } export interface TrackPageViewOptions extends TrackBaseOptions { diff --git a/packages/analytics/src/types.ts b/packages/analytics/src/types.ts index 4ae608d..4ad9a2c 100644 --- a/packages/analytics/src/types.ts +++ b/packages/analytics/src/types.ts @@ -30,12 +30,6 @@ export interface ExtensionAnalyticsConfig { * collecting their data. Return `false` if they have opted-out, and `true` if they have opted-in. */ isEnabled: () => boolean | Promise; - /** - * Returns the user id that may be reported with events and page views. If this method is not - * provided, a random UUID will be generated and stored in storage. If the storage permission is - * missing, the user ID will not be included in any reports. - */ - getUserId?: () => string | Promise; /** * By default, the analytics application will track some standard events: `extension_installed`, * `extension_updated`, etc. By setting this to `false`, these events will not be tracked diff --git a/packages/analytics/src/utils/createAnalytics.ts b/packages/analytics/src/utils/createAnalytics.ts index 614eb2b..4428084 100644 --- a/packages/analytics/src/utils/createAnalytics.ts +++ b/packages/analytics/src/utils/createAnalytics.ts @@ -1,4 +1,3 @@ -import { localExtStorage } from '@webext-core/storage'; import browser from 'webextension-polyfill'; import { ExtensionAnalytics, ExtensionAnalyticsConfig } from '../types'; import { TrackBaseOptions } from '../clients'; @@ -20,14 +19,6 @@ export function createExtensionAnalytics(config: ExtensionAnalyticsConfig): Exte const ua = UAParser(navigator.userAgent); - const getUserId = async (): Promise => { - if (config.getUserId) return await config.getUserId(); - if (browser.storage == null) return undefined; - - const userId: string | null = await localExtStorage.getItem('@webext-core/analytics/userId'); - return userId ?? undefined; - }; - const getBaseOptions = async (): Promise> => { return { browser: ua.browser.name, @@ -39,7 +30,8 @@ export function createExtensionAnalytics(config: ExtensionAnalyticsConfig): Exte ? `${globalThis.screen.width}x${globalThis.screen.height}` : undefined, sessionId, - userId: await getUserId(), + referrer: globalThis.document?.referrer, + title: globalThis.document?.title, }; }; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index db58701..9fe195a 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -44,7 +44,6 @@ importers: '@types/ua-parser-js': ^0.7.36 '@vitest/coverage-c8': ^0.24.5 '@webext-core/proxy-service': workspace:* - '@webext-core/storage': workspace:* jsdom: ^20.0.3 tsconfig: workspace:* tsup: ^6.4.0 @@ -54,7 +53,6 @@ importers: webextension-polyfill: ^0.10.0 dependencies: '@webext-core/proxy-service': link:../proxy-service - '@webext-core/storage': link:../storage ua-parser-js: 1.0.35 webextension-polyfill: 0.10.0 devDependencies: From a40571485d59cc97e8892d946d6860623f0492c7 Mon Sep 17 00:00:00 2001 From: Aaron Klinker Date: Sat, 27 May 2023 19:42:04 -0500 Subject: [PATCH 4/7] Cleanup documentation --- docs/api/analytics.md | 33 +++++++--- .../clients/createGoogleAnalyticsClient.ts | 62 +++++++++++-------- .../src/clients/createUmamiClient.ts | 29 ++++----- 3 files changed, 73 insertions(+), 51 deletions(-) diff --git a/docs/api/analytics.md b/docs/api/analytics.md index 403d745..241ed29 100644 --- a/docs/api/analytics.md +++ b/docs/api/analytics.md @@ -141,22 +141,31 @@ Return `true` if an event should be uploaded. Return false to skip uploading the ## `GoogleAnalyticsConfig` ```ts - +interface GoogleAnalyticsConfig { + measurementId: string; + apiSecret: string; + getUserId?: () => string | Promise; + getClientId: () => string | Promise; + debug?: boolean; + nonPersonalizedAds?: boolean; +} ``` ### Properties -- ***``***
Used for the `measurement_id` query parameter. +- ***`measurementId: string`***
Used for the [`measurement_id` query parameter](https://developers.google.com/analytics/devguides/collection/protocol/ga4/sending-events?client_type=gtag#required_parameters). -- ***``***
Used for the `api_secret` query parameter. +- ***`apiSecret: string`***
Used for the [`api_secret` query parameter](https://developers.google.com/analytics/devguides/collection/protocol/ga4/sending-events?client_type=gtag#required_parameters). -- ***``***
Return value used for the `user_id` field in the request body. +- ***`getUserId?: () => string | Promise`***
Return value used for the [`user_id` field in the request body](https://developers.google.com/analytics/devguides/collection/protocol/ga4/reference?client_type=gtag#payload_post_body). -- ***``***
Return value used for the `client_id` field in the request body. +- ***`getClientId: () => string | Promise`***
Return value used for the [`client_id` field in the request body](https://developers.google.com/analytics/devguides/collection/protocol/ga4/reference?client_type=gtag#payload_post_body). -- ***``***
Set to true to enable debug mode - requests will go to the `/debug/mp/collect` endpoint instead of the regular `/mp/collect` endpoint. +- ***`debug?: boolean`***
Set to `true` to enable debug mode. When `true`, requests will go to the +[`/debug/mp/collect` endpoint](https://developers.google.com/analytics/devguides/collection/protocol/ga4/validating-events?client_type=gtag#sending_events_for_validation) +instead of the regular [`/mp/collect` endpoint](https://developers.google.com/analytics/devguides/collection/protocol/ga4/reference?client_type=gtag#url_endpoint). -- ***``***
Used for `non_personalized_ads` in the request body. +- ***`nonPersonalizedAds?: boolean`***
Used for the [`non_personalized_ads` field](https://developers.google.com/analytics/devguides/collection/protocol/ga4/reference?client_type=gtag#payload_post_body) in the request body. ## `TrackBaseOptions` @@ -230,16 +239,20 @@ interface TrackPageViewOptions extends TrackBaseOptions { ## `UmamiConfig` ```ts - +interface UmamiConfig { + websiteId: string; + url: string; +} ``` Used to pass config into `defineUmamiClient`. ### Properties -- ***``***
See [Umami's documentation for more details](https://umami.is/docs/collect-data). +- ***`websiteId: string`***
See [Umami's documentation for more details](https://umami.is/docs/collect-data). -- ***``***
URL to your Umami instance (`https://stats.aklinker1.io`, `https://analytics.umami.is/share/LGazGOecbDtaIwDr/umami.is`, etc). Include the path up until the `/api` +- ***`url: string`***
URL to your Umami instance (`https://stats.aklinker1.io`, +`https://analytics.umami.is/share/LGazGOecbDtaIwDr/umami.is`, etc).

diff --git a/packages/analytics/src/clients/createGoogleAnalyticsClient.ts b/packages/analytics/src/clients/createGoogleAnalyticsClient.ts index e7ad28f..8eafd4e 100644 --- a/packages/analytics/src/clients/createGoogleAnalyticsClient.ts +++ b/packages/analytics/src/clients/createGoogleAnalyticsClient.ts @@ -1,5 +1,34 @@ import { ExtensionAnalyticsClient } from './types'; +export interface GoogleAnalyticsConfig { + /** + * Used for the [`measurement_id` query parameter](https://developers.google.com/analytics/devguides/collection/protocol/ga4/sending-events?client_type=gtag#required_parameters). + */ + measurementId: string; + /** + * Used for the [`api_secret` query parameter](https://developers.google.com/analytics/devguides/collection/protocol/ga4/sending-events?client_type=gtag#required_parameters). + */ + apiSecret: string; + /** + * Return value used for the [`user_id` field in the request body](https://developers.google.com/analytics/devguides/collection/protocol/ga4/reference?client_type=gtag#payload_post_body). + */ + getUserId?: () => string | Promise; + /** + * Return value used for the [`client_id` field in the request body](https://developers.google.com/analytics/devguides/collection/protocol/ga4/reference?client_type=gtag#payload_post_body). + */ + getClientId: () => string | Promise; + /** + * Set to `true` to enable debug mode. When `true`, requests will go to the + * [`/debug/mp/collect` endpoint](https://developers.google.com/analytics/devguides/collection/protocol/ga4/validating-events?client_type=gtag#sending_events_for_validation) + * instead of the regular [`/mp/collect` endpoint](https://developers.google.com/analytics/devguides/collection/protocol/ga4/reference?client_type=gtag#url_endpoint). + */ + debug?: boolean; + /** + * Used for the [`non_personalized_ads` field](https://developers.google.com/analytics/devguides/collection/protocol/ga4/reference?client_type=gtag#payload_post_body) in the request body. + */ + nonPersonalizedAds?: boolean; +} + /** * Returns a client for reporting analytics to Google Analytics 4 through the * [Measurement Protocol](https://developers.google.com/analytics/devguides/collection/protocol/ga4). @@ -53,33 +82,6 @@ export function createGoogleAnalyticsClient( }; } -export interface GoogleAnalyticsConfig { - /** - * Used for the `measurement_id` query parameter. - */ - measurementId: string; - /** - * Used for the `api_secret` query parameter. - */ - apiSecret: string; - /** - * Return value used for the `user_id` field in the request body. - */ - getUserId?: () => string | Promise; - /** - * Return value used for the `client_id` field in the request body. - */ - getClientId: () => string | Promise; - /** - * Set to true to enable debug mode - requests will go to the `/debug/mp/collect` endpoint instead of the regular `/mp/collect` endpoint. - */ - debug?: boolean; - /** - * Used for `non_personalized_ads` in the request body. - */ - nonPersonalizedAds?: boolean; -} - interface RequestBody { client_id: string; user_id?: string; @@ -89,7 +91,13 @@ interface RequestBody { events: Array<{ name: string; params: { + /** + * See [Recommended Parameters for Reports](https://developers.google.com/analytics/devguides/collection/protocol/ga4/sending-events?client_type=gtag#recommended_parameters_for_reports). + */ engagement_time_msec?: string; + /** + * See [Recommended Parameters for Reports](https://developers.google.com/analytics/devguides/collection/protocol/ga4/sending-events?client_type=gtag#recommended_parameters_for_reports). + */ session_id?: string; [param: string]: any; }; diff --git a/packages/analytics/src/clients/createUmamiClient.ts b/packages/analytics/src/clients/createUmamiClient.ts index 7393836..123badb 100644 --- a/packages/analytics/src/clients/createUmamiClient.ts +++ b/packages/analytics/src/clients/createUmamiClient.ts @@ -1,5 +1,20 @@ import { ExtensionAnalyticsClient } from './types'; +/** + * Used to pass config into `defineUmamiClient`. + */ +export interface UmamiConfig { + /** + * See [Umami's documentation for more details](https://umami.is/docs/collect-data). + */ + websiteId: string; + /** + * URL to your Umami instance (`https://stats.aklinker1.io`, + * `https://analytics.umami.is/share/LGazGOecbDtaIwDr/umami.is`, etc). + */ + url: string; +} + /** * Umami is a privacy focused alternative to google analytics. * @@ -46,20 +61,6 @@ export function createUmamiClient(config: UmamiConfig): ExtensionAnalyticsClient }; } -/** - * Used to pass config into `defineUmamiClient`. - */ -export interface UmamiConfig { - /** - * See [Umami's documentation for more details](https://umami.is/docs/collect-data). - */ - websiteId: string; - /** - * URL to your Umami instance (`https://stats.aklinker1.io`, `https://analytics.umami.is/share/LGazGOecbDtaIwDr/umami.is`, etc). Include the path up until the `/api` - */ - url: string; -} - /** * See https://umami.is/docs/sending-stats */ From 7e1a7c9303f0d60dee1932b61b0b3afbab113707 Mon Sep 17 00:00:00 2001 From: Aaron Klinker Date: Sat, 27 May 2023 20:07:26 -0500 Subject: [PATCH 5/7] Add client examples --- docs/api/analytics.md | 57 +++++++++++++------ ...ent.ts => createGoogleAnalytics4Client.ts} | 35 ++++++++---- .../src/clients/createUmamiClient.ts | 18 +++--- packages/analytics/src/clients/index.ts | 2 +- 4 files changed, 77 insertions(+), 35 deletions(-) rename packages/analytics/src/clients/{createGoogleAnalyticsClient.ts => createGoogleAnalytics4Client.ts} (80%) diff --git a/docs/api/analytics.md b/docs/api/analytics.md index 241ed29..bdb0f1a 100644 --- a/docs/api/analytics.md +++ b/docs/api/analytics.md @@ -4,29 +4,45 @@ > [`@webext-core/analytics`](/guide/analytics/) -## `createGoogleAnalyticsClient` +## `createGoogleAnalytics4Client` ```ts -function createGoogleAnalyticsClient( +function createGoogleAnalytics4Client( config: GoogleAnalyticsConfig ): ExtensionAnalyticsClient { // ... } ``` -Returns a client for reporting analytics to Google Analytics 4 through the +Creates an `ExtensionAnalyticsClient` for Google Analytics 4 using the [Measurement Protocol](https://developers.google.com/analytics/devguides/collection/protocol/ga4). -It is worth noting that the measurment protocol restricts the reporting of some events, user -properties, and event parameters. [See the docs](https://developers.google.com/analytics/devguides/collection/protocol/ga4/reference?client_type=firebase#reserved_names) -for more information. That means that this client WILL NOT provide the same amount of stats as -your standard web, gtag setup. +### Examples + +```ts +import { createGoogleAnalytics4Client } from '@webext-core/analytics'; + +function generateRandomId(): string { + // ... +} -The client will: +async function getOrInit(key: string, init: () => string): Promise { + const value = localExtStorage.getItem(key); + if (value != null) return value; -- Upload a single event per network request -- Send the `context` and `page` as event parameterss -- Does not upload anything for `trackPageView` - the `page_view` event is one of the restricted events for the MP API + const newValue = init(); + await localExtStorage.setItem(key, newValue); + return newValue; +} + +const googleAnalytics = createGoogleAnalytics4Client({ + measurementId: "...", + apiSecret: "...", + nonPersonalizedAds: true, + getUserId: () => getOrInit("googleAnalytics/userId", generateRandomId), + getClientId: () => getOrInit("googleAnalytics/clientId", generateRandomId), +}) +``` ## `createUmamiClient` @@ -36,15 +52,22 @@ function createUmamiClient(config: UmamiConfig): ExtensionAnalyticsClient { } ``` -Umami is a privacy focused alternative to google analytics. +Creates an `ExtensionAnalyticsClient` for . -> https://umami.is/ +### Examples -The Umami client returned by this function: +```ts +import { createUmamiClient } from '@webext-core/analytics'; -- Uploads a single event at a time -- Sends the `context` string as the `hostname` parameter -- Does not upload anything for `trackPageView` - pages are apart of events. +const umami = createUmamiClient({ + websiteId: "...", + sendUrl: "https://stats.aklinker1.io" +}); +export const [registerAnalytics, getAnalytics] = defineExtensionAnalytics({ + client: umami, + ... +}) +``` ## `defineExtensionAnalytics` diff --git a/packages/analytics/src/clients/createGoogleAnalyticsClient.ts b/packages/analytics/src/clients/createGoogleAnalytics4Client.ts similarity index 80% rename from packages/analytics/src/clients/createGoogleAnalyticsClient.ts rename to packages/analytics/src/clients/createGoogleAnalytics4Client.ts index 8eafd4e..1457570 100644 --- a/packages/analytics/src/clients/createGoogleAnalyticsClient.ts +++ b/packages/analytics/src/clients/createGoogleAnalytics4Client.ts @@ -30,21 +30,34 @@ export interface GoogleAnalyticsConfig { } /** - * Returns a client for reporting analytics to Google Analytics 4 through the + * Creates an `ExtensionAnalyticsClient` for Google Analytics 4 using the * [Measurement Protocol](https://developers.google.com/analytics/devguides/collection/protocol/ga4). * - * It is worth noting that the measurment protocol restricts the reporting of some events, user - * properties, and event parameters. [See the docs](https://developers.google.com/analytics/devguides/collection/protocol/ga4/reference?client_type=firebase#reserved_names) - * for more information. That means that this client WILL NOT provide the same amount of stats as - * your standard web, gtag setup. + * @example + * import { createGoogleAnalytics4Client } from '@webext-core/analytics'; * - * The client will: + * function generateRandomId(): string { + * // ... + * } * - * - Upload a single event per network request - * - Send the `context` and `page` as event parameterss - * - Does not upload anything for `trackPageView` - the `page_view` event is one of the restricted events for the MP API + * async function getOrInit(key: string, init: () => string): Promise { + * const value = localExtStorage.getItem(key); + * if (value != null) return value; + * + * const newValue = init(); + * await localExtStorage.setItem(key, newValue); + * return newValue; + * } + * + * const googleAnalytics = createGoogleAnalytics4Client({ + * measurementId: "...", + * apiSecret: "...", + * nonPersonalizedAds: true, + * getUserId: () => getOrInit("googleAnalytics/userId", generateRandomId), + * getClientId: () => getOrInit("googleAnalytics/clientId", generateRandomId), + * }) */ -export function createGoogleAnalyticsClient( +export function createGoogleAnalytics4Client( config: GoogleAnalyticsConfig, ): ExtensionAnalyticsClient { const prodUrl = 'https://www.google-analytics.com/mp/collect'; @@ -55,6 +68,8 @@ export function createGoogleAnalyticsClient( const url = new URL(config.debug ? debugUrl : prodUrl); url.searchParams.set('measurement_id', config.measurementId); url.searchParams.set('api_secret', config.apiSecret); + + // TODO: figure out how to report the os/browser info and language const body: RequestBody = { client_id: await config.getClientId(), user_id: await config.getUserId?.(), diff --git a/packages/analytics/src/clients/createUmamiClient.ts b/packages/analytics/src/clients/createUmamiClient.ts index 123badb..0ffb303 100644 --- a/packages/analytics/src/clients/createUmamiClient.ts +++ b/packages/analytics/src/clients/createUmamiClient.ts @@ -16,15 +16,19 @@ export interface UmamiConfig { } /** - * Umami is a privacy focused alternative to google analytics. + * Creates an `ExtensionAnalyticsClient` for . * - * > https://umami.is/ + * @example + * import { createUmamiClient } from '@webext-core/analytics'; * - * The Umami client returned by this function: - * - * - Uploads a single event at a time - * - Sends the `context` string as the `hostname` parameter - * - Does not upload anything for `trackPageView` - pages are apart of events. + * const umami = createUmamiClient({ + * websiteId: "...", + * sendUrl: "https://stats.aklinker1.io" + * }); + * export const [registerAnalytics, getAnalytics] = defineExtensionAnalytics({ + * client: umami, + * ... + * }) */ export function createUmamiClient(config: UmamiConfig): ExtensionAnalyticsClient { const baseUrl = config.url.endsWith('/') diff --git a/packages/analytics/src/clients/index.ts b/packages/analytics/src/clients/index.ts index 7d64841..2907306 100644 --- a/packages/analytics/src/clients/index.ts +++ b/packages/analytics/src/clients/index.ts @@ -1,3 +1,3 @@ export * from './types'; -export * from './createGoogleAnalyticsClient'; +export * from './createGoogleAnalytics4Client'; export * from './createUmamiClient'; From f0432fb4dd183c5df69fbb736f9ba6b4d53396c5 Mon Sep 17 00:00:00 2001 From: Aaron Klinker Date: Fri, 14 Jul 2023 09:48:49 -0500 Subject: [PATCH 6/7] WIP --- docs/.vitepress/config.ts | 10 ++--- docs/guide/analytics/custom.md | 7 +++ docs/guide/analytics/google-analytics-4.md | 1 + docs/guide/analytics/index.md | 42 ++++++++++++++++++ docs/guide/analytics/umami.md | 1 + packages/analytics-demo/public/128.png | Bin 0 -> 12669 bytes packages/analytics-demo/public/16.png | Bin 0 -> 698 bytes packages/analytics-demo/public/32.png | Bin 0 -> 1630 bytes packages/analytics-demo/public/48.png | Bin 0 -> 2941 bytes packages/analytics-demo/public/96.png | Bin 0 -> 8080 bytes .../analytics-demo/src/components/Page.vue | 16 +++++++ packages/analytics-demo/src/manifest.json | 16 +++++++ .../analytics-demo/src/utils/analytics.ts | 10 +++++ .../analytics-demo/src/utils/popup-router.ts | 19 ++++++++ .../analytics/src/defineExtensionAnalytics.ts | 1 + packages/analytics/src/types.ts | 3 +- .../analytics/src/utils/createAnalytics.ts | 41 +++++++++-------- 17 files changed, 143 insertions(+), 24 deletions(-) create mode 100644 docs/guide/analytics/custom.md create mode 100644 docs/guide/analytics/google-analytics-4.md create mode 100644 docs/guide/analytics/index.md create mode 100644 docs/guide/analytics/umami.md create mode 100644 packages/analytics-demo/public/128.png create mode 100644 packages/analytics-demo/public/16.png create mode 100644 packages/analytics-demo/public/32.png create mode 100644 packages/analytics-demo/public/48.png create mode 100644 packages/analytics-demo/public/96.png create mode 100644 packages/analytics-demo/src/components/Page.vue create mode 100644 packages/analytics-demo/src/manifest.json create mode 100644 packages/analytics-demo/src/utils/analytics.ts create mode 100644 packages/analytics-demo/src/utils/popup-router.ts diff --git a/docs/.vitepress/config.ts b/docs/.vitepress/config.ts index c610e44..4a8ebff 100644 --- a/docs/.vitepress/config.ts +++ b/docs/.vitepress/config.ts @@ -92,16 +92,16 @@ const packagePages = { text: 'Get Started', link: '/guide/analytics/', }, - { - text: 'Google Analytics', - link: '/guide/analytics/google-analytics', - }, { text: 'Umami', link: '/guide/analytics/umami', }, { - text: 'Custom Analytics Service', + text: 'Google Analytics 4', + link: '/guide/analytics/google-analytics-4', + }, + { + text: 'Custom Service', link: '/guide/analytics/custom', }, ], diff --git a/docs/guide/analytics/custom.md b/docs/guide/analytics/custom.md new file mode 100644 index 0000000..e9f2c8a --- /dev/null +++ b/docs/guide/analytics/custom.md @@ -0,0 +1,7 @@ +--- +next: + text: API Reference + link: /api/analytics +--- + +# Custom Service diff --git a/docs/guide/analytics/google-analytics-4.md b/docs/guide/analytics/google-analytics-4.md new file mode 100644 index 0000000..2400a27 --- /dev/null +++ b/docs/guide/analytics/google-analytics-4.md @@ -0,0 +1 @@ +# Google Analytics 4 diff --git a/docs/guide/analytics/index.md b/docs/guide/analytics/index.md new file mode 100644 index 0000000..2c4bf10 --- /dev/null +++ b/docs/guide/analytics/index.md @@ -0,0 +1,42 @@ +# Analytics + + + + + + + + + +## Overview + +`@webext-core/analytics` provides basic analytics (event reporting and page views) for web extensions! Report events from anywhere: your background script/service worker, content scripts, or HTML pages. + +Comes with built-in support for [Umami](/guide/analytics/umami) and [Google Analytics 4](/guide/analytics/google-analytics-4). + +## Installation + +###### NPM + +```sh +pnpm i @webext-core/analytics +``` + +```ts +import { ... } from '@webext-core/analytics'; +``` + +###### CDN + +```sh +curl -o analytics.js https://cdn.jsdelivr.net/npm/@webext-core/analytics/lib/index.global.js +``` + +```html + + +``` + +## Usage diff --git a/docs/guide/analytics/umami.md b/docs/guide/analytics/umami.md new file mode 100644 index 0000000..4a96d80 --- /dev/null +++ b/docs/guide/analytics/umami.md @@ -0,0 +1 @@ +# Umami diff --git a/packages/analytics-demo/public/128.png b/packages/analytics-demo/public/128.png new file mode 100644 index 0000000000000000000000000000000000000000..53e9353acf1060645a8d1569fc33a0e93d172135 GIT binary patch literal 12669 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 literal 0 HcmV?d00001 diff --git a/packages/analytics-demo/public/16.png b/packages/analytics-demo/public/16.png new file mode 100644 index 0000000000000000000000000000000000000000..3fe36745faf20616d43a67ac083bd5889f7f5ae7 GIT binary patch literal 698 zcmV;r0!96aP)+@@Pv*Ibkp{}o53mKT zHT7W&+!6t@mI`EyL3m0Sbd-+N6Y32q@wVf_^l?gG1rD-G)&n>kFr*R_Qw|KvWk84s z5PP+G9HYipwD<^`7)eS90cJ;2!c0Q|xXyq_7ct;!`UDzc6tnHSSn2ivMqEP03Rv9^ z)VB|8F+M8@k03nuAi!cPht}#CY>ss98Re2DYMU!iMi^A%4$>Eo?8B;hCG9LF8T=OG zxZZe(@2PR(DHA-EHmxEn+-N?gsq8^1F)gJ*KLX8-^I literal 0 HcmV?d00001 diff --git a/packages/analytics-demo/public/32.png b/packages/analytics-demo/public/32.png new file mode 100644 index 0000000000000000000000000000000000000000..d1063e5b1bcd21f825d5e24f5b984a22cebb45b3 GIT binary patch literal 1630 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 e_&In3w*Lhs_|9Q$&b4v?0000 +import { useRoute } from 'vue-router'; + +const route = useRoute(); + + + diff --git a/packages/analytics-demo/src/manifest.json b/packages/analytics-demo/src/manifest.json new file mode 100644 index 0000000..4ae9c83 --- /dev/null +++ b/packages/analytics-demo/src/manifest.json @@ -0,0 +1,16 @@ +{ + "manifest_version": 3, + "icons": { + "16": "16.png", + "32": "32.png", + "48": "48.png", + "96": "96.png", + "128": "128.png" + }, + "action": { + "default_popup": "src/popup.html" + }, + "background": { + "service_worker": "src/background.ts" + } +} diff --git a/packages/analytics-demo/src/utils/analytics.ts b/packages/analytics-demo/src/utils/analytics.ts new file mode 100644 index 0000000..a475b6e --- /dev/null +++ b/packages/analytics-demo/src/utils/analytics.ts @@ -0,0 +1,10 @@ +import { defineExtensionAnalytics, createUmamiClient } from '@webext-core/analytics'; + +export const [registerAnalytics, getAnalytics] = defineExtensionAnalytics({ + logger: console, + client: createUmamiClient({ + url: 'https://stats.aklinker1.io', + websiteId: 'd44e128b-9d3e-4e5b-a340-f0442fcf1d61', + }), + isEnabled: () => true, +}); diff --git a/packages/analytics-demo/src/utils/popup-router.ts b/packages/analytics-demo/src/utils/popup-router.ts new file mode 100644 index 0000000..2a1ca15 --- /dev/null +++ b/packages/analytics-demo/src/utils/popup-router.ts @@ -0,0 +1,19 @@ +import { createRouter, createWebHashHistory } from 'vue-router'; + +export const popupRouter = createRouter({ + history: createWebHashHistory(), + routes: [ + { + path: '/', + component: () => import('../components/Page.vue'), + }, + { + path: '/page-2', + component: () => import('../components/Page.vue'), + }, + { + path: '/page-3', + component: () => import('../components/Page.vue'), + }, + ], +}); diff --git a/packages/analytics/src/defineExtensionAnalytics.ts b/packages/analytics/src/defineExtensionAnalytics.ts index e8aaf20..7641aa8 100644 --- a/packages/analytics/src/defineExtensionAnalytics.ts +++ b/packages/analytics/src/defineExtensionAnalytics.ts @@ -19,6 +19,7 @@ export function defineExtensionAnalytics(config: ExtensionAnalyticsConfig) { const [registerClient, getClient] = defineProxyService( '@webext-core/analytics-client', (client: ExtensionAnalyticsClient) => client, + config, ); let singletonAnalytics: ExtensionAnalytics | undefined; diff --git a/packages/analytics/src/types.ts b/packages/analytics/src/types.ts index 4ad9a2c..a415fc9 100644 --- a/packages/analytics/src/types.ts +++ b/packages/analytics/src/types.ts @@ -1,4 +1,5 @@ import { ExtensionAnalyticsClient } from './clients'; +import { ProxyServiceConfig } from '@webext-core/proxy-service'; export interface ExtensionAnalytics { /** @@ -19,7 +20,7 @@ export interface ExtensionAnalytics { trackPageView: (pathname: string) => void; } -export interface ExtensionAnalyticsConfig { +export interface ExtensionAnalyticsConfig extends ProxyServiceConfig { /** * The client to report analytics to. Use `defineUmamiClient`, `defineGoogleAnalyticsClient`, or * implement your own `ExtensionAnalyticsClient`. diff --git a/packages/analytics/src/utils/createAnalytics.ts b/packages/analytics/src/utils/createAnalytics.ts index 4428084..25a234b 100644 --- a/packages/analytics/src/utils/createAnalytics.ts +++ b/packages/analytics/src/utils/createAnalytics.ts @@ -1,10 +1,11 @@ import browser from 'webextension-polyfill'; import { ExtensionAnalytics, ExtensionAnalyticsConfig } from '../types'; -import { TrackBaseOptions } from '../clients'; +import { TrackBaseOptions, TrackEventOptions, TrackPageViewOptions } from '../clients'; import UAParser from 'ua-parser-js'; export function createExtensionAnalytics(config: ExtensionAnalyticsConfig): ExtensionAnalytics { const { client } = config; + const logger = config.logger === null ? null : config.logger ?? console; let currentContext: string | undefined; let currentPage: string | undefined; const sessionId = Date.now(); @@ -14,7 +15,7 @@ export function createExtensionAnalytics(config: ExtensionAnalyticsConfig): Exte const sampled = await (isSampled ?? (() => true))(); if (!sampled) return false; - return await isEnabled(); + return await config.isEnabled(); }; const ua = UAParser(navigator.userAgent); @@ -49,16 +50,18 @@ export function createExtensionAnalytics(config: ExtensionAnalyticsConfig): Exte if (!enabled) return; const baseOptions = await getBaseOptions(); + const options: TrackEventOptions = { + timestamp, + action, + properties: properties ?? {}, + context, + page, + ...baseOptions, + }; + logger?.log('Reporting event: ' + action, options); await client - .uploadEvent({ - timestamp, - action, - properties: properties ?? {}, - context, - page, - ...baseOptions, - }) + .uploadEvent(options) // ignore network errors .catch(); }; @@ -74,13 +77,15 @@ export function createExtensionAnalytics(config: ExtensionAnalyticsConfig): Exte if (!enabled) return; const baseOptions = await getBaseOptions(); + const options: TrackPageViewOptions = { + timestamp, + context, + page, + ...baseOptions, + }; + logger?.log('Reporting page view: ' + page, options); await client - .uploadPageView?.({ - timestamp, - context, - page, - ...baseOptions, - }) + .uploadPageView?.(options) // ignore network errors .catch(); }; @@ -96,7 +101,7 @@ export function createExtensionAnalytics(config: ExtensionAnalyticsConfig): Exte // Grab variables synchronously const context = currentContext; const page = currentPage; - void trackEventAsync(timestamp, context, page, action, properties); + void trackEventAsync(timestamp, context, page, action, properties).catch(logger?.warn); }, trackPageView(page) { @@ -105,7 +110,7 @@ export function createExtensionAnalytics(config: ExtensionAnalyticsConfig): Exte // Grab variables synchronously const context = currentContext; - void trackPageViewAsync(timestamp, context, page); + void trackPageViewAsync(timestamp, context, page).catch(logger?.warn); }, }; } From b610bfc6e13f5e60dc4dc36a09991e81839901b6 Mon Sep 17 00:00:00 2001 From: Aaron Klinker Date: Fri, 14 Jul 2023 09:48:52 -0500 Subject: [PATCH 7/7] WIP --- packages/analytics-demo/.gitignore | 28 ++ packages/analytics-demo/package.json | 24 ++ packages/analytics-demo/src/background.ts | 6 + .../analytics-demo/src/components/Page.vue | 11 +- packages/analytics-demo/src/popup.html | 13 + packages/analytics-demo/src/popup.ts | 14 + packages/analytics-demo/src/vite-env.d.ts | 1 + packages/analytics-demo/tsconfig.json | 18 ++ packages/analytics-demo/tsconfig.node.json | 9 + packages/analytics-demo/vite.config.ts | 25 ++ .../src/clients/createUmamiClient.ts | 2 +- pnpm-lock.yaml | 306 +++++++++++++----- 12 files changed, 377 insertions(+), 80 deletions(-) create mode 100644 packages/analytics-demo/.gitignore create mode 100644 packages/analytics-demo/package.json create mode 100644 packages/analytics-demo/src/background.ts create mode 100644 packages/analytics-demo/src/popup.html create mode 100644 packages/analytics-demo/src/popup.ts create mode 100644 packages/analytics-demo/src/vite-env.d.ts create mode 100644 packages/analytics-demo/tsconfig.json create mode 100644 packages/analytics-demo/tsconfig.node.json create mode 100644 packages/analytics-demo/vite.config.ts diff --git a/packages/analytics-demo/.gitignore b/packages/analytics-demo/.gitignore new file mode 100644 index 0000000..49053f3 --- /dev/null +++ b/packages/analytics-demo/.gitignore @@ -0,0 +1,28 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* + +node_modules +dist +dist-ssr +*.local + +# Editor directories and files +.vscode/* +!.vscode/extensions.json +.idea +.DS_Store +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? + +# Config files +.webextrc +.webextrc.* diff --git a/packages/analytics-demo/package.json b/packages/analytics-demo/package.json new file mode 100644 index 0000000..c7206e2 --- /dev/null +++ b/packages/analytics-demo/package.json @@ -0,0 +1,24 @@ +{ + "name": "analytics-demo", + "private": true, + "version": "1.0.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "vue-tsc && vite build" + }, + "dependencies": { + "@webext-core/analytics": "workspace:*", + "vue": "^3.2.47", + "vue-router": "^4.2.1" + }, + "devDependencies": { + "@types/webextension-polyfill": "^0.10.0", + "@vitejs/plugin-vue": "^4.0.0", + "typescript": "^4.9.5", + "vite": "^4.1.4", + "vite-plugin-web-extension": "^3.0.1", + "vue-tsc": "^1.2.0", + "webextension-polyfill": "^0.10.0" + } +} diff --git a/packages/analytics-demo/src/background.ts b/packages/analytics-demo/src/background.ts new file mode 100644 index 0000000..277b9df --- /dev/null +++ b/packages/analytics-demo/src/background.ts @@ -0,0 +1,6 @@ +import { registerAnalytics } from './utils/analytics'; + +console.log('background.ts'); + +const analytics = registerAnalytics(); +analytics.init('background'); diff --git a/packages/analytics-demo/src/components/Page.vue b/packages/analytics-demo/src/components/Page.vue index acf7d11..fcc717e 100644 --- a/packages/analytics-demo/src/components/Page.vue +++ b/packages/analytics-demo/src/components/Page.vue @@ -8,9 +8,10 @@ const route = useRoute();

{{
     JSON.stringify({ path: route.path, params: route.params, query: route.query }, null, 2)
   }}
-
    -
  • /
  • -
  • /page-2
  • -
  • /page-3
  • -
+ + / +
+ /page-2 +
+ /page-3 diff --git a/packages/analytics-demo/src/popup.html b/packages/analytics-demo/src/popup.html new file mode 100644 index 0000000..422cf62 --- /dev/null +++ b/packages/analytics-demo/src/popup.html @@ -0,0 +1,13 @@ + + + + + + + Popup + + + + + + diff --git a/packages/analytics-demo/src/popup.ts b/packages/analytics-demo/src/popup.ts new file mode 100644 index 0000000..20b6c2b --- /dev/null +++ b/packages/analytics-demo/src/popup.ts @@ -0,0 +1,14 @@ +import { RouterView } from 'vue-router'; +import { createApp } from 'vue'; +import { popupRouter } from './utils/popup-router'; +import { getAnalytics } from './utils/analytics'; + +createApp(RouterView).use(popupRouter).mount('body'); + +const analytics = getAnalytics(); +analytics.init('popup'); +analytics.trackEvent('opened'); + +popupRouter.beforeEach(to => { + analytics.trackPageView(to.path); +}); diff --git a/packages/analytics-demo/src/vite-env.d.ts b/packages/analytics-demo/src/vite-env.d.ts new file mode 100644 index 0000000..11f02fe --- /dev/null +++ b/packages/analytics-demo/src/vite-env.d.ts @@ -0,0 +1 @@ +/// diff --git a/packages/analytics-demo/tsconfig.json b/packages/analytics-demo/tsconfig.json new file mode 100644 index 0000000..b557c40 --- /dev/null +++ b/packages/analytics-demo/tsconfig.json @@ -0,0 +1,18 @@ +{ + "compilerOptions": { + "target": "ESNext", + "useDefineForClassFields": true, + "module": "ESNext", + "moduleResolution": "Node", + "strict": true, + "jsx": "preserve", + "resolveJsonModule": true, + "isolatedModules": true, + "esModuleInterop": true, + "lib": ["ESNext", "DOM"], + "skipLibCheck": true, + "noEmit": true + }, + "include": ["src/**/*.ts", "src/**/*.d.ts", "src/**/*.tsx", "src/**/*.vue"], + "references": [{ "path": "./tsconfig.node.json" }] +} diff --git a/packages/analytics-demo/tsconfig.node.json b/packages/analytics-demo/tsconfig.node.json new file mode 100644 index 0000000..9d31e2a --- /dev/null +++ b/packages/analytics-demo/tsconfig.node.json @@ -0,0 +1,9 @@ +{ + "compilerOptions": { + "composite": true, + "module": "ESNext", + "moduleResolution": "Node", + "allowSyntheticDefaultImports": true + }, + "include": ["vite.config.ts"] +} diff --git a/packages/analytics-demo/vite.config.ts b/packages/analytics-demo/vite.config.ts new file mode 100644 index 0000000..af04995 --- /dev/null +++ b/packages/analytics-demo/vite.config.ts @@ -0,0 +1,25 @@ +import { defineConfig } from 'vite'; +import vue from '@vitejs/plugin-vue'; +import webExtension, { readJsonFile } from 'vite-plugin-web-extension'; + +function generateManifest() { + const manifest = readJsonFile('src/manifest.json'); + const pkg = readJsonFile('package.json'); + return { + name: pkg.name, + description: pkg.description, + version: pkg.version, + ...manifest, + }; +} + +// https://vitejs.dev/config/ +export default defineConfig({ + plugins: [ + vue(), + webExtension({ + manifest: generateManifest, + watchFilePaths: ['package.json', 'manifest.json'], + }), + ], +}); diff --git a/packages/analytics/src/clients/createUmamiClient.ts b/packages/analytics/src/clients/createUmamiClient.ts index 0ffb303..a30f58e 100644 --- a/packages/analytics/src/clients/createUmamiClient.ts +++ b/packages/analytics/src/clients/createUmamiClient.ts @@ -34,7 +34,7 @@ export function createUmamiClient(config: UmamiConfig): ExtensionAnalyticsClient const baseUrl = config.url.endsWith('/') ? config.url.substring(config.url.length - 1) : config.url; - const sendUrl = `${baseUrl}/api/send`; + const sendUrl = `${baseUrl}/api/collect`; return { async uploadEvent(options) { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 9fe195a..0b9d3fe 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -64,6 +64,31 @@ importers: typescript: 4.8.4 vitest: 0.24.5_jsdom@20.0.3 + packages/analytics-demo: + specifiers: + '@types/webextension-polyfill': ^0.10.0 + '@vitejs/plugin-vue': ^4.0.0 + '@webext-core/analytics': workspace:* + typescript: ^4.9.5 + vite: ^4.1.4 + vite-plugin-web-extension: ^3.0.1 + vue: ^3.2.47 + vue-router: ^4.2.1 + vue-tsc: ^1.2.0 + webextension-polyfill: ^0.10.0 + dependencies: + '@webext-core/analytics': link:../analytics + vue: 3.2.47 + vue-router: 4.2.1_vue@3.2.47 + devDependencies: + '@types/webextension-polyfill': 0.10.0 + '@vitejs/plugin-vue': 4.2.1_vite@4.3.5+vue@3.2.47 + typescript: 4.9.5 + vite: 4.3.5 + vite-plugin-web-extension: 3.0.2_vite@4.3.5 + vue-tsc: 1.6.5_typescript@4.9.5 + webextension-polyfill: 0.10.0 + packages/fake-browser: specifiers: '@types/lodash.merge': ^4.6.7 @@ -421,12 +446,10 @@ packages: /@babel/helper-string-parser/7.19.4: resolution: {integrity: sha512-nHtDoQcuqFmwYNYPz3Rah5ph2p8PFeFCsZk9A/48dPc/rGocJ5J3hAAZ7pb76VWX3fZKu+uEr/FhH5jLx7umrw==} engines: {node: '>=6.9.0'} - dev: true /@babel/helper-validator-identifier/7.19.1: resolution: {integrity: sha512-awrNfaMtnHUr653GgGEs++LlAvW6w+DcPrOliSMXWCKo597CwL5Acf/wWdNkf/tfEQE3mjkeD1YOVZOUV/od1w==} engines: {node: '>=6.9.0'} - dev: true /@babel/highlight/7.18.6: resolution: {integrity: sha512-u7stbOuYjaPezCuLj29hNW1v64M2Md2qupEKP1fHc7WdOA3DgLh37suiSrZYY7haUB7iBeQZ9P1uiRF359do3g==} @@ -443,6 +466,13 @@ packages: hasBin: true dependencies: '@babel/types': 7.20.7 + + /@babel/parser/7.22.3: + resolution: {integrity: sha512-vrukxyW/ep8UD1UDzOYpTKQ6abgjFoeG6L+4ar9+c5TN9QnlqiOi6QK7LSR5ewm/ERyGkT/Ai6VboNrxhbr9Uw==} + engines: {node: '>=6.0.0'} + hasBin: true + dependencies: + '@babel/types': 7.20.7 dev: true /@babel/runtime/7.19.4: @@ -459,7 +489,6 @@ packages: '@babel/helper-string-parser': 7.19.4 '@babel/helper-validator-identifier': 7.19.1 to-fast-properties: 2.0.0 - dev: true /@bcoe/v8-coverage/0.2.3: resolution: {integrity: sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==} @@ -1148,6 +1177,10 @@ packages: resolution: {integrity: sha512-4p9vcSmxAayx72yn70joFoL44c9MO/0+iVEBIQXe3v2h2SiAsEIo/G5v6ObFWvNKRFjbrVadNf9LqEEZeQPzdA==} dev: true + /@types/webextension-polyfill/0.10.0: + resolution: {integrity: sha512-If4EcaHzYTqcbNMp/FdReVdRmLL/Te42ivnJII551bYjhX19bWem5m14FERCqdJA732OloGuxCRvLBvcMGsn4A==} + dev: true + /@types/webextension-polyfill/0.9.1: resolution: {integrity: sha512-6aNzPIhqKlAV9t06nwSH3/veAceYE2dS2RVFZI8V1+UXHqsFNB6cRwxNmheiBvEGRc45E/gyZNzH0xAYIC27KA==} dev: true @@ -1313,6 +1346,51 @@ packages: pretty-format: 27.5.1 dev: true + /@volar/language-core/1.4.1: + resolution: {integrity: sha512-EIY+Swv+TjsWpxOxujjMf1ZXqOjg9MT2VMXZ+1dKva0wD8W0L6EtptFFcCJdBbcKmGMFkr57Qzz9VNMWhs3jXQ==} + dependencies: + '@volar/source-map': 1.4.1 + dev: true + + /@volar/source-map/1.4.1: + resolution: {integrity: sha512-bZ46ad72dsbzuOWPUtJjBXkzSQzzSejuR3CT81+GvTEI2E994D8JPXzM3tl98zyCNnjgs4OkRyliImL1dvJ5BA==} + dependencies: + muggle-string: 0.2.2 + dev: true + + /@volar/typescript/1.4.1-patch.2_typescript@4.9.5: + resolution: {integrity: sha512-lPFYaGt8OdMEzNGJJChF40uYqMO4Z/7Q9fHPQC/NRVtht43KotSXLrkPandVVMf9aPbiJ059eAT+fwHGX16k4w==} + peerDependencies: + typescript: '*' + dependencies: + '@volar/language-core': 1.4.1 + typescript: 4.9.5 + dev: true + + /@volar/vue-language-core/1.6.5: + resolution: {integrity: sha512-IF2b6hW4QAxfsLd5mePmLgtkXzNi+YnH6ltCd80gb7+cbdpFMjM1I+w+nSg2kfBTyfu+W8useCZvW89kPTBpzg==} + dependencies: + '@volar/language-core': 1.4.1 + '@volar/source-map': 1.4.1 + '@vue/compiler-dom': 3.3.4 + '@vue/compiler-sfc': 3.3.4 + '@vue/reactivity': 3.3.4 + '@vue/shared': 3.3.4 + minimatch: 9.0.1 + muggle-string: 0.2.2 + vue-template-compiler: 2.7.14 + dev: true + + /@volar/vue-typescript/1.6.5_typescript@4.9.5: + resolution: {integrity: sha512-er9rVClS4PHztMUmtPMDTl+7c7JyrxweKSAEe/o/Noeq2bQx6v3/jZHVHBe8ZNUti5ubJL/+Tg8L3bzmlalV8A==} + peerDependencies: + typescript: '*' + dependencies: + '@volar/typescript': 1.4.1-patch.2_typescript@4.9.5 + '@volar/vue-language-core': 1.6.5 + typescript: 4.9.5 + dev: true + /@vue/compiler-core/3.2.47: resolution: {integrity: sha512-p4D7FDnQb7+YJmO2iPEv0SQNeNzcbHdGByJDsT4lynf63AFkOTFN07HsiRSvjGo0QrxR/o3d0hUyNCUnBU2Tig==} dependencies: @@ -1320,6 +1398,14 @@ packages: '@vue/shared': 3.2.47 estree-walker: 2.0.2 source-map: 0.6.1 + + /@vue/compiler-core/3.3.4: + resolution: {integrity: sha512-cquyDNvZ6jTbf/+x+AgM2Arrp6G4Dzbb0R64jiG804HRMfRiFXWI6kqUVqZ6ZR0bQhIoQjB4+2bhNtVwndW15g==} + dependencies: + '@babel/parser': 7.22.3 + '@vue/shared': 3.3.4 + estree-walker: 2.0.2 + source-map-js: 1.0.2 dev: true /@vue/compiler-dom/3.2.47: @@ -1327,6 +1413,12 @@ packages: dependencies: '@vue/compiler-core': 3.2.47 '@vue/shared': 3.2.47 + + /@vue/compiler-dom/3.3.4: + resolution: {integrity: sha512-wyM+OjOVpuUukIq6p5+nwHYtj9cFroz9cwkfmP9O1nzH68BenTTv0u7/ndggT8cIQlnBeOo6sUT/gvHcIkLA5w==} + dependencies: + '@vue/compiler-core': 3.3.4 + '@vue/shared': 3.3.4 dev: true /@vue/compiler-sfc/3.2.47: @@ -1340,8 +1432,22 @@ packages: '@vue/shared': 3.2.47 estree-walker: 2.0.2 magic-string: 0.25.9 - postcss: 8.4.21 + postcss: 8.4.23 source-map: 0.6.1 + + /@vue/compiler-sfc/3.3.4: + resolution: {integrity: sha512-6y/d8uw+5TkCuzBkgLS0v3lSM3hJDntFEiUORM11pQ/hKvkhSKZrXW6i69UyXlJQisJxuUEJKAWEqWbWsLeNKQ==} + dependencies: + '@babel/parser': 7.20.15 + '@vue/compiler-core': 3.3.4 + '@vue/compiler-dom': 3.3.4 + '@vue/compiler-ssr': 3.3.4 + '@vue/reactivity-transform': 3.3.4 + '@vue/shared': 3.3.4 + estree-walker: 2.0.2 + magic-string: 0.30.0 + postcss: 8.4.23 + source-map-js: 1.0.2 dev: true /@vue/compiler-ssr/3.2.47: @@ -1349,11 +1455,16 @@ packages: dependencies: '@vue/compiler-dom': 3.2.47 '@vue/shared': 3.2.47 + + /@vue/compiler-ssr/3.3.4: + resolution: {integrity: sha512-m0v6oKpup2nMSehwA6Uuu+j+wEwcy7QmwMkVNVfrV9P2qE5KshC6RwOCq8fjGS/Eak/uNb8AaWekfiXxbBB6gQ==} + dependencies: + '@vue/compiler-dom': 3.3.4 + '@vue/shared': 3.3.4 dev: true /@vue/devtools-api/6.5.0: resolution: {integrity: sha512-o9KfBeaBmCKl10usN4crU53fYtC1r7jJwdGKjPT24t348rHxgfpZ0xL3Xm/gLUYnc0oTp8LAmrxOeLyu6tbk2Q==} - dev: true /@vue/reactivity-transform/3.2.47: resolution: {integrity: sha512-m8lGXw8rdnPVVIdIFhf0LeQ/ixyHkH5plYuS83yop5n7ggVJU+z5v0zecwEnX7fa7HNLBhh2qngJJkxpwEEmYA==} @@ -1363,12 +1474,26 @@ packages: '@vue/shared': 3.2.47 estree-walker: 2.0.2 magic-string: 0.25.9 + + /@vue/reactivity-transform/3.3.4: + resolution: {integrity: sha512-MXgwjako4nu5WFLAjpBnCj/ieqcjE2aJBINUNQzkZQfzIZA4xn+0fV1tIYBJvvva3N3OvKGofRLvQIwEQPpaXw==} + dependencies: + '@babel/parser': 7.22.3 + '@vue/compiler-core': 3.3.4 + '@vue/shared': 3.3.4 + estree-walker: 2.0.2 + magic-string: 0.30.0 dev: true /@vue/reactivity/3.2.47: resolution: {integrity: sha512-7khqQ/75oyyg+N/e+iwV6lpy1f5wq759NdlS1fpAhFXa8VeAIKGgk2E/C4VF59lx5b+Ezs5fpp/5WsRYXQiKxQ==} dependencies: '@vue/shared': 3.2.47 + + /@vue/reactivity/3.3.4: + resolution: {integrity: sha512-kLTDLwd0B1jG08NBF3R5rqULtv/f8x3rOFByTDz4J53ttIQEDmALqKqXY0J+XQeN0aV2FBxY8nJDf88yvOPAqQ==} + dependencies: + '@vue/shared': 3.3.4 dev: true /@vue/runtime-core/3.2.47: @@ -1376,7 +1501,6 @@ packages: dependencies: '@vue/reactivity': 3.2.47 '@vue/shared': 3.2.47 - dev: true /@vue/runtime-dom/3.2.47: resolution: {integrity: sha512-ArXrFTjS6TsDei4qwNvgrdmHtD930KgSKGhS5M+j8QxXrDJYLqYw4RRcDy1bz1m1wMmb6j+zGLifdVHtkXA7gA==} @@ -1384,7 +1508,6 @@ packages: '@vue/runtime-core': 3.2.47 '@vue/shared': 3.2.47 csstype: 2.6.21 - dev: true /@vue/server-renderer/3.2.47_vue@3.2.47: resolution: {integrity: sha512-dN9gc1i8EvmP9RCzvneONXsKfBRgqFeFZLurmHOveL7oH6HiFXJw5OGu294n1nHc/HMgTy6LulU/tv5/A7f/LA==} @@ -1394,10 +1517,12 @@ packages: '@vue/compiler-ssr': 3.2.47 '@vue/shared': 3.2.47 vue: 3.2.47 - dev: true /@vue/shared/3.2.47: resolution: {integrity: sha512-BHGyyGN3Q97EZx0taMQ+OLNuZcW3d37ZEVmEAyeoA9ERdGvm9Irc/0Fua8SNyOtV1w6BS4q25wbMzJujO9HIfQ==} + + /@vue/shared/3.3.4: + resolution: {integrity: sha512-7OjdcV8vQ74eiz1TZLzZP4JwqM5fA94K6yntPS5Z25r9HDuGNzaGdgvwKYq6S+MxwF0TFRwe50fIR/MYnakdkQ==} dev: true /@vueuse/core/10.1.2_vue@3.2.47: @@ -2253,7 +2378,6 @@ packages: /csstype/2.6.21: resolution: {integrity: sha512-Z1PhmomIfypOpoMjRQB70jfvy/wxT50qW08YXO5lMIJkrdq4yOTR+AW7FqutScmB9NkLwxo+jU+kZLbofZZq/w==} - dev: true /dashdash/1.14.1: resolution: {integrity: sha512-jRFi8UDGo6j+odZiEpjazZaWqEal3w/basFjQHQEwVtZJGDpxbH1MeYluwCS8Xq5wmLJooDlMgvVarmWfGM44g==} @@ -2276,6 +2400,10 @@ packages: whatwg-url: 11.0.0 dev: true + /de-indent/1.0.2: + resolution: {integrity: sha512-e/1zu3xH5MQryN2zdVaF0OrdNLUbvWxzMbi+iNA6Bky7l1RoP8a2fIbRocyHclXt/arDrrR6lL3TqFD9pMQTsg==} + dev: true + /debounce/1.2.1: resolution: {integrity: sha512-XRRe6Glud4rd/ZGQfiV1ruXSfbvfJedlV9Y6zOlP+2K04vBYiJEte6stfFkCP03aMnY5tsipamumUjL14fofug==} dev: true @@ -2913,7 +3041,6 @@ packages: /estree-walker/2.0.2: resolution: {integrity: sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==} - dev: true /esutils/2.0.3: resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==} @@ -3383,6 +3510,11 @@ packages: function-bind: 1.1.1 dev: true + /he/1.2.0: + resolution: {integrity: sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==} + hasBin: true + dev: true + /html-encoding-sniffer/3.0.0: resolution: {integrity: sha512-oWv4T4yJ52iKrufjnyZPkrN0CH3QnrUqdB6In1g5Fe1mia8GmF36gnfNySxoZtxD5+NmYw1EElVXiBk93UeskA==} engines: {node: '>=12'} @@ -4088,6 +4220,12 @@ packages: resolution: {integrity: sha512-RmF0AsMzgt25qzqqLc1+MbHmhdx0ojF2Fvs4XnOqz2ZOBXzzkEwc/dJQZCYHAn7v1jbVOjAZfK8msRn4BxO4VQ==} dependencies: sourcemap-codec: 1.4.8 + + /magic-string/0.30.0: + resolution: {integrity: sha512-LA+31JYDJLs82r2ScLrlz1GjSgu66ZV518eyWT+S8VhyQn/JL0u9MeBOvQMGYiPk1DBiSN9DDMOcXvigJZaViQ==} + engines: {node: '>=12'} + dependencies: + '@jridgewell/sourcemap-codec': 1.4.14 dev: true /make-dir/3.1.0: @@ -4190,6 +4328,13 @@ packages: brace-expansion: 2.0.1 dev: true + /minimatch/9.0.1: + resolution: {integrity: sha512-0jWhJpD/MdhPXwPuiRkCbfYfSKp2qnn2eOc279qI7f+osl/l+prKSrvhg157zSYvx/1nmgn2NqdT6k2Z7zSH9w==} + engines: {node: '>=16 || 14 >=14.17'} + dependencies: + brace-expansion: 2.0.1 + dev: true + /minimist/1.2.7: resolution: {integrity: sha512-bzfL1YUZsP41gmu/qjrEk0Q6i2ix/cVeAhbCbqH9u3zYutS1cLg00qhrD0M2MVdCcx4Sc0UpP2eBWo9rotpq6g==} dev: true @@ -4245,6 +4390,10 @@ packages: resolution: {integrity: sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==} dev: true + /muggle-string/0.2.2: + resolution: {integrity: sha512-YVE1mIJ4VpUMqZObFndk9CJu6DBJR/GB13p3tXuNbwD4XExaI5EOuRl6BHeIDxIqXZVxSfAC+y6U1Z/IxCfKUg==} + dev: true + /multimatch/4.0.0: resolution: {integrity: sha512-lDmx79y1z6i7RNx0ZGCPq1bzJ6ZoDDKbvh7jxr9SJcWLkShMzXrHbYVpTdnhNM5MXpDUxCQ4DgqVttVXlBgiBQ==} engines: {node: '>=8'} @@ -4300,7 +4449,6 @@ packages: resolution: {integrity: sha512-BGcqMMJuToF7i1rt+2PWSNVnWIkGCU78jBG3RxO/bZlnZPK2Cmi2QaffxGO/2RvWi9sL+FAiRiXMgsyxQ1DIDA==} engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} hasBin: true - dev: true /natural-compare/1.4.0: resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==} @@ -4596,7 +4744,6 @@ packages: /picocolors/1.0.0: resolution: {integrity: sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==} - dev: true /picomatch/2.3.1: resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==} @@ -4685,7 +4832,6 @@ packages: nanoid: 3.3.6 picocolors: 1.0.0 source-map-js: 1.0.2 - dev: true /preact/10.11.3: resolution: {integrity: sha512-eY93IVpod/zG3uMF22Unl8h9KkrcKIRs2EGar8hwLZZDU1lkjph303V9HZBwufh2s736U6VXuhD109LYqPoffg==} @@ -4998,14 +5144,6 @@ packages: fsevents: 2.3.2 dev: true - /rollup/3.18.0: - resolution: {integrity: sha512-J8C6VfEBjkvYPESMQYxKHxNOh4A5a3FlP+0BETGo34HEcE4eTlgCrO2+eWzlu2a/sHs2QUkZco+wscH7jhhgWg==} - engines: {node: '>=14.18.0', npm: '>=8.0.0'} - hasBin: true - optionalDependencies: - fsevents: 2.3.2 - dev: true - /rollup/3.21.6: resolution: {integrity: sha512-SXIICxvxQxR3D4dp/3LDHZIJPC8a4anKMHd4E3Jiz2/JnY+2bEjqrOokAauc5ShGVNFHlEFjBXAXlaxkJqIqSg==} engines: {node: '>=14.18.0', npm: '>=8.0.0'} @@ -5189,7 +5327,6 @@ packages: /source-map-js/1.0.2: resolution: {integrity: sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==} engines: {node: '>=0.10.0'} - dev: true /source-map-support/0.5.21: resolution: {integrity: sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==} @@ -5201,7 +5338,6 @@ packages: /source-map/0.6.1: resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==} engines: {node: '>=0.10.0'} - dev: true /source-map/0.8.0-beta.0: resolution: {integrity: sha512-2ymg6oRBpebeZi9UUNsgQ89bhx01TcTkmNTGnNO88imTmbSgy4nfujrgVEFKWpMTEGA11EDkTt7mqObTPdigIA==} @@ -5213,7 +5349,6 @@ packages: /sourcemap-codec/1.4.8: resolution: {integrity: sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA==} deprecated: Please use @jridgewell/sourcemap-codec instead - dev: true /spawn-sync/1.0.15: resolution: {integrity: sha512-9DWBgrgYZzNghseho0JOuh+5fg9u6QWhAWa51QC7+U5rCheZ/j1DrEZnyE0RBBRqZ9uEXGPgSSM0nky6burpVw==} @@ -5459,7 +5594,6 @@ packages: /to-fast-properties/2.0.0: resolution: {integrity: sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==} engines: {node: '>=4'} - dev: true /to-regex-range/5.0.1: resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} @@ -5692,6 +5826,12 @@ packages: hasBin: true dev: true + /typescript/4.9.5: + resolution: {integrity: sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==} + engines: {node: '>=4.2.0'} + hasBin: true + dev: true + /typescript/5.0.4: resolution: {integrity: sha512-cW9T5W9xY37cc+jfEnaUvX91foxtHkza3Nw3wkoF4sSlKn0MONdkdEndig/qPBWXNkmplh3NzayQzCiHM4/hqw==} engines: {node: '>=12.20'} @@ -5814,7 +5954,7 @@ packages: picocolors: 1.0.0 source-map: 0.6.1 source-map-support: 0.5.21 - vite: 4.0.4_@types+node@18.11.9 + vite: 4.3.5_@types+node@18.11.9 transitivePeerDependencies: - '@types/node' - less @@ -5837,7 +5977,7 @@ packages: picocolors: 1.0.0 source-map: 0.6.1 source-map-support: 0.5.21 - vite: 4.1.4_@types+node@18.11.9 + vite: 4.3.5_@types+node@18.11.9 transitivePeerDependencies: - '@types/node' - less @@ -5858,7 +5998,7 @@ packages: mlly: 1.1.1 pathe: 1.1.0 picocolors: 1.0.0 - vite: 4.0.4_@types+node@18.11.9 + vite: 4.3.5_@types+node@18.11.9 transitivePeerDependencies: - '@types/node' - less @@ -5919,6 +6059,32 @@ packages: - utf-8-validate dev: true + /vite-plugin-web-extension/3.0.2_vite@4.3.5: + resolution: {integrity: sha512-YdjDYkeSZIh1xo2qbGTzKjxapDwn0aqVtSjbpA8Jn3iQGZLjNRYDE0qcuFftvKPBxKlV06Q4VzkY6x9M0UprkQ==} + peerDependencies: + vite: ^4 + dependencies: + ajv: 8.11.2 + async-lock: 1.4.0 + fs-extra: 10.1.0 + json5: 2.2.3 + linkedom: 0.14.25 + lodash.uniq: 4.5.0 + lodash.uniqby: 4.7.0 + md5: 2.3.0 + vite: 4.3.5 + web-ext: 7.3.1 + webextension-polyfill: 0.10.0 + yaml: 2.2.1 + transitivePeerDependencies: + - body-parser + - bufferutil + - express + - safe-compare + - supports-color + - utf-8-validate + dev: true + /vite/3.2.4: resolution: {integrity: sha512-Z2X6SRAffOUYTa+sLy3NQ7nlHFU100xwanq1WDwqaiFiCe+25zdxP1TfCS5ojPV2oDDcXudHIoPnI1Z/66B7Yw==} engines: {node: ^14.18.0 || >=16.0.0} @@ -6019,42 +6185,8 @@ packages: fsevents: 2.3.2 dev: true - /vite/4.0.4_@types+node@18.11.9: - resolution: {integrity: sha512-xevPU7M8FU0i/80DMR+YhgrzR5KS2ORy1B4xcX/cXLsvnUWvfHuqMmVU6N0YiJ4JWGRJJsLCgjEzKjG9/GKoSw==} - engines: {node: ^14.18.0 || >=16.0.0} - hasBin: true - peerDependencies: - '@types/node': '>= 14' - less: '*' - sass: '*' - stylus: '*' - sugarss: '*' - terser: ^5.4.0 - peerDependenciesMeta: - '@types/node': - optional: true - less: - optional: true - sass: - optional: true - stylus: - optional: true - sugarss: - optional: true - terser: - optional: true - dependencies: - '@types/node': 18.11.9 - esbuild: 0.16.15 - postcss: 8.4.21 - resolve: 1.22.1 - rollup: 3.9.1 - optionalDependencies: - fsevents: 2.3.2 - dev: true - - /vite/4.1.4_@types+node@18.11.9: - resolution: {integrity: sha512-3knk/HsbSTKEin43zHu7jTwYWv81f8kgAL99G5NWBcA1LKvtvcVAC4JjBH1arBunO9kQka+1oGbrMKOjk4ZrBg==} + /vite/4.3.5: + resolution: {integrity: sha512-0gEnL9wiRFxgz40o/i/eTBwm+NEbpUeTWhzKrZDSdKm6nplj+z4lKz8ANDgildxHm47Vg8EUia0aicKbawUVVA==} engines: {node: ^14.18.0 || >=16.0.0} hasBin: true peerDependencies: @@ -6078,16 +6210,14 @@ packages: terser: optional: true dependencies: - '@types/node': 18.11.9 - esbuild: 0.16.15 - postcss: 8.4.21 - resolve: 1.22.1 - rollup: 3.18.0 + esbuild: 0.17.18 + postcss: 8.4.23 + rollup: 3.21.6 optionalDependencies: fsevents: 2.3.2 dev: true - /vite/4.3.5: + /vite/4.3.5_@types+node@18.11.9: resolution: {integrity: sha512-0gEnL9wiRFxgz40o/i/eTBwm+NEbpUeTWhzKrZDSdKm6nplj+z4lKz8ANDgildxHm47Vg8EUia0aicKbawUVVA==} engines: {node: ^14.18.0 || >=16.0.0} hasBin: true @@ -6112,6 +6242,7 @@ packages: terser: optional: true dependencies: + '@types/node': 18.11.9 esbuild: 0.17.18 postcss: 8.4.23 rollup: 3.21.6 @@ -6276,7 +6407,7 @@ packages: tinybench: 2.3.1 tinypool: 0.3.1 tinyspy: 1.0.2 - vite: 4.0.4_@types+node@18.11.9 + vite: 4.3.5_@types+node@18.11.9 vite-node: 0.28.0_@types+node@18.11.9 why-is-node-running: 2.2.2 transitivePeerDependencies: @@ -6331,7 +6462,7 @@ packages: tinybench: 2.3.1 tinypool: 0.3.1 tinyspy: 1.0.2 - vite: 4.1.4_@types+node@18.11.9 + vite: 4.3.5_@types+node@18.11.9 vite-node: 0.28.5_@types+node@18.11.9 why-is-node-running: 2.2.2 transitivePeerDependencies: @@ -6386,7 +6517,7 @@ packages: tinybench: 2.3.1 tinypool: 0.3.1 tinyspy: 1.0.2 - vite: 4.0.4_@types+node@18.11.9 + vite: 4.3.5_@types+node@18.11.9 vite-node: 0.29.2_@types+node@18.11.9 why-is-node-running: 2.2.2 transitivePeerDependencies: @@ -6421,6 +6552,34 @@ packages: vue: 3.2.47 dev: true + /vue-router/4.2.1_vue@3.2.47: + resolution: {integrity: sha512-nW28EeifEp8Abc5AfmAShy5ZKGsGzjcnZ3L1yc2DYUo+MqbBClrRP9yda3dIekM4I50/KnEwo1wkBLf7kHH5Cw==} + peerDependencies: + vue: ^3.2.0 + dependencies: + '@vue/devtools-api': 6.5.0 + vue: 3.2.47 + dev: false + + /vue-template-compiler/2.7.14: + resolution: {integrity: sha512-zyA5Y3ArvVG0NacJDkkzJuPQDF8RFeRlzV2vLeSnhSpieO6LK2OVbdLPi5MPPs09Ii+gMO8nY4S3iKQxBxDmWQ==} + dependencies: + de-indent: 1.0.2 + he: 1.2.0 + dev: true + + /vue-tsc/1.6.5_typescript@4.9.5: + resolution: {integrity: sha512-Wtw3J7CC+JM2OR56huRd5iKlvFWpvDiU+fO1+rqyu4V2nMTotShz4zbOZpW5g9fUOcjnyZYfBo5q5q+D/q27JA==} + hasBin: true + peerDependencies: + typescript: '*' + dependencies: + '@volar/vue-language-core': 1.6.5 + '@volar/vue-typescript': 1.6.5_typescript@4.9.5 + semver: 7.3.8 + typescript: 4.9.5 + dev: true + /vue/3.2.47: resolution: {integrity: sha512-60188y/9Dc9WVrAZeUVSDxRQOZ+z+y5nO2ts9jWXSTkMvayiWxCWOWtBQoYjLeccfXkiiPZWAHcV+WTPhkqJHQ==} dependencies: @@ -6429,7 +6588,6 @@ packages: '@vue/runtime-dom': 3.2.47 '@vue/server-renderer': 3.2.47_vue@3.2.47 '@vue/shared': 3.2.47 - dev: true /w3c-xmlserializer/4.0.0: resolution: {integrity: sha512-d+BFHzbiCx6zGfz0HyQ6Rg69w9k19nviJspaj4yNscGjrHu94sVP+aRm75yEbCh+r2/yR+7q6hux9LVtbuTGBw==}