From d9b9db208f4992a59726edfe445153987e4b97a1 Mon Sep 17 00:00:00 2001 From: youssef-intuned Date: Sun, 21 Jun 2026 16:29:06 +0300 Subject: [PATCH] add app insights + telemetry to extension different components --- .gitignore | 3 + README.md | 23 ++ entrypoints/background.ts | 18 ++ entrypoints/content.ts | 15 + .../popup/components/TelemetryToggle.tsx | 46 ++++ .../popup/components/WorkspaceSwitcher.tsx | 2 + entrypoints/popup/main.tsx | 42 ++- entrypoints/popup/ui.module.css | 25 ++ lib/agent/agentLoopController.ts | 24 ++ lib/auth/request.ts | 57 +++- lib/background/context.ts | 3 + lib/background/handlers/auth.ts | 23 +- lib/background/handlers/index.ts | 6 + lib/background/handlers/telemetry.ts | 44 +++ lib/background/registerHandlers.ts | 39 ++- lib/config.ts | 73 +++++ lib/messaging/index.ts | 2 + lib/messaging/protocol.ts | 39 +++ lib/telemetry/api.ts | 35 +++ lib/telemetry/client.ts | 259 ++++++++++++++++++ lib/telemetry/connectionString.ts | 45 +++ lib/telemetry/forwardingSink.ts | 62 +++++ lib/telemetry/globalErrors.ts | 28 ++ lib/telemetry/index.ts | 1 + lib/telemetry/scrub.ts | 111 ++++++++ lib/telemetry/types.ts | 61 +++++ package.json | 3 + tests/unit/agent/agentLoopController.test.ts | 17 +- tests/unit/auth/request.test.ts | 90 ++++++ tests/unit/background/harness.ts | 43 ++- .../unit/background/registerHandlers.test.ts | 134 +++++++++ tests/unit/background/telemetry.test.ts | 47 ++++ tests/unit/config.test.ts | 61 +++++ tests/unit/telemetry/client.test.ts | 240 ++++++++++++++++ tests/unit/telemetry/connectionString.test.ts | 35 +++ tests/unit/telemetry/forwardingSink.test.ts | 52 ++++ tests/unit/telemetry/scrub.test.ts | 102 +++++++ wxt.config.ts | 14 +- yarn.lock | 37 +++ 39 files changed, 1930 insertions(+), 31 deletions(-) create mode 100644 entrypoints/popup/components/TelemetryToggle.tsx create mode 100644 lib/background/handlers/telemetry.ts create mode 100644 lib/telemetry/api.ts create mode 100644 lib/telemetry/client.ts create mode 100644 lib/telemetry/connectionString.ts create mode 100644 lib/telemetry/forwardingSink.ts create mode 100644 lib/telemetry/globalErrors.ts create mode 100644 lib/telemetry/index.ts create mode 100644 lib/telemetry/scrub.ts create mode 100644 lib/telemetry/types.ts create mode 100644 tests/unit/background/registerHandlers.test.ts create mode 100644 tests/unit/background/telemetry.test.ts create mode 100644 tests/unit/config.test.ts create mode 100644 tests/unit/telemetry/client.test.ts create mode 100644 tests/unit/telemetry/connectionString.test.ts create mode 100644 tests/unit/telemetry/forwardingSink.test.ts create mode 100644 tests/unit/telemetry/scrub.test.ts diff --git a/.gitignore b/.gitignore index 38d3fe8..42d8f30 100644 --- a/.gitignore +++ b/.gitignore @@ -6,3 +6,6 @@ playwright-report/ test-results/ *.log .DS_Store +.env +.env.local +.env.*.local diff --git a/README.md b/README.md index 414a8d8..a6a4d3a 100644 --- a/README.md +++ b/README.md @@ -74,6 +74,29 @@ After the first `yarn dev`, load the unpacked extension from `.output/chrome-mv3 - **Browser** — Vitest browser-mode tests that run selector generation against a real DOM and prove each candidate resolves to exactly the expected element set. This is the correctness oracle. Both layers run under `yarn test`. - **E2E** — Playwright against the packaged MV3 extension with a real page, pointer flow, popup, content script, and background worker. Run with `yarn e2e`. +## Telemetry + +The extension reports anonymous diagnostics to Azure Application Insights so we can +spot errors and usage issues in the wild. It is **anonymous** (a random per-install +id — never your email, workspace name, browsed-page URLs, or selector strings) and can +be turned off from the workspace menu in the popup ("Share anonymous usage data"). + +What's collected: exceptions/unhandled rejections, command events with timing, +agent-loop outcomes, and Intuned API request host, path, status, and latency — never +the query string (it carries your workspace id). See [lib/telemetry/](./lib/telemetry). + +The background service worker is the single egress; content and popup forward items +to it over the message protocol. Because an MV3 worker has no DOM and is short-lived, +the SDK pipeline is built from `@microsoft/applicationinsights-core-js` + +`-channel-js` directly (fetch transport, in-memory buffer) — not the Web SDK. + +The Azure connection string is hard-coded in [lib/config.ts](./lib/config.ts) +(`HARDCODED_CONNECTION_STRING`); the write-only ingestion key is safe to embed in the +published bundle. Because it is always present, dev builds report too — while developing, +either use the in-popup opt-out or point telemetry at a throwaway resource by setting the +`config.appInsightsConnectionString` key in `browser.storage.local` (an empty value falls +back to the hard-coded string; clearing it restores the default). + ## Project layout ``` diff --git a/entrypoints/background.ts b/entrypoints/background.ts index f109ef1..e5c7be3 100644 --- a/entrypoints/background.ts +++ b/entrypoints/background.ts @@ -13,21 +13,39 @@ import { } from "@/lib/background/contextMenu"; import { createBackgroundMessagingClient } from "@/lib/messaging"; import { SelectorState } from "@/lib/state"; +import { BackgroundTelemetryClient } from "@/lib/telemetry/client"; +import { setTelemetrySink } from "@/lib/telemetry/api"; +import { reportGlobalErrors } from "@/lib/telemetry/globalErrors"; export default defineBackground(() => { const state = new SelectorState(); const messaging = createBackgroundMessagingClient(); + + // Single App Insights egress for the whole extension. Registered as the api.ts + // sink so background-originated trackEvent/trackException (registerHandlers, + // fetchIntunedApi, …) route here; content/popup forward over messaging. init() + // is async — calls before it resolves are safe no-ops. + const telemetry = new BackgroundTelemetryClient(); + setTelemetrySink(telemetry); + void telemetry.init(); + const agentLoopController = new AgentLoopController({ state, backgroundMessagingClient: messaging, + telemetry, }); const context: BackgroundContext = { state, agentLoopController, backgroundMessagingClient: messaging, + telemetry, }; + // Global safety net. This runs at the top of every worker cold start, so any + // otherwise-unobserved error/rejection in the worker is captured. + reportGlobalErrors(self); + void state.hydrate(); registerBackgroundHandlers(backgroundHandlers, context); diff --git a/entrypoints/content.ts b/entrypoints/content.ts index 29b8be4..5a04579 100644 --- a/entrypoints/content.ts +++ b/entrypoints/content.ts @@ -6,11 +6,24 @@ import { type ContentContext, } from "@/lib/content"; import { createContentMessagingClient } from "@/lib/messaging"; +import { setTelemetrySink } from "@/lib/telemetry/api"; +import { createForwardingSink } from "@/lib/telemetry/forwardingSink"; +import { reportGlobalErrors } from "@/lib/telemetry/globalErrors"; + +// A content script shares the page's `window`, so its error listeners also fire +// for host-page errors. Gate on the extension origin so we only report our own. +const EXTENSION_ORIGIN = /(?:chrome|moz)-extension:\/\//; +function isExtensionError(error: unknown, filename?: string): boolean { + if (filename && EXTENSION_ORIGIN.test(filename)) return true; + return error instanceof Error && !!error.stack && EXTENSION_ORIGIN.test(error.stack); +} export default defineContentScript({ matches: [""], runAt: "document_idle", main: () => { + setTelemetrySink(createForwardingSink("selector-extension-content")); + const contextMenu = new ContextMenuTracker(); contextMenu.addContextMenuListener(); const picker = new PickerSession(contextMenu); @@ -18,5 +31,7 @@ export default defineContentScript({ const context: ContentContext = { picker, contentMessagingClient }; registerContentHandlers(contentHandlers, context); + + reportGlobalErrors(window, isExtensionError); }, }); diff --git a/entrypoints/popup/components/TelemetryToggle.tsx b/entrypoints/popup/components/TelemetryToggle.tsx new file mode 100644 index 0000000..950cbdb --- /dev/null +++ b/entrypoints/popup/components/TelemetryToggle.tsx @@ -0,0 +1,46 @@ +import { useEffect, useState } from "react"; +import { getTelemetryEnabled, setTelemetryEnabled } from "@/lib/config"; +import styles from "../ui.module.css"; + +/** + * Opt-out control for anonymous telemetry. Reads/writes the shared config flag + * (browser.storage.local); the background client picks up the change live via a + * storage.onChanged listener. Renders nothing until the current value loads. + */ +export function TelemetryToggle() { + const [enabled, setEnabled] = useState(null); + + useEffect(() => { + let active = true; + void getTelemetryEnabled().then((value) => { + if (active) setEnabled(value); + }); + return () => { + active = false; + }; + }, []); + + if (enabled === null) return null; + + const onChange = (event: React.ChangeEvent) => { + const next = event.target.checked; + setEnabled(next); + void setTelemetryEnabled(next); + }; + + return ( + + ); +} diff --git a/entrypoints/popup/components/WorkspaceSwitcher.tsx b/entrypoints/popup/components/WorkspaceSwitcher.tsx index 0243546..0012163 100644 --- a/entrypoints/popup/components/WorkspaceSwitcher.tsx +++ b/entrypoints/popup/components/WorkspaceSwitcher.tsx @@ -5,6 +5,7 @@ import { ChevronDown, SignOutIcon } from "../icons"; import { useClickOutside } from "../hooks/useClickOutside"; import { useCachedAvatar } from "../hooks/useCachedAvatar"; import { displayName, initials } from "../utils"; +import { TelemetryToggle } from "./TelemetryToggle"; export function WorkspaceSwitcher({ identity, @@ -48,6 +49,7 @@ export function WorkspaceSwitcher({
{name}
+