From d6bb9c4f425a9b5c5a906c882f093d3fe0e31a69 Mon Sep 17 00:00:00 2001 From: brainstorm-os Date: Tue, 14 Jul 2026 01:17:25 +0200 Subject: [PATCH] =?UTF-8?q?chore(release):=20v0.4.3=20=E2=80=94=20Beta=20u?= =?UTF-8?q?sage=20analytics?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit During the public beta, Brainstorm collects anonymous product analytics (Amplitude, EU) and session replays so we can see which features people use and where they get stuck. Vault contents, passwords, and note text are never sent. - One-time disclosure popover on first launch; standing notice in Settings → Privacy → Network - Analytics runs only on pre-1.0 builds and turns off automatically at 1.0 - Shell tracks vault/app/launcher/settings events; apps initialize on open --- apps/agent/src/main.tsx | 3 + apps/automations/src/main.tsx | 3 + apps/bookmarks/src/main.tsx | 3 + apps/books/src/main.tsx | 3 + apps/browser/src/main.tsx | 3 + apps/calendar/src/main.tsx | 3 + apps/chat/src/main.tsx | 3 + apps/code-editor/src/main.tsx | 3 + apps/contacts/src/main.tsx | 3 + apps/files/src/main.tsx | 3 + apps/form-designer/src/main.tsx | 3 + apps/graph/src/main.tsx | 3 + apps/journal/src/main.tsx | 3 + apps/mailbox/src/main.tsx | 3 + apps/notes/src/main.tsx | 3 + apps/preview/src/main.tsx | 3 + apps/tasks/src/main.tsx | 3 + apps/theme-editor/src/main.tsx | 3 + apps/whiteboard/src/main.tsx | 3 + bun.lock | 111 ++++++++++++++- package.json | 2 +- packages/sdk/package.json | 2 + packages/sdk/src/analytics/analytics.test.ts | 97 +++++++++++++ .../sdk/src/analytics/beta-notice-storage.ts | 22 +++ packages/sdk/src/analytics/beta.test.ts | 14 ++ packages/sdk/src/analytics/beta.ts | 6 + packages/sdk/src/analytics/index.ts | 123 +++++++++++++++++ packages/shell/changelog/changelog.json | 129 ++++++++++++++++++ packages/shell/electron.vite.config.ts | 5 + packages/shell/package.json | 2 +- .../renderer/analytics/beta-notice.test.tsx | 61 +++++++++ .../src/renderer/analytics/beta-notice.tsx | 58 ++++++++ .../src/renderer/analytics/shell-tracking.tsx | 27 ++++ .../renderer/analytics/track-app-launch.ts | 10 ++ packages/shell/src/renderer/dashboard.tsx | 13 +- .../src/renderer/dashboard/widgets-layer.tsx | 3 +- .../src/renderer/i18n/en.descriptions.json | 6 + packages/shell/src/renderer/i18n/en.json | 6 + .../shell/src/renderer/launcher/launcher.tsx | 9 +- packages/shell/src/renderer/main.tsx | 7 + .../settings/network-egress-panel.tsx | 18 +++ packages/shell/src/renderer/vault-context.tsx | 2 + tools/new-app.ts | 3 + vitest.config.ts | 1 + 44 files changed, 784 insertions(+), 10 deletions(-) create mode 100644 packages/sdk/src/analytics/analytics.test.ts create mode 100644 packages/sdk/src/analytics/beta-notice-storage.ts create mode 100644 packages/sdk/src/analytics/beta.test.ts create mode 100644 packages/sdk/src/analytics/beta.ts create mode 100644 packages/sdk/src/analytics/index.ts create mode 100644 packages/shell/src/renderer/analytics/beta-notice.test.tsx create mode 100644 packages/shell/src/renderer/analytics/beta-notice.tsx create mode 100644 packages/shell/src/renderer/analytics/shell-tracking.tsx create mode 100644 packages/shell/src/renderer/analytics/track-app-launch.ts diff --git a/apps/agent/src/main.tsx b/apps/agent/src/main.tsx index f1443ea5..4328bfb2 100644 --- a/apps/agent/src/main.tsx +++ b/apps/agent/src/main.tsx @@ -1,4 +1,5 @@ import "@brainstorm/editor/editor-theme.css"; +import { initAnalytics } from "@brainstorm/sdk/analytics"; import "@brainstorm/sdk/app-theme.css"; import "@brainstorm/sdk/composer-context.css"; import "@brainstorm/sdk/empty-state.css"; @@ -13,6 +14,8 @@ import { AgentI18nProvider } from "./i18n-provider"; import "./styles.css"; import { AgentWidget } from "./widget"; +initAnalytics(); + const root = document.getElementById("root"); if (!root) throw new Error("Agent: #root not found in index.html"); // Stand up the shared fancy-menus runtime (object / context menus). diff --git a/apps/automations/src/main.tsx b/apps/automations/src/main.tsx index dfd83c81..604e8896 100644 --- a/apps/automations/src/main.tsx +++ b/apps/automations/src/main.tsx @@ -1,4 +1,5 @@ import "@brainstorm/sdk/app-theme.css"; +import { initAnalytics } from "@brainstorm/sdk/analytics"; import "@brainstorm/sdk/empty-state.css"; import { AppErrorBoundary } from "@brainstorm/sdk/error-boundary"; import { mountMenuHost } from "@brainstorm/sdk/menus"; @@ -10,6 +11,8 @@ import { AutomationsI18nProvider } from "./i18n-provider"; import "./styles.css"; import { AutomationsWidget } from "./widget"; +initAnalytics(); + const root = document.getElementById("root"); if (!root) throw new Error("automations: #root not found in index.html"); diff --git a/apps/bookmarks/src/main.tsx b/apps/bookmarks/src/main.tsx index 8099e05f..ea4a3b7a 100644 --- a/apps/bookmarks/src/main.tsx +++ b/apps/bookmarks/src/main.tsx @@ -7,6 +7,7 @@ */ import "@brainstorm/sdk/app-theme.css"; +import { initAnalytics } from "@brainstorm/sdk/analytics"; import { setEditorHost } from "@brainstorm/editor"; import { AppErrorBoundary } from "@brainstorm/sdk/error-boundary"; import { mountMenuHost } from "@brainstorm/sdk/menus"; @@ -18,6 +19,8 @@ import { getBrainstorm } from "./storage/runtime"; import { mountBookmarksWidget } from "./widget"; import "./styles.css"; +initAnalytics(); + // Widget-mode (Stage 7.3): the dashboard launched this bundle as a widget. // Mount the compact React widget surface (recent-bookmarks glance) instead of // the full app, and skip the full-app bootstrap below. diff --git a/apps/books/src/main.tsx b/apps/books/src/main.tsx index 5cae85dd..42e239f4 100644 --- a/apps/books/src/main.tsx +++ b/apps/books/src/main.tsx @@ -1,4 +1,5 @@ import "@brainstorm/sdk/app-theme.css"; +import { initAnalytics } from "@brainstorm/sdk/analytics"; import "@brainstorm/sdk/empty-state.css"; import "@brainstorm/editor/editor.css"; import { AppErrorBoundary } from "@brainstorm/sdk/error-boundary"; @@ -11,6 +12,8 @@ import { BooksI18nProvider } from "./i18n-provider"; import "./styles.css"; import { BooksWidget } from "./widget"; +initAnalytics(); + const root = document.getElementById("root"); if (!root) throw new Error("books: #root not found in index.html"); diff --git a/apps/browser/src/main.tsx b/apps/browser/src/main.tsx index 7b91bfd2..7227ca54 100644 --- a/apps/browser/src/main.tsx +++ b/apps/browser/src/main.tsx @@ -1,10 +1,13 @@ import "@brainstorm/sdk/app-theme.css"; +import { initAnalytics } from "@brainstorm/sdk/analytics"; import { AppErrorBoundary } from "@brainstorm/sdk/error-boundary"; import { createRoot } from "react-dom/client"; import { BrowserApp } from "./app"; import { BrowserI18nProvider } from "./i18n-provider"; import "./styles.css"; +initAnalytics(); + const root = document.getElementById("root"); if (!root) throw new Error("Web Browser: #root not found in index.html"); // No — the dev double-mount races shell-driven view attachment diff --git a/apps/calendar/src/main.tsx b/apps/calendar/src/main.tsx index b53422d0..f4263805 100644 --- a/apps/calendar/src/main.tsx +++ b/apps/calendar/src/main.tsx @@ -1,4 +1,5 @@ import "@brainstorm/sdk/app-theme.css"; +import { initAnalytics } from "@brainstorm/sdk/analytics"; import "@brainstorm/sdk/recurrence-editor.css"; import { AppErrorBoundary } from "@brainstorm/sdk/error-boundary"; import { mountMenuHost } from "@brainstorm/sdk/menus"; @@ -13,6 +14,8 @@ import "./styles.css"; // through the shared bridge (matches the other React apps' main.tsx). mountMenuHost(); +initAnalytics(); + const root = document.getElementById("root"); if (!root) throw new Error("[calendar] #root not found in index.html"); diff --git a/apps/chat/src/main.tsx b/apps/chat/src/main.tsx index 9c095a57..1cc4fac7 100644 --- a/apps/chat/src/main.tsx +++ b/apps/chat/src/main.tsx @@ -1,4 +1,5 @@ import "@brainstorm/editor/editor-theme.css"; +import { initAnalytics } from "@brainstorm/sdk/analytics"; import "@brainstorm/sdk/app-theme.css"; import "@brainstorm/sdk/composer-context.css"; import "@brainstorm/sdk/empty-state.css"; @@ -11,6 +12,8 @@ import { ChatI18nProvider } from "./i18n-provider"; import "./styles.css"; import { ChatWidget } from "./widget"; +initAnalytics(); + const root = document.getElementById("root"); if (!root) throw new Error("Chat: #root not found in index.html"); // Stand up the shared fancy-menus runtime (object / context menus). diff --git a/apps/code-editor/src/main.tsx b/apps/code-editor/src/main.tsx index b873b3e3..124bad5b 100644 --- a/apps/code-editor/src/main.tsx +++ b/apps/code-editor/src/main.tsx @@ -1,4 +1,5 @@ import "@brainstorm/sdk/app-theme.css"; +import { initAnalytics } from "@brainstorm/sdk/analytics"; import "@brainstorm/sdk/empty-state.css"; import { AppErrorBoundary } from "@brainstorm/sdk/error-boundary"; import { mountMenuHost } from "@brainstorm/sdk/menus"; @@ -8,6 +9,8 @@ import { CodeEditorApp } from "./app"; import { CodeEditorI18nProvider } from "./i18n-provider"; import "./styles.css"; +initAnalytics(); + const root = document.getElementById("root"); if (!root) throw new Error("code-editor: #root not found in index.html"); diff --git a/apps/contacts/src/main.tsx b/apps/contacts/src/main.tsx index 066a8e02..d1ea04a5 100644 --- a/apps/contacts/src/main.tsx +++ b/apps/contacts/src/main.tsx @@ -1,4 +1,5 @@ import "@brainstorm/sdk/app-theme.css"; +import { initAnalytics } from "@brainstorm/sdk/analytics"; import "@brainstorm/sdk/empty-state.css"; import "@brainstorm/editor/editor.css"; import "@brainstorm/editor/editor-theme.css"; @@ -16,6 +17,8 @@ import { ContactsWidget } from "./widget"; // overflow, row right-click) renders through it. mountMenuHost(); +initAnalytics(); + const root = document.getElementById("root"); if (!root) throw new Error("Contacts: #root not found in index.html"); diff --git a/apps/files/src/main.tsx b/apps/files/src/main.tsx index b8168154..71979e88 100644 --- a/apps/files/src/main.tsx +++ b/apps/files/src/main.tsx @@ -1,4 +1,5 @@ import "@brainstorm/sdk/app-theme.css"; +import { initAnalytics } from "@brainstorm/sdk/analytics"; import "@brainstorm/editor/editor.css"; import { AppErrorBoundary } from "@brainstorm/sdk/error-boundary"; import { mountMenuHost } from "@brainstorm/sdk/menus"; @@ -23,6 +24,8 @@ applyPersistedPanelWidth({ defaultWidth: 320, }); +initAnalytics(); + const root = document.getElementById("root"); if (!root) throw new Error("Files: #root not found in index.html"); // Stand up the fancy-menus runtime so object / context menus open through diff --git a/apps/form-designer/src/main.tsx b/apps/form-designer/src/main.tsx index 3be4aa9e..0cfdb038 100644 --- a/apps/form-designer/src/main.tsx +++ b/apps/form-designer/src/main.tsx @@ -1,4 +1,5 @@ import "@brainstorm/sdk/app-theme.css"; +import { initAnalytics } from "@brainstorm/sdk/analytics"; import "@brainstorm/sdk/property-ui/cells.css"; import "@brainstorm/sdk/select-menu.css"; import { AppErrorBoundary } from "@brainstorm/sdk/error-boundary"; @@ -9,6 +10,8 @@ import { FormDesignerApp } from "./app"; import { FormDesignerI18nProvider } from "./i18n-provider"; import "./styles.css"; +initAnalytics(); + const root = document.getElementById("root"); if (!root) throw new Error("form-designer: #root not found in index.html"); diff --git a/apps/graph/src/main.tsx b/apps/graph/src/main.tsx index 04b008f9..3dd9f294 100644 --- a/apps/graph/src/main.tsx +++ b/apps/graph/src/main.tsx @@ -1,4 +1,5 @@ import "@brainstorm/sdk/app-theme.css"; +import { initAnalytics } from "@brainstorm/sdk/analytics"; import "@brainstorm/editor/editor.css"; import { AppErrorBoundary } from "@brainstorm/sdk/error-boundary"; import { applyPersistedPanelWidth } from "@brainstorm/sdk/resizable"; @@ -16,6 +17,8 @@ applyPersistedPanelWidth({ defaultWidth: 320, }); +initAnalytics(); + const root = document.getElementById("root"); if (!root) throw new Error("graph: #root not found in index.html"); createRoot(root).render( diff --git a/apps/journal/src/main.tsx b/apps/journal/src/main.tsx index b8a6bb21..be0a45e2 100644 --- a/apps/journal/src/main.tsx +++ b/apps/journal/src/main.tsx @@ -1,4 +1,5 @@ import "@brainstorm/sdk/app-theme.css"; +import { initAnalytics } from "@brainstorm/sdk/analytics"; import { AppErrorBoundary } from "@brainstorm/sdk/error-boundary"; import { getWidgetLaunch } from "@brainstorm/sdk/widget"; import { StrictMode } from "react"; @@ -8,6 +9,8 @@ import "./types"; import "./styles.css"; import { JournalWidget } from "./widget"; +initAnalytics(); + const root = document.getElementById("journal-root"); if (!root) throw new Error("journal: #journal-root missing"); diff --git a/apps/mailbox/src/main.tsx b/apps/mailbox/src/main.tsx index dd4f428f..916f807f 100644 --- a/apps/mailbox/src/main.tsx +++ b/apps/mailbox/src/main.tsx @@ -1,4 +1,5 @@ import "@brainstorm/sdk/app-theme.css"; +import { initAnalytics } from "@brainstorm/sdk/analytics"; import "@brainstorm/sdk/count-badge.css"; import "@brainstorm/sdk/empty-state.css"; import { AppErrorBoundary } from "@brainstorm/sdk/error-boundary"; @@ -14,6 +15,8 @@ import { MailboxWidget } from "./widget"; // through openAnchoredMenu, which is a no-op without a mounted host. mountMenuHost(); +initAnalytics(); + const root = document.getElementById("root"); if (!root) throw new Error("Mailbox: #root not found in index.html"); // No : dev double-mount re-binds @lexical/yjs editors to an diff --git a/apps/notes/src/main.tsx b/apps/notes/src/main.tsx index bb3d5431..546d7a8d 100644 --- a/apps/notes/src/main.tsx +++ b/apps/notes/src/main.tsx @@ -1,4 +1,5 @@ import "@brainstorm/sdk/app-theme.css"; +import { initAnalytics } from "@brainstorm/sdk/analytics"; import "@brainstorm/sdk/empty-state.css"; import "@brainstorm/sdk/share-dialog.css"; import { mountMenuHost } from "@brainstorm/sdk/menus"; @@ -21,6 +22,8 @@ import { getYDocResolverApi } from "./store/ydoc-resolver"; import { NotesWidget } from "./widget"; import "./styles.css"; +initAnalytics(); + const root = document.getElementById("root"); if (!root) throw new Error("Notes: #root not found in index.html"); diff --git a/apps/preview/src/main.tsx b/apps/preview/src/main.tsx index 78f2ca55..d295a8d4 100644 --- a/apps/preview/src/main.tsx +++ b/apps/preview/src/main.tsx @@ -1,4 +1,5 @@ import "@brainstorm/sdk/app-theme.css"; +import { initAnalytics } from "@brainstorm/sdk/analytics"; import "@brainstorm/sdk/empty-state.css"; import "@brainstorm/editor/editor.css"; import { AppErrorBoundary } from "@brainstorm/sdk/error-boundary"; @@ -10,6 +11,8 @@ import { PreviewI18nProvider } from "./i18n-provider"; import { registerBuiltInPreviewModules } from "./logic/registry"; import "./styles.css"; +initAnalytics(); + const root = document.getElementById("root"); if (!root) throw new Error("preview: #root not found in index.html"); diff --git a/apps/tasks/src/main.tsx b/apps/tasks/src/main.tsx index 74184633..e57ad6e3 100644 --- a/apps/tasks/src/main.tsx +++ b/apps/tasks/src/main.tsx @@ -14,6 +14,7 @@ */ import "@brainstorm/sdk/app-theme.css"; +import { initAnalytics } from "@brainstorm/sdk/analytics"; import "@brainstorm/sdk/empty-state.css"; import "@brainstorm/editor/editor.css"; import "@brainstorm/sdk/recurrence-editor.css"; @@ -45,6 +46,8 @@ import { getBrainstorm } from "./storage/runtime"; import { mountTasksWidget } from "./widget-mount"; import "./styles.css"; +initAnalytics(); + // Widget-mode (Stage 7.3a): the dashboard launched this bundle as a widget. // Mount the compact widget surface (open-tasks glance list) instead of the full // Tasks app, and skip the full-app bootstrap below. diff --git a/apps/theme-editor/src/main.tsx b/apps/theme-editor/src/main.tsx index 5221d714..de2b2d0e 100644 --- a/apps/theme-editor/src/main.tsx +++ b/apps/theme-editor/src/main.tsx @@ -1,4 +1,5 @@ import "@brainstorm/sdk/app-theme.css"; +import { initAnalytics } from "@brainstorm/sdk/analytics"; import "@brainstorm/sdk/color-picker.css"; import { AppErrorBoundary } from "@brainstorm/sdk/error-boundary"; import { mountMenuHost } from "@brainstorm/sdk/menus"; @@ -8,6 +9,8 @@ import { ThemeEditorApp } from "./app"; import { ThemeEditorI18nProvider } from "./i18n-provider"; import "./styles.css"; +initAnalytics(); + const root = document.getElementById("root"); if (!root) throw new Error("theme-editor: #root not found in index.html"); diff --git a/apps/whiteboard/src/main.tsx b/apps/whiteboard/src/main.tsx index 83b7087e..99ee902b 100644 --- a/apps/whiteboard/src/main.tsx +++ b/apps/whiteboard/src/main.tsx @@ -1,10 +1,13 @@ import "@brainstorm/sdk/app-theme.css"; +import { initAnalytics } from "@brainstorm/sdk/analytics"; import { AppErrorBoundary } from "@brainstorm/sdk/error-boundary"; import { StrictMode } from "react"; import { createRoot } from "react-dom/client"; import { WhiteboardApp } from "./app"; import "./styles.css"; +initAnalytics(); + const root = document.getElementById("root"); if (!root) throw new Error("whiteboard: #root not found in index.html"); createRoot(root).render( diff --git a/bun.lock b/bun.lock index 99818284..99284454 100644 --- a/bun.lock +++ b/bun.lock @@ -551,6 +551,7 @@ "name": "@brainstorm/sdk", "version": "0.0.1", "dependencies": { + "@amplitude/unified": "^1.1.22", "@brainstorm/react-yjs": "workspace:*", "@brainstorm/sdk-types": "workspace:*", "@phosphor-icons/core": "^2.1.1", @@ -581,7 +582,7 @@ }, "packages/shell": { "name": "@brainstorm/shell", - "version": "0.3.1", + "version": "0.4.2", "dependencies": { "@blockprotocol/core": "0.1.4", "@blockprotocol/graph": "0.4.2", @@ -646,6 +647,66 @@ "packages": { "7zip-bin": ["7zip-bin@5.2.0", "", {}, "sha512-ukTPVhqG4jNzMro2qA9HSCSSVJN3aN7tlb+hfqYCt3ER0yWroeA2VR38MNrOHLQ/cVj+DaIMad0kFCtWWowh/A=="], + "@amplitude/analytics-browser": ["@amplitude/analytics-browser@2.44.6", "", { "dependencies": { "@amplitude/analytics-core": "2.52.2", "@amplitude/plugin-autocapture-browser": "1.28.4", "@amplitude/plugin-custom-enrichment-browser": "0.1.15", "@amplitude/plugin-event-property-attribution-browser": "0.2.7", "@amplitude/plugin-network-capture-browser": "1.10.7", "@amplitude/plugin-page-url-enrichment-browser": "0.7.17", "@amplitude/plugin-page-view-tracking-browser": "2.11.7", "@amplitude/plugin-web-vitals-browser": "1.1.39", "tslib": "^2.4.1" } }, "sha512-8K3PNmfPTzgIEiMDxY0+w8laXqlZ5PZk6mwRDCTQqTZN2xbLkApUelEgffqld5JtVX9AgeS2ysYC5iWyulD6vg=="], + + "@amplitude/analytics-client-common": ["@amplitude/analytics-client-common@2.4.54", "", { "dependencies": { "@amplitude/analytics-connector": "^1.4.8", "@amplitude/analytics-core": "2.52.2", "@amplitude/analytics-types": "2.11.1", "tslib": "^2.4.1" } }, "sha512-km/pwURJx4ARvOmN2h48GvUIvHFxzta8gC/GMS9q2P/dCY5dlblyLDAsO39WXFIEbbQbouJu+LnfX0RPNmjg+A=="], + + "@amplitude/analytics-connector": ["@amplitude/analytics-connector@1.6.5", "", {}, "sha512-EcDT+E4vliT7WAeOCbyE4rNkTUdLMR9dGCETX+36U7gAROr8jARG6ObhW7TTWdoYDlUe25IllZnRKdXHRKlWJQ=="], + + "@amplitude/analytics-core": ["@amplitude/analytics-core@2.52.2", "", { "dependencies": { "@amplitude/analytics-connector": "^1.6.4", "@types/zen-observable": "0.8.3", "safe-json-stringify": "1.2.0", "tslib": "^2.4.1", "zen-observable": "0.10.0" } }, "sha512-KFwDcPnpp1gqnK38XWyHh2QVvfzVWAN4SwYS3xHQgH3PoVZGLhSvbvNje4lKOaSSQB3J3swpHGeS/c+ENAH4Fw=="], + + "@amplitude/analytics-types": ["@amplitude/analytics-types@1.4.0", "", {}, "sha512-RiMPHBqdrJ8ktTqG+Wzj2htnN/PCG9jGZG0SXtTFnWwVvcAJYbYm55/nrP1TTyrx1OlLhvF2VG3lVUP/xGAU8w=="], + + "@amplitude/element-selector": ["@amplitude/element-selector@0.2.0", "", { "dependencies": { "tslib": "^2.4.1" } }, "sha512-xT+EuBpEdjDPn6t90wYF2Tr+mr/wqK4bK7RHV6Oykr/vNsOt7/JP31atdH6LR0WbSP4hV75pTTya/DnbFTA6bA=="], + + "@amplitude/engagement-browser": ["@amplitude/engagement-browser@1.0.11", "", { "dependencies": { "@amplitude/analytics-types": "^1.0.0" } }, "sha512-hGO0ArhnO+jqYyerLmHT21+PQ957sqsJtmNgO+55tpeU80Jl6nsjc0icAelLGKK01m5Z4QzWQ3rzh1Ocxlq+/w=="], + + "@amplitude/experiment-core": ["@amplitude/experiment-core@0.13.3", "", { "dependencies": { "js-base64": "^3.7.5" } }, "sha512-iBFaiTr5BYiUAe8NcAnJ39pe1vqPHq3638E6FTYocoRIh5zryzYQvfqE7mhDNcCj7OUm4IJ3DFq8EVNqqlcnUw=="], + + "@amplitude/experiment-js-client": ["@amplitude/experiment-js-client@1.21.3", "", { "dependencies": { "@amplitude/analytics-connector": "^1.6.5", "@amplitude/experiment-core": "^0.13.3", "@amplitude/ua-parser-js": "^0.7.31", "base64-js": "1.5.1", "unfetch": "4.1.0" } }, "sha512-YAw6K2GR4d18fdRLrNiEnuH0+5bTV9cIn/b04MA4GfxUHnbOI0ZmQTtqj6AfU4myiBjTBLPRH7+rlMRMnLe8iQ=="], + + "@amplitude/plugin-autocapture-browser": ["@amplitude/plugin-autocapture-browser@1.28.4", "", { "dependencies": { "@amplitude/analytics-core": "2.52.2", "@amplitude/element-selector": "0.2.0", "tslib": "^2.4.1" } }, "sha512-hMrlTCTHb+gev/isEijEw91NWgLB0AMe5Fke+efXZhlKgInyzKXFznIs4/ppG75+kac0v92LX2edpAYzcvzM3g=="], + + "@amplitude/plugin-custom-enrichment-browser": ["@amplitude/plugin-custom-enrichment-browser@0.1.15", "", { "dependencies": { "@amplitude/analytics-core": "2.52.2", "tslib": "^2.4.1" } }, "sha512-VtVQteVmwF4A+WX19kPHb/R+8E/lftrKlQhxxLv8NlVnIX8yyx9sJmMJMLolzebtlVoIPMW+bkGP0yV3J2eFKw=="], + + "@amplitude/plugin-event-property-attribution-browser": ["@amplitude/plugin-event-property-attribution-browser@0.2.7", "", { "dependencies": { "@amplitude/analytics-core": "2.52.2", "tslib": "^2.4.1" } }, "sha512-zgGsvJjfMjzdkfu2orTwJTEEs+6aJWdQQb3jSFfnLA1j6VOf/SYuyLFcmXOyrMGSTukYnHLIWKF92Yg9c9miXQ=="], + + "@amplitude/plugin-experiment-browser": ["@amplitude/plugin-experiment-browser@1.0.0-beta.34", "", { "dependencies": { "@amplitude/analytics-core": "2.52.2", "@amplitude/experiment-js-client": "^1.15.5" } }, "sha512-Q1EI3OWDrtMVv/QT/TUGOIYgpvnMe+CEJBwSh+xewN0jbjbDLCdbW/vePZjfquI96DrBKjjigVzLawSKyqFBNw=="], + + "@amplitude/plugin-network-capture-browser": ["@amplitude/plugin-network-capture-browser@1.10.7", "", { "dependencies": { "@amplitude/analytics-core": "2.52.2", "tslib": "^2.4.1" } }, "sha512-G3VzR8bpJO7TwBCDMqjZFIffrPzz8a0sOqcQnTYVdg119rPgATDqD/P9cU7g3iOwWEb6NQkOc3J34U2FbfSBHQ=="], + + "@amplitude/plugin-page-url-enrichment-browser": ["@amplitude/plugin-page-url-enrichment-browser@0.7.17", "", { "dependencies": { "@amplitude/analytics-core": "2.52.2", "tslib": "^2.4.1" } }, "sha512-27XtZVsROAFOqRMu+/ojN38r+FQbpmPoAe8pq5nTw+07qtCj30TdFb7XNhJ0uVYyRq/SZCMupY95IFpyxNAoiQ=="], + + "@amplitude/plugin-page-view-tracking-browser": ["@amplitude/plugin-page-view-tracking-browser@2.11.7", "", { "dependencies": { "@amplitude/analytics-core": "2.52.2", "tslib": "^2.4.1" } }, "sha512-qTvyMxvIVLB4U/aNKZ63tcnwTnFCSnQ5LXOrpBZBv9IY4NLXjjdxTPgUUG1ECp1/buD52M7sYbE5shfA+ixKhA=="], + + "@amplitude/plugin-session-replay-browser": ["@amplitude/plugin-session-replay-browser@1.33.2", "", { "dependencies": { "@amplitude/analytics-client-common": "2.4.54", "@amplitude/analytics-core": "2.52.2", "@amplitude/analytics-types": "2.11.1", "@amplitude/rrweb-plugin-console-record": "2.0.0-alpha.40", "@amplitude/rrweb-record": "2.0.0-alpha.40", "@amplitude/session-replay-browser": "1.47.2", "idb-keyval": "^6.2.1", "tslib": "^2.4.1" } }, "sha512-htLkMgl2NW6HH2qFXDCmFFpPVPa9UK4D+6p2BjIvxIwhaX7W1ei86ms7zfnYmebVQLuR2FKvmyM5wYsJAxng+A=="], + + "@amplitude/plugin-web-vitals-browser": ["@amplitude/plugin-web-vitals-browser@1.1.39", "", { "dependencies": { "@amplitude/analytics-core": "2.52.2", "tslib": "^2.4.1", "web-vitals": "5.1.0" } }, "sha512-0Sb4EPppzWQ+TRSzBR467QuRNbvReM09dIGyu1bERMuas31a5y/GHghTnjB7OmbNLxzZtDGtDCcTFPSlYHszvw=="], + + "@amplitude/rrdom": ["@amplitude/rrdom@2.1.2", "", { "dependencies": { "@amplitude/rrweb-snapshot": "^2.1.2" } }, "sha512-jIjDnuZGW7f4Nb7jxbQG7wz4bcS6rIkHeneLgF5EFj5o7O2kJu7tw0z7PND9eo982nSswiHb56mkAfGcue911Q=="], + + "@amplitude/rrweb": ["@amplitude/rrweb@2.1.3", "", { "dependencies": { "@amplitude/rrdom": "^2.1.2", "@amplitude/rrweb-snapshot": "^2.1.2", "@amplitude/rrweb-types": "^2.1.2", "@amplitude/rrweb-utils": "^2.1.2", "@types/css-font-loading-module": "0.0.7", "@xstate/fsm": "^1.4.0", "base64-arraybuffer": "^1.0.1", "mitt": "^3.0.0" } }, "sha512-K/+ob14aDZ3tYhW/PAz4mQp+Ikr8H7qvepGgNRPhm4bG3sztsdnWhz+eK7xEcx05l+RIo0MYAp3+GXE5mDYM9w=="], + + "@amplitude/rrweb-packer": ["@amplitude/rrweb-packer@2.0.0-alpha.40", "", { "dependencies": { "@amplitude/rrweb-types": "^2.0.0-alpha.40", "fflate": "^0.4.4" } }, "sha512-Btb6b9pS1IvDMbvyYxpUdTk9NRJugSoJjRCl7R6jP/iSlPWXoveJIwHaNFAS9ZmWUEK7HhyBJ8bKGFN3giUsDg=="], + + "@amplitude/rrweb-plugin-console-record": ["@amplitude/rrweb-plugin-console-record@2.0.0-alpha.40", "", { "peerDependencies": { "@amplitude/rrweb": "^2.0.0-alpha.40" } }, "sha512-vtY7T/kGFl62nC1u7ZUXQvU7ulB70cZGVHPRN/SO9fzVfsY7y6rCmBfoc2jS5KmISdlgkVzMjY2r/EE2Gk9AQA=="], + + "@amplitude/rrweb-record": ["@amplitude/rrweb-record@2.0.0-alpha.40", "", { "dependencies": { "@amplitude/rrweb": "^2.0.0-alpha.40", "@amplitude/rrweb-types": "^2.0.0-alpha.40" } }, "sha512-5cJhQwzhymJWX5/XOtpWK0h2NLq9+t2YiO6ub0cdZ9F5AZizaRbsVH88int07DfX0YiXTKWbISezVuduCLqgSQ=="], + + "@amplitude/rrweb-snapshot": ["@amplitude/rrweb-snapshot@2.1.2", "", { "dependencies": { "postcss": "^8.4.38" } }, "sha512-xdASm3wP2hHnEZL6Y0+uFO7rBCRYadKIRLwVJewpln+BHj+F96GGfw2JmsZx4J/5a+kXRFgtajp9mEboy3dyxw=="], + + "@amplitude/rrweb-types": ["@amplitude/rrweb-types@2.1.2", "", {}, "sha512-HvzZpWGigXGXmtGntyDoNbk6i2YO89wrFV1tkILns7R4ECntsNsXzwP72oyWYVpONvU/x0xM5oe6/rzv4L29BQ=="], + + "@amplitude/rrweb-utils": ["@amplitude/rrweb-utils@2.0.0-alpha.40", "", {}, "sha512-i1CCt6MCjlqoeNc+1Hse5bz+ZbASaWaIJ0WdJZvnQjUCHH29Xy/QFouyOuor73RZ+UWX4s2tYSrUIdmBepXk3w=="], + + "@amplitude/session-replay-browser": ["@amplitude/session-replay-browser@1.47.2", "", { "dependencies": { "@amplitude/analytics-client-common": "2.4.54", "@amplitude/analytics-core": "2.52.2", "@amplitude/analytics-types": "2.11.1", "@amplitude/experiment-core": "0.7.2", "@amplitude/rrweb-packer": "2.0.0-alpha.40", "@amplitude/rrweb-plugin-console-record": "2.0.0-alpha.40", "@amplitude/rrweb-record": "2.0.0-alpha.40", "@amplitude/rrweb-types": "2.0.0-alpha.40", "@amplitude/rrweb-utils": "2.0.0-alpha.40", "@amplitude/targeting": "0.3.4", "@rollup/plugin-replace": "^6.0.1", "idb": "8.0.0", "tslib": "^2.4.1" } }, "sha512-AfbGdCUS3sMvc09KNWxUh+wKXi2u6c2e1vm90yAbw9XbpPMJBvBhwBgnJtCTb5o8YPsw3x5ZmiEBhtc5vOT1mA=="], + + "@amplitude/targeting": ["@amplitude/targeting@0.3.4", "", { "dependencies": { "@amplitude/analytics-client-common": "2.4.54", "@amplitude/analytics-core": "2.52.2", "@amplitude/analytics-types": "2.11.1", "@amplitude/experiment-core": "0.7.2", "idb": "8.0.0", "tslib": "^2.4.1" } }, "sha512-KyEQXpO9U0pvNyiNKmPjRZmTyliclklLp1Q8xW3N2mQysOt8KAB7Wcd6Y/VW+xHeLv256YC4qevUzHEiyYmhVg=="], + + "@amplitude/ua-parser-js": ["@amplitude/ua-parser-js@0.7.33", "", {}, "sha512-wKEtVR4vXuPT9cVEIJkYWnlF++Gx3BdLatPBM+SZ1ztVIvnhdGBZR/mn9x/PzyrMcRlZmyi6L56I2J3doVBnjA=="], + + "@amplitude/unified": ["@amplitude/unified@1.1.22", "", { "dependencies": { "@amplitude/analytics-browser": "2.44.6", "@amplitude/analytics-core": "2.52.2", "@amplitude/engagement-browser": "^1.0.10", "@amplitude/plugin-experiment-browser": "1.0.0-beta.34", "@amplitude/plugin-session-replay-browser": "1.33.2" } }, "sha512-we1oYnEaNpA07ItDMhSoDmuL/KJ0W946oAdZEAF0R4nF2xMq69BLznN2RPOGYiKJTfPIK8XX7xsb4AvWeJfV/A=="], + "@apidevtools/json-schema-ref-parser": ["@apidevtools/json-schema-ref-parser@11.9.3", "", { "dependencies": { "@jsdevtools/ono": "^7.1.3", "@types/json-schema": "^7.0.15", "js-yaml": "^4.1.0" } }, "sha512-60vepv88RwcJtSHrD6MjIL6Ta3SOYbgfnkHb+ppAVK+o9mXprRtulx7VlRl3lN3bbvysAfCS7WMVfhUYemB0IQ=="], "@asamuzakjp/css-color": ["@asamuzakjp/css-color@5.1.11", "", { "dependencies": { "@asamuzakjp/generational-cache": "^1.0.1", "@csstools/css-calc": "^3.2.0", "@csstools/css-color-parser": "^4.1.0", "@csstools/css-parser-algorithms": "^4.0.0", "@csstools/css-tokenizer": "^4.0.0" } }, "sha512-KVw6qIiCTUQhByfTd78h2yD1/00waTmm9uy/R7Ck/ctUyAPj+AEDLkQIdJW0T8+qGgj3j5bpNKK7Q3G+LedJWg=="], @@ -1296,6 +1357,10 @@ "@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.0-beta.27", "", {}, "sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA=="], + "@rollup/plugin-replace": ["@rollup/plugin-replace@6.0.3", "", { "dependencies": { "@rollup/pluginutils": "^5.0.1", "magic-string": "^0.30.3" }, "peerDependencies": { "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" }, "optionalPeers": ["rollup"] }, "sha512-J4RZarRvQAm5IF0/LwUUg+obsm+xZhYnbMXmXROyoSE1ATJe3oXSb9L5MMppdxP2ylNSjv6zFBwKYjcKMucVfA=="], + + "@rollup/pluginutils": ["@rollup/pluginutils@5.4.0", "", { "dependencies": { "@types/estree": "^1.0.0", "estree-walker": "^2.0.2", "picomatch": "^4.0.2" }, "peerDependencies": { "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" }, "optionalPeers": ["rollup"] }, "sha512-MfPp06CjRLfXQ3wY0R8vJDYBy/MvVcc9OulEfR0B8Iv9ko+GCNaRZ+EpJYFl27LhKsZK0o420sYCRHCjfCgeUg=="], + "@rollup/rollup-android-arm-eabi": ["@rollup/rollup-android-arm-eabi@4.60.4", "", { "os": "android", "cpu": "arm" }, "sha512-F5QXMSiFebS9hKZj02XhWLLnRpJ3B3AROP0tWbFBSj+6kCbg5m9j5JoHKd4mmSVy5mS/IMQloYgYxCuJC0fxEQ=="], "@rollup/rollup-android-arm64": ["@rollup/rollup-android-arm64@4.60.4", "", { "os": "android", "cpu": "arm64" }, "sha512-GxxTKApUpzRhof7poWvCJHRF51C67u1R7D6DiluBE8wKU1u5GWE8t+v81JvJYtbawoBFX1hLv5Ei4eVjkWokaw=="], @@ -1392,6 +1457,8 @@ "@types/chai": ["@types/chai@5.2.3", "", { "dependencies": { "@types/deep-eql": "*", "assertion-error": "^2.0.1" } }, "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA=="], + "@types/css-font-loading-module": ["@types/css-font-loading-module@0.0.7", "", {}, "sha512-nl09VhutdjINdWyXxHWN/w9zlNCfr60JUqJbd24YXUuCwgeL0TpFSdElCwb6cxfB6ybE19Gjj4g0jsgkXxKv1Q=="], + "@types/debug": ["@types/debug@4.1.13", "", { "dependencies": { "@types/ms": "*" } }, "sha512-KSVgmQmzMwPlmtljOomayoR89W4FynCAi3E8PPs7vmDVPe84hT+vGPKkJfThkmXs0x0jAaa9U8uW8bbfyS2fWw=="], "@types/deep-eql": ["@types/deep-eql@4.0.2", "", {}, "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw=="], @@ -1464,6 +1531,8 @@ "@types/yauzl": ["@types/yauzl@2.10.3", "", { "dependencies": { "@types/node": "*" } }, "sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q=="], + "@types/zen-observable": ["@types/zen-observable@0.8.3", "", {}, "sha512-fbF6oTd4sGGy0xjHPKAt+eS2CrxJ3+6gQ3FGcBoIJR2TLAyCkCyI8JqZNy+FeON0AhVgNJoUumVoZQjBFUqHkw=="], + "@ungap/structured-clone": ["@ungap/structured-clone@1.3.1", "", {}, "sha512-mUFwbeTqrVgDQxFveS+df2yfap6iuP20NAKAsBt5jDEoOTDew+zwLAOilHCeQJOVSvmgCX4ogqIrA0mnyr08yQ=="], "@vitejs/plugin-react": ["@vitejs/plugin-react@4.7.0", "", { "dependencies": { "@babel/core": "^7.28.0", "@babel/plugin-transform-react-jsx-self": "^7.27.1", "@babel/plugin-transform-react-jsx-source": "^7.27.1", "@rolldown/pluginutils": "1.0.0-beta.27", "@types/babel__core": "^7.20.5", "react-refresh": "^0.17.0" }, "peerDependencies": { "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" } }, "sha512-gUu9hwfWvvEDBBmgtAowQCojwZmJ5mcLn3aufeCsitijs3+f2NsrPtlAWIR6OPiqljl96GVCUbLe0HyqIpVaoA=="], @@ -1488,6 +1557,8 @@ "@xmldom/xmldom": ["@xmldom/xmldom@0.7.13", "", {}, "sha512-lm2GW5PkosIzccsaZIz7tp8cPADSIlIHWDFTR1N0SzfinhhYgeIQjFMz4rYzanCScr3DqQLeomUDArp6MWKm+g=="], + "@xstate/fsm": ["@xstate/fsm@1.6.5", "", {}, "sha512-b5o1I6aLNeYlU/3CPlj/Z91ybk1gUsKT+5NAJI+2W4UjvS5KLG28K9v5UvNoFVjHV8PajVZ00RH3vnjyQO7ZAw=="], + "@zone-eu/mailsplit": ["@zone-eu/mailsplit@5.4.14", "", { "dependencies": { "libbase64": "1.3.0", "libmime": "5.4.1", "libqp": "2.1.1" } }, "sha512-rz0FQOhN3Vq1XrSeSSa9+dPcaFbBxmQPjiZm6zS9oxdVHV7rOWIAYX3yP2YAUf0qBncY8CI+NogzPCmMVrMXcw=="], "abbrev": ["abbrev@4.0.0", "", {}, "sha512-a1wflyaL0tHtJSmLSOVybYhy22vRih4eduhhrkcjgrWGnRfrZtovJ2FRjxuTtkkj47O/baf0R86QU5OuYpz8fA=="], @@ -1532,6 +1603,8 @@ "balanced-match": ["balanced-match@4.0.4", "", {}, "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA=="], + "base64-arraybuffer": ["base64-arraybuffer@1.0.2", "", {}, "sha512-I3yl4r9QB5ZRY3XuJVEPfc2XhZO6YweFPI+UovAzn+8/hb3oJ6lnysaFcjVpkCPfVWFUDvoZ8kmVDP7WyRtYtQ=="], + "base64-js": ["base64-js@1.5.1", "", {}, "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA=="], "baseline-browser-mapping": ["baseline-browser-mapping@2.10.32", "", { "bin": { "baseline-browser-mapping": "dist/cli.cjs" } }, "sha512-wbPvpyjJPC0zdfdKXxqEL3Ea+bOMD/87X4lftiJkkaBiuG6ALQy1SLmEd7BSmVCuwCQsBrCamgBoLyfFDD1EPg=="], @@ -1920,6 +1993,10 @@ "iconv-lite": ["iconv-lite@0.6.3", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw=="], + "idb": ["idb@8.0.0", "", {}, "sha512-l//qvlAKGmQO31Qn7xdzagVPPaHTxXx199MhrAFuVBTPqydcPYBWjkrbv4Y0ktB+GmWOiwHl237UUOrLmQxLvw=="], + + "idb-keyval": ["idb-keyval@6.3.0", "", {}, "sha512-um+2dgAWmYsu615EXpWVwSmapJhON0G43t3Ka/EVaohzPQXSMqKEqeDK/oIW3Ow+BXaF2PvSc+oBTFp793A5Ow=="], + "ieee754": ["ieee754@1.2.1", "", {}, "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA=="], "imapflow": ["imapflow@1.4.6", "", { "dependencies": { "@zone-eu/mailsplit": "5.4.14", "encoding-japanese": "2.2.0", "iconv-lite": "0.7.3", "libbase64": "1.3.0", "libmime": "5.4.1", "libqp": "2.1.1", "nodemailer": "9.0.3", "pino": "10.3.1", "socks": "2.8.9" } }, "sha512-rStOxV2g1WKKSkctKluUHSJB99mRT2l8Dr0W67owCHsCjqYSMY+AnvzXbaG3NGSpRziu6PWNik1/nVwL0Zlycg=="], @@ -1968,6 +2045,8 @@ "jiti": ["jiti@2.7.0", "", { "bin": { "jiti": "lib/jiti-cli.mjs" } }, "sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ=="], + "js-base64": ["js-base64@3.8.1", "", {}, "sha512-5xVjhUZlHHeuO2W7w2rDFj/Kl1xLX+HjZxdOQwCsUOifl6UaoH1o1wsbsTMz+r0aeC7gCijvru02j6TfKZWzKg=="], + "js-binary-schema-parser": ["js-binary-schema-parser@2.0.3", "", {}, "sha512-xezGJmOb4lk/M1ZZLTR/jaBHQ4gG/lqQnJqdIv4721DMggsa1bDVlHXNeHYogaIEHD9vCRv0fcL4hMA+Coarkg=="], "js-tokens": ["js-tokens@10.0.0", "", {}, "sha512-lM/UBzQmfJRo9ABXbPWemivdCW8V2G8FHaHdypQaIy523snUjog0W71ayWXTjiR+ixeMyVHN2XcpnTd/liPg/Q=="], @@ -2120,6 +2199,8 @@ "minizlib": ["minizlib@3.1.0", "", { "dependencies": { "minipass": "^7.1.2" } }, "sha512-KZxYo1BUkWD2TVFLr0MQoM8vUUigWD3LlD83a/75BqC+4qE0Hb1Vo5v1FgcfaNXvfXzr+5EhQ6ing/CaBijTlw=="], + "mitt": ["mitt@3.0.1", "", {}, "sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw=="], + "mkdirp": ["mkdirp@1.0.4", "", { "bin": { "mkdirp": "bin/cmd.js" } }, "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw=="], "mkdirp-classic": ["mkdirp-classic@0.5.3", "", {}, "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A=="], @@ -2312,6 +2393,8 @@ "safe-buffer": ["safe-buffer@5.1.2", "", {}, "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g=="], + "safe-json-stringify": ["safe-json-stringify@1.2.0", "", {}, "sha512-gH8eh2nZudPQO6TytOvbxnuhYBOvDBBLW52tz5q6X58lJcd/tkmqFR+5Z9adS8aJtURSXWThWy/xJtJwixErvg=="], + "safe-stable-stringify": ["safe-stable-stringify@2.5.0", "", {}, "sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA=="], "safer-buffer": ["safer-buffer@2.1.2", "", {}, "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="], @@ -2490,6 +2573,8 @@ "undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="], + "unfetch": ["unfetch@4.1.0", "", {}, "sha512-crP/n3eAPUJxZXM9T80/yv0YhkTEx2K1D3h7D1AJM6fzsWZrxdyRuLN0JH/dkZh1LNH8LxCnBzoPFCPbb2iGpg=="], + "unicode-emoji-json": ["unicode-emoji-json@0.9.0", "", {}, "sha512-HdrQW/7SdbXUsTd8uTATv7LvQJkCSgbAgRCDCtOhQ6xn3EAx+xZHQtRniOXWJK3ywIqPegkNhP4U4EbuFsiQng=="], "unist-util-is": ["unist-util-is@6.0.1", "", { "dependencies": { "@types/unist": "^3.0.0" } }, "sha512-LsiILbtBETkDz8I9p1dQ0uyRUWuaQzd/cuEeS1hoRSyW5E5XGmTzlwY1OrNzzakGowI9Dr/I8HVaw4hTtnxy8g=="], @@ -2530,6 +2615,8 @@ "w3c-xmlserializer": ["w3c-xmlserializer@5.0.0", "", { "dependencies": { "xml-name-validator": "^5.0.0" } }, "sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA=="], + "web-vitals": ["web-vitals@5.1.0", "", {}, "sha512-ArI3kx5jI0atlTtmV0fWU3fjpLmq/nD3Zr1iFFlJLaqa5wLBkUSzINwBPySCX/8jRyjlmy1Volw1kz1g9XE4Jg=="], + "webidl-conversions": ["webidl-conversions@8.0.1", "", {}, "sha512-BMhLD/Sw+GbJC21C/UgyaZX41nPt8bUTg+jWyDeg7e7YN4xOM05YPSIXceACnXVtqyEw/LMClUQMtMZ+PGGpqQ=="], "whatwg-mimetype": ["whatwg-mimetype@3.0.0", "", {}, "sha512-nt+N2dzIutVRxARx1nghPKGv1xHikU7HKdfafKkLNLindmPU/ch3U31NOCGGA/dmPcmb1VlofO0vnKAcsm0o/Q=="], @@ -2572,8 +2659,28 @@ "yocto-queue": ["yocto-queue@0.1.0", "", {}, "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q=="], + "zen-observable": ["zen-observable@0.10.0", "", {}, "sha512-iI3lT0iojZhKwT5DaFy2Ce42n3yFcLdFyOh01G7H0flMY60P8MJuVFEoJoNwXlmAyQ45GrjL6AcZmmlv8A5rbw=="], + "zwitch": ["zwitch@2.0.4", "", {}, "sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A=="], + "@amplitude/analytics-client-common/@amplitude/analytics-types": ["@amplitude/analytics-types@2.11.1", "", {}, "sha512-wFEgb0t99ly2uJKm5oZ28Lti0Kh5RecR5XBkwfUpDzn84IoCIZ8GJTsMw/nThu8FZFc7xFDA4UAt76zhZKrs9A=="], + + "@amplitude/plugin-session-replay-browser/@amplitude/analytics-types": ["@amplitude/analytics-types@2.11.1", "", {}, "sha512-wFEgb0t99ly2uJKm5oZ28Lti0Kh5RecR5XBkwfUpDzn84IoCIZ8GJTsMw/nThu8FZFc7xFDA4UAt76zhZKrs9A=="], + + "@amplitude/rrweb/@amplitude/rrweb-utils": ["@amplitude/rrweb-utils@2.1.2", "", {}, "sha512-FRiJj7h1ziiWqW9hbmr1PfQoCgpX7zxhqVdlVaFE9NkqzXlePmStjOgkBLNnz/WbBD0A4pZp26o9Cy0he47Qwg=="], + + "@amplitude/rrweb-packer/fflate": ["fflate@0.4.8", "", {}, "sha512-FJqqoDBR00Mdj9ppamLa/Y7vxm+PRmNWA67N846RvsoYVMKB4q3y/de5PA7gUmRMYK/8CMz2GDZQmCRN1wBcWA=="], + + "@amplitude/session-replay-browser/@amplitude/analytics-types": ["@amplitude/analytics-types@2.11.1", "", {}, "sha512-wFEgb0t99ly2uJKm5oZ28Lti0Kh5RecR5XBkwfUpDzn84IoCIZ8GJTsMw/nThu8FZFc7xFDA4UAt76zhZKrs9A=="], + + "@amplitude/session-replay-browser/@amplitude/experiment-core": ["@amplitude/experiment-core@0.7.2", "", { "dependencies": { "js-base64": "^3.7.5" } }, "sha512-Wc2NWvgQ+bLJLeF0A9wBSPIaw0XuqqgkPKsoNFQrmS7r5Djd56um75In05tqmVntPJZRvGKU46pAp8o5tdf4mA=="], + + "@amplitude/session-replay-browser/@amplitude/rrweb-types": ["@amplitude/rrweb-types@2.0.0-alpha.40", "", {}, "sha512-rP7CBDkzXupxOA7ukvC+zDYLuCtsz54TuJKC4+5O72Jsz4YdokLznKZRG34P6zXozfhGU0261qckk87lLY6mKQ=="], + + "@amplitude/targeting/@amplitude/analytics-types": ["@amplitude/analytics-types@2.11.1", "", {}, "sha512-wFEgb0t99ly2uJKm5oZ28Lti0Kh5RecR5XBkwfUpDzn84IoCIZ8GJTsMw/nThu8FZFc7xFDA4UAt76zhZKrs9A=="], + + "@amplitude/targeting/@amplitude/experiment-core": ["@amplitude/experiment-core@0.7.2", "", { "dependencies": { "js-base64": "^3.7.5" } }, "sha512-Wc2NWvgQ+bLJLeF0A9wBSPIaw0XuqqgkPKsoNFQrmS7r5Djd56um75In05tqmVntPJZRvGKU46pAp8o5tdf4mA=="], + "@babel/code-frame/js-tokens": ["js-tokens@4.0.0", "", {}, "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="], "@babel/core/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], @@ -2620,6 +2727,8 @@ "@malept/flatpak-bundler/fs-extra": ["fs-extra@9.1.0", "", { "dependencies": { "at-least-node": "^1.0.0", "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", "universalify": "^2.0.0" } }, "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ=="], + "@rollup/pluginutils/estree-walker": ["estree-walker@2.0.2", "", {}, "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w=="], + "@tanstack/react-virtual/@tanstack/virtual-core": ["@tanstack/virtual-core@3.15.0", "", {}, "sha512-0AwPGx0I8QxPYjAxShT/+z+ZOe9u8mW5rsXvivCTjRfRmz9a43+3mRyi4wwlyoUqOC56q/jatKa0Bh9M99BEHQ=="], "@types/cacheable-request/@types/node": ["@types/node@24.12.4", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-GUUEShf+PBCGW2KaXwcIt3Yk+e3pkKwWKb9GSyM9WQVE+ep2jzmHdGsHzu4wgcZy5fN9FBdVzjpBQsYlpfpgLA=="], diff --git a/package.json b/package.json index 31e884f7..7ca0b3a6 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "brainstorm", - "version": "0.4.2", + "version": "0.4.3", "private": true, "description": "Brainstorm — knowledge-management product modeled as a desktop OS.", "license": "AGPL-3.0-or-later", diff --git a/packages/sdk/package.json b/packages/sdk/package.json index 68d77c6f..9a74d6e3 100644 --- a/packages/sdk/package.json +++ b/packages/sdk/package.json @@ -56,6 +56,7 @@ "./shortcut": "./src/shortcut/index.ts", "./nav-history": "./src/nav-history/index.ts", "./a11y": "./src/a11y/index.ts", + "./analytics": "./src/analytics/index.ts", "./date-pager": "./src/date-pager/index.ts", "./date-grid": "./src/date-grid/index.ts", "./calendar": "./src/calendar/index.ts", @@ -124,6 +125,7 @@ "./export-popover/export-popover.css": "./src/export-popover/export-popover.css" }, "dependencies": { + "@amplitude/unified": "^1.1.22", "@brainstorm/react-yjs": "workspace:*", "@brainstorm/sdk-types": "workspace:*", "@phosphor-icons/core": "^2.1.1", diff --git a/packages/sdk/src/analytics/analytics.test.ts b/packages/sdk/src/analytics/analytics.test.ts new file mode 100644 index 00000000..3fd0fda5 --- /dev/null +++ b/packages/sdk/src/analytics/analytics.test.ts @@ -0,0 +1,97 @@ +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; + +const initAll = vi.fn(); +const trackFn = vi.fn(); +const identifyFn = vi.fn(); + +vi.mock("@amplitude/unified", () => ({ + initAll, + track: trackFn, + identify: identifyFn, + Identify: class { + props = new Map(); + set(key: string, value: string) { + this.props.set(key, value); + return this; + } + }, +})); + +describe("initAnalytics", () => { + beforeEach(() => { + vi.resetModules(); + initAll.mockClear(); + trackFn.mockClear(); + identifyFn.mockClear(); + }); + + afterEach(() => { + // @ts-expect-error test cleanup + delete globalThis.window; + }); + + it("initializes Amplitude once with EU zone + autocapture + session replay", async () => { + globalThis.window = { + brainstorm: { version: "0.4.2", platform: "darwin" }, + } as unknown as Window & typeof globalThis; + + const { initAnalytics, track } = await import("./index"); + initAnalytics(); + initAnalytics(); + await vi.waitFor(() => expect(initAll).toHaveBeenCalledTimes(1)); + + expect(initAll).toHaveBeenCalledWith("691a93081b9b1af38116b0655eb17bd9", { + serverZone: "EU", + analytics: { autocapture: true }, + sessionReplay: { sampleRate: 1 }, + }); + expect(identifyFn).toHaveBeenCalledTimes(1); + expect(trackFn).toHaveBeenCalledWith("Application Started", expect.objectContaining({ + surface: "shell", + platform: "darwin", + shell_version: "0.4.2", + })); + + track("Vault Opened", { vault_id: "vault-1" }); + await vi.waitFor(() => + expect(trackFn).toHaveBeenCalledWith("Vault Opened", { vault_id: "vault-1" }), + ); + }); + + it("does not initialize on GA builds (1.0+)", async () => { + globalThis.window = { + brainstorm: { version: "1.0.0", platform: "darwin" }, + } as unknown as Window & typeof globalThis; + + const { initAnalytics, track } = await import("./index"); + initAnalytics(); + track("Vault Opened", { vault_id: "vault-1" }); + await new Promise((resolve) => setTimeout(resolve, 0)); + + expect(initAll).not.toHaveBeenCalled(); + expect(trackFn).not.toHaveBeenCalled(); + }); + + it("stamps app context when running inside an app renderer", async () => { + globalThis.window = { + brainstorm: { + version: "0.4.2", + platform: "win32", + app: { id: "io.brainstorm.notes", version: "1.2.0", sdkVersion: "1" }, + }, + } as unknown as Window & typeof globalThis; + + const { initAnalytics } = await import("./index"); + initAnalytics(); + await vi.waitFor(() => expect(trackFn).toHaveBeenCalled()); + + expect(trackFn).toHaveBeenCalledWith( + "Application Started", + expect.objectContaining({ + surface: "app", + app_id: "io.brainstorm.notes", + app_version: "1.2.0", + }), + ); + }); +}); \ No newline at end of file diff --git a/packages/sdk/src/analytics/beta-notice-storage.ts b/packages/sdk/src/analytics/beta-notice-storage.ts new file mode 100644 index 00000000..494e2b8c --- /dev/null +++ b/packages/sdk/src/analytics/beta-notice-storage.ts @@ -0,0 +1,22 @@ +/** Bump when the beta analytics copy changes so users see the new notice. */ +export const BETA_ANALYTICS_NOTICE_ID = "beta-analytics-v1"; + +const STORAGE_KEY = "brainstorm.analytics.betaNoticeDismissed"; + +export function hasDismissedBetaAnalyticsNotice(): boolean { + if (typeof window === "undefined") return true; + try { + return window.localStorage.getItem(STORAGE_KEY) === BETA_ANALYTICS_NOTICE_ID; + } catch { + return true; + } +} + +export function markBetaAnalyticsNoticeDismissed(): void { + if (typeof window === "undefined") return; + try { + window.localStorage.setItem(STORAGE_KEY, BETA_ANALYTICS_NOTICE_ID); + } catch { + // localStorage unavailable — the popover may reappear next launch. + } +} \ No newline at end of file diff --git a/packages/sdk/src/analytics/beta.test.ts b/packages/sdk/src/analytics/beta.test.ts new file mode 100644 index 00000000..55d69372 --- /dev/null +++ b/packages/sdk/src/analytics/beta.test.ts @@ -0,0 +1,14 @@ +import { describe, expect, it } from "vitest"; +import { isPublicBeta } from "./beta"; + +describe("isPublicBeta", () => { + it("treats 0.x as public beta", () => { + expect(isPublicBeta("0.4.2")).toBe(true); + expect(isPublicBeta("0.1.0-beta.1")).toBe(true); + }); + + it("treats 1.0+ as GA (analytics off)", () => { + expect(isPublicBeta("1.0.0")).toBe(false); + expect(isPublicBeta("2.3.1")).toBe(false); + }); +}); \ No newline at end of file diff --git a/packages/sdk/src/analytics/beta.ts b/packages/sdk/src/analytics/beta.ts new file mode 100644 index 00000000..02a3d21c --- /dev/null +++ b/packages/sdk/src/analytics/beta.ts @@ -0,0 +1,6 @@ +/** True while the product ships pre-1.0 (public beta). Analytics runs only then. */ +export function isPublicBeta(version: string): boolean { + const core = version.trim().split("-")[0] ?? ""; + const major = Number.parseInt(core.split(".")[0] ?? "", 10); + return !Number.isFinite(major) || major < 1; +} \ No newline at end of file diff --git a/packages/sdk/src/analytics/index.ts b/packages/sdk/src/analytics/index.ts new file mode 100644 index 00000000..798916e6 --- /dev/null +++ b/packages/sdk/src/analytics/index.ts @@ -0,0 +1,123 @@ +/** + * Client-side product analytics — Amplitude Browser SDK wrapper. + * Initializes once per renderer process; safe to call from every app entry. + * + * The SDK is dynamically imported so session replay + autocapture land in a + * separate chunk (the shell renderer's `index-*.js` size budget is tight). + */ + +import { isPublicBeta } from "./beta"; + +export { isPublicBeta } from "./beta"; +export { + BETA_ANALYTICS_NOTICE_ID, + hasDismissedBetaAnalyticsNotice, + markBetaAnalyticsNoticeDismissed, +} from "./beta-notice-storage"; + +const AMPLITUDE_API_KEY = "691a93081b9b1af38116b0655eb17bd9"; + +let initialized = false; +let analyticsEnabled = false; +let initPromise: Promise | null = null; +type AmplitudeModule = typeof import("@amplitude/unified"); +let amplitudeModule: AmplitudeModule | null = null; + +type AnalyticsBridge = { + version?: string; + platform?: string; + app?: { id?: string; version?: string }; +}; + +type AnalyticsContext = { + surface: "shell" | "app"; + appId?: string; + appVersion?: string; + platform?: string; + shellVersion?: string; +}; + +function readBridge(): AnalyticsBridge | null { + if (typeof window === "undefined") return null; + return (window as Window & { brainstorm?: AnalyticsBridge }).brainstorm ?? null; +} + +function readContext(): AnalyticsContext { + const bridge = readBridge(); + if (!bridge) return { surface: "shell" }; + const base: AnalyticsContext = { surface: "shell" }; + if (bridge.platform) base.platform = bridge.platform; + if (bridge.version) base.shellVersion = bridge.version; + const app = bridge.app; + if (!app?.id) return base; + const context: AnalyticsContext = { ...base, surface: "app", appId: app.id }; + if (app.version) context.appVersion = app.version; + return context; +} + +async function ensureInitialized(): Promise { + if (typeof window === "undefined") return null; + if (amplitudeModule) return amplitudeModule; + if (!initPromise) { + initPromise = (async () => { + const amplitude = await import("@amplitude/unified"); + amplitude.initAll(AMPLITUDE_API_KEY, { + serverZone: "EU", + analytics: { autocapture: true }, + sessionReplay: { sampleRate: 1 }, + }); + + const context = readContext(); + const userProps: Record = { surface: context.surface }; + if (context.platform) userProps.platform = context.platform; + if (context.shellVersion) userProps.shell_version = context.shellVersion; + if (context.appId) userProps.app_id = context.appId; + if (context.appVersion) userProps.app_version = context.appVersion; + + const identify = new amplitude.Identify(); + for (const [key, value] of Object.entries(userProps)) { + identify.set(key, value); + } + amplitude.identify(identify); + amplitude.track("Application Started", userProps); + amplitudeModule = amplitude; + })().catch((error: unknown) => { + initPromise = null; + console.warn("[brainstorm] analytics init failed:", error); + }); + } + await initPromise; + return amplitudeModule; +} + +/** Whether analytics is active in this renderer (public beta builds only). */ +export function isAnalyticsEnabled(): boolean { + return analyticsEnabled; +} + +/** Initialize Amplitude analytics + session replay once for this renderer. */ +export function initAnalytics(): void { + if (initialized || typeof window === "undefined") return; + initialized = true; + const version = readBridge()?.version ?? "0.0.0"; + analyticsEnabled = isPublicBeta(version); + if (!analyticsEnabled) return; + void ensureInitialized(); +} + +/** Track a named product event. No-ops until initialization completes. */ +export function track( + event: string, + properties?: Record, +): void { + if (!analyticsEnabled || !initialized || typeof window === "undefined") return; + const cleaned: Record = {}; + if (properties) { + for (const [key, value] of Object.entries(properties)) { + if (value !== undefined && value !== null) cleaned[key] = value; + } + } + void ensureInitialized().then((amplitude) => { + amplitude?.track(event, cleaned); + }); +} \ No newline at end of file diff --git a/packages/shell/changelog/changelog.json b/packages/shell/changelog/changelog.json index e0b3e124..d836e271 100644 --- a/packages/shell/changelog/changelog.json +++ b/packages/shell/changelog/changelog.json @@ -2,6 +2,135 @@ "$schema": "../src/main/help/changelog-schema.json", "format": "brainstorm/changelog/v2", "releases": [ + { + "version": "0.4.3", + "date": "2026-07-13", + "icon": "📊", + "title": "Beta usage analytics", + "summary": "During the public beta, Brainstorm now collects anonymous product analytics so we can see which features people use and where they get stuck — with a clear disclosure on first launch and in Settings → Privacy.", + "body": [ + { + "kind": "h1", + "text": "Why we're doing this" + }, + { + "kind": "p", + "text": "Brainstorm is still in public beta. To improve the product we need a picture of real usage — which apps people open, where the shell feels confusing, and which flows succeed. This patch turns on anonymous analytics for beta builds only." + }, + { + "kind": "h1", + "text": "What is collected" + }, + { + "kind": "li", + "text": [ + { + "text": "Anonymous product events", + "marks": ["bold"] + }, + { + "text": " — vault opened, app launched, launcher and settings opened, and similar shell-level actions. " + }, + { + "text": "No vault contents, passwords, or note text", + "marks": ["highlight"] + }, + { + "text": " are ever sent." + } + ] + }, + { + "kind": "li", + "text": [ + { + "text": "Session replays", + "marks": ["bold"] + }, + { + "text": " — anonymized recordings of how you navigate the UI during beta, hosted in the " + }, + { + "text": "EU", + "marks": ["highlight"] + }, + { + "text": ", so we can spot friction in real sessions." + } + ] + }, + { + "kind": "h1", + "text": "What you'll see" + }, + { + "kind": "li", + "text": [ + { + "text": "First launch", + "marks": ["bold"] + }, + { + "text": " — a one-time " + }, + { + "text": "Beta usage analytics", + "marks": ["highlight"] + }, + { + "text": " popover explains the policy. Dismiss it with " + }, + { + "text": "Got it", + "marks": ["highlight"] + }, + { + "text": "; it won't appear again on this device." + } + ] + }, + { + "kind": "li", + "text": [ + { + "text": "Settings → Privacy → Network", + "marks": ["highlight"] + }, + { + "text": " — a standing notice while we're in beta, with the same explanation." + } + ] + }, + { + "kind": "h1", + "text": "When it stops" + }, + { + "kind": "p", + "text": "Analytics runs only on pre-1.0 beta builds. When Brainstorm reaches version 1.0, collection turns off automatically and these notices go away." + }, + { + "kind": "callout", + "icon": "🔄", + "text": [ + { + "text": "Update in place. ", + "marks": ["bold"] + }, + { + "text": "Existing installs update from " + }, + { + "text": "Settings → Updates", + "marks": ["highlight"] + }, + { + "text": "." + } + ] + } + ] + }, { "version": "0.4.2", "date": "2026-07-13", diff --git a/packages/shell/electron.vite.config.ts b/packages/shell/electron.vite.config.ts index 6082895c..922785fd 100644 --- a/packages/shell/electron.vite.config.ts +++ b/packages/shell/electron.vite.config.ts @@ -99,6 +99,11 @@ export default defineConfig(({ command }) => { index: resolve(__dirname, "src/renderer/index.html"), "chrome/tab-strip": resolve(__dirname, "src/renderer/chrome/tab-strip.html"), }, + output: { + manualChunks(id) { + if (id.includes("@amplitude") || id.includes("rrweb")) return "analytics"; + }, + }, }, }, resolve: { diff --git a/packages/shell/package.json b/packages/shell/package.json index 3517f58f..b8127f99 100644 --- a/packages/shell/package.json +++ b/packages/shell/package.json @@ -1,6 +1,6 @@ { "name": "@brainstorm/shell", - "version": "0.4.2", + "version": "0.4.3", "private": true, "description": "The Brainstorm shell (Electron main + preload + dashboard renderer).", "author": "Brainstorm ", diff --git a/packages/shell/src/renderer/analytics/beta-notice.test.tsx b/packages/shell/src/renderer/analytics/beta-notice.test.tsx new file mode 100644 index 00000000..844dedee --- /dev/null +++ b/packages/shell/src/renderer/analytics/beta-notice.test.tsx @@ -0,0 +1,61 @@ +/** + * @vitest-environment jsdom + */ + +import { act } from "react"; +import { type Root, createRoot } from "react-dom/client"; +import { afterEach, beforeEach, describe, expect, it } from "vitest"; +import { AnalyticsBetaNotice } from "./beta-notice"; + +(globalThis as { IS_REACT_ACT_ENVIRONMENT?: boolean }).IS_REACT_ACT_ENVIRONMENT = true; + +let container: HTMLDivElement; +let root: Root; + +beforeEach(() => { + window.localStorage.clear(); + window.brainstorm = { + version: "0.4.2", + platform: "darwin", + } as typeof window.brainstorm; + container = document.createElement("div"); + document.body.appendChild(container); + root = createRoot(container); +}); + +afterEach(() => { + act(() => root.unmount()); + container.remove(); + window.localStorage.clear(); +}); + +function panel(): HTMLElement | null { + return document.querySelector('[data-testid="analytics-beta-notice"]'); +} + +describe("", () => { + it("shows once on beta builds and hides after dismiss", () => { + act(() => root.render()); + expect(panel()).not.toBeNull(); + + const dismiss = document.querySelector( + '[data-testid="analytics-beta-notice-dismiss"]', + ); + act(() => dismiss?.click()); + + expect(window.localStorage.getItem("brainstorm.analytics.betaNoticeDismissed")).toBe( + "beta-analytics-v1", + ); + + act(() => root.unmount()); + root = createRoot(container); + act(() => root.render()); + expect(panel()).toBeNull(); + }); + + it("does not show on GA builds", () => { + window.brainstorm = { version: "1.0.0", platform: "darwin" } as typeof window.brainstorm; + act(() => root.render()); + expect(panel()).toBeNull(); + }); +}); \ No newline at end of file diff --git a/packages/shell/src/renderer/analytics/beta-notice.tsx b/packages/shell/src/renderer/analytics/beta-notice.tsx new file mode 100644 index 00000000..364db987 --- /dev/null +++ b/packages/shell/src/renderer/analytics/beta-notice.tsx @@ -0,0 +1,58 @@ +import { + hasDismissedBetaAnalyticsNotice, + isPublicBeta, + markBetaAnalyticsNoticeDismissed, +} from "@brainstorm/sdk/analytics"; +import { AnimatePresence } from "framer-motion"; +import { useEffect, useState } from "react"; +import { t } from "../i18n/t"; +import { useShortcut } from "../shortcuts/use-shortcut"; +import { Button, ButtonVariant } from "../ui/button"; +import { Popover } from "../ui/popover"; +import { PopoverBodyPadding, PopoverSize } from "../ui/popover-types"; + +/** One-time disclosure while the product is in public beta and analytics is on. */ +export function AnalyticsBetaNotice() { + const [open, setOpen] = useState(false); + + useEffect(() => { + const version = window.brainstorm?.version ?? "0.0.0"; + if (!isPublicBeta(version)) return; + if (hasDismissedBetaAnalyticsNotice()) return; + setOpen(true); + }, []); + + const dismiss = () => { + markBetaAnalyticsNoticeDismissed(); + setOpen(false); + }; + + useShortcut("shell/popover.confirm", dismiss, { enabled: open }); + + return ( + + {open && ( + + {t("shell.analytics.betaNotice.dismiss")} + + } + > +

{t("shell.analytics.betaNotice.body")}

+

{t("shell.analytics.betaNotice.detail")}

+
+ )} +
+ ); +} \ No newline at end of file diff --git a/packages/shell/src/renderer/analytics/shell-tracking.tsx b/packages/shell/src/renderer/analytics/shell-tracking.tsx new file mode 100644 index 00000000..d69891e1 --- /dev/null +++ b/packages/shell/src/renderer/analytics/shell-tracking.tsx @@ -0,0 +1,27 @@ +import { useEffect, useRef } from "react"; +import { track } from "@brainstorm/sdk/analytics"; +import { useVault } from "../vault-context"; + +/** Observes shell routing + vault lifecycle and emits Amplitude events. */ +export function ShellTracking() { + const { loading, current } = useVault(); + const lastVaultId = useRef(null); + + useEffect(() => { + if (loading) return; + if (current) { + if (lastVaultId.current !== current.id) { + track("Vault Opened", { vault_id: current.id }); + lastVaultId.current = current.id; + } + return; + } + if (lastVaultId.current) { + track("Vault Closed", { vault_id: lastVaultId.current }); + lastVaultId.current = null; + } + track("Welcome Viewed"); + }, [loading, current]); + + return null; +} \ No newline at end of file diff --git a/packages/shell/src/renderer/analytics/track-app-launch.ts b/packages/shell/src/renderer/analytics/track-app-launch.ts new file mode 100644 index 00000000..cec10eb1 --- /dev/null +++ b/packages/shell/src/renderer/analytics/track-app-launch.ts @@ -0,0 +1,10 @@ +import { track } from "@brainstorm/sdk/analytics"; + +export function trackAppLaunch(appId: string, source: string): void { + track("App Launched", { app_id: appId, source }); +} + +export function launchApp(appId: string, source: string): void { + trackAppLaunch(appId, source); + void window.brainstorm.apps.launch(appId); +} \ No newline at end of file diff --git a/packages/shell/src/renderer/dashboard.tsx b/packages/shell/src/renderer/dashboard.tsx index 56eb4478..e6a3d83a 100644 --- a/packages/shell/src/renderer/dashboard.tsx +++ b/packages/shell/src/renderer/dashboard.tsx @@ -108,6 +108,8 @@ import { Icon, IconName } from "./ui/icon"; import { IconButton } from "./ui/icon-button"; import { Spinner } from "./ui/spinner"; import { ToastKind, pushToast } from "./ui/toasts"; +import { track } from "@brainstorm/sdk/analytics"; +import { launchApp, trackAppLaunch } from "./analytics/track-app-launch"; import { useVault } from "./vault-context"; export function Dashboard() { @@ -129,6 +131,10 @@ export function Dashboard() { const [vaultSwitcherOpen, setVaultSwitcherOpen] = useState(false); const [recoveredVaults, setRecoveredVaults] = useState([]); const [launcherOpen, setLauncherOpen] = useState(false); + + useEffect(() => { + if (settingsOpen) track("Settings Opened", { section: settingsInitialSection ?? "default" }); + }, [settingsOpen, settingsInitialSection]); // 9.8.9 — a `ui.openSearch` handoff pre-fills the palette; null = open clean. const [launcherQuery, setLauncherQuery] = useState(null); const [switcherOpen, setSwitcherOpen] = useState(false); @@ -420,6 +426,7 @@ export function Dashboard() { const activateAppIcon = useCallback((icon: DashboardIcon) => { void (async () => { try { + trackAppLaunch(icon.target, "dashboard-icon"); await window.brainstorm.apps.launch(icon.target); } catch (error) { const message = (error as Error).message ?? String(error); @@ -492,7 +499,7 @@ export function Dashboard() { } const appId = resolution?.appId; if (appId) { - void window.brainstorm.apps.launch(appId); + launchApp(appId, "dashboard-pin"); return; } pushToast({ @@ -504,7 +511,7 @@ export function Dashboard() { .catch((error: unknown) => { console.warn("[brainstorm] dashboard: pin intent.open failed:", error); const appId = resolution?.appId; - if (appId) void window.brainstorm.apps.launch(appId); + if (appId) launchApp(appId, "dashboard-pin-fallback"); }); }, [snapshot, activateAppIcon], @@ -856,7 +863,7 @@ export function Dashboard() { setAppGridOpen(false)} - onLaunch={(appId) => void window.brainstorm.apps.launch(appId)} + onLaunch={(appId) => launchApp(appId, "app-grid")} onPin={onPinApp} onUnpin={onUnpinApp} pinnedAppIds={pinnedAppIds} diff --git a/packages/shell/src/renderer/dashboard/widgets-layer.tsx b/packages/shell/src/renderer/dashboard/widgets-layer.tsx index d6b59558..02179a88 100644 --- a/packages/shell/src/renderer/dashboard/widgets-layer.tsx +++ b/packages/shell/src/renderer/dashboard/widgets-layer.tsx @@ -23,6 +23,7 @@ import { closeAnchoredMenu, openAnchoredMenu } from "@brainstorm/sdk/object-menu import { widgetFrameOrigin, widgetIframeQuery } from "@brainstorm/sdk/widget"; import { DEFAULT_THEME, flattenTokens, isThemeName, themes } from "@brainstorm/tokens"; import { type CSSProperties, memo, useCallback, useEffect, useMemo, useRef, useState } from "react"; +import { launchApp } from "../analytics/track-app-launch"; import type { DashboardWidget } from "../../preload"; import { t } from "../i18n/t"; import { Icon, IconName } from "../ui/icon"; @@ -489,7 +490,7 @@ function DashboardWidgetsLayerInner({ widgets }: DashboardWidgetsLayerProps) { ); const openApp = useCallback((appId: string) => { - void window.brainstorm.apps.launch(appId); + launchApp(appId, "widget"); }, []); // Keyboard move/resize (F-383): the grips are focusable and nudge the diff --git a/packages/shell/src/renderer/i18n/en.descriptions.json b/packages/shell/src/renderer/i18n/en.descriptions.json index 46fe983f..821e270b 100644 --- a/packages/shell/src/renderer/i18n/en.descriptions.json +++ b/packages/shell/src/renderer/i18n/en.descriptions.json @@ -6,6 +6,10 @@ "shell.actions.delete": "Generic Delete button label — removes the current object (soft-delete to the Bin). Verb, imperative; destructive tone.", "shell.actions.reset": "Generic Reset button label — returns the current setting to its default value. Verb, imperative.", "shell.actions.save": "Generic Save button label — commits the current edit. Verb, imperative.", + "shell.analytics.betaNotice.body": "Primary paragraph of the one-time beta analytics disclosure popover. Plain-language explanation that anonymous usage analytics and session replays run during beta.", + "shell.analytics.betaNotice.detail": "Secondary paragraph under the beta analytics popover body. Reassures that vault contents are excluded and points to Settings → Privacy.", + "shell.analytics.betaNotice.dismiss": "Primary button on the beta analytics disclosure popover — acknowledges the notice and dismisses it permanently (until the notice id bumps). Short, affirmative.", + "shell.analytics.betaNotice.title": "Title of the one-time beta analytics disclosure popover shown on first launch during public beta.", "shell.appGrid.empty": "Empty-state text in the dashboard app grid (launcher): “No apps match.”.", "shell.appGrid.gridLabel": "Field label in the dashboard app grid (launcher): “All installed apps”.", "shell.appGrid.menu.open": "Button label in the dashboard app grid (launcher): “Open”.", @@ -247,6 +251,8 @@ "shell.settings.mcp.transportLabel": "Field label in Settings → Apps & contributions → MCP servers (Model Context Protocol): “Transport”.", "shell.settings.mcp.untrustedNote": "UI string in Settings → Apps & contributions → MCP servers (Model Context Protocol): “Tool names and descriptions below are provided by the server and are u”.", "shell.settings.mcp.urlLabel": "Field label in Settings → Apps & contributions → MCP servers (Model Context Protocol): “Endpoint URL”.", + "shell.settings.network.analytics.summary": "Explanatory paragraph in Settings → Privacy → Network describing that beta builds send anonymous product analytics and session replays, what is excluded, and that it stops at 1.0.", + "shell.settings.network.analytics.title": "Section heading in Settings → Privacy → Network for the beta analytics disclosure. Noun phrase.", "shell.settings.network.automation.add": "Button label in Settings → Network: “Add host”.", "shell.settings.network.automation.empty": "Empty-state text in Settings → Network: “No hosts approved — Automations HTTP steps are blocked until you add o”.", "shell.settings.network.automation.placeholder": "Input placeholder in Settings → Network: “api.example.com”.", diff --git a/packages/shell/src/renderer/i18n/en.json b/packages/shell/src/renderer/i18n/en.json index 1b809b35..b3282b78 100644 --- a/packages/shell/src/renderer/i18n/en.json +++ b/packages/shell/src/renderer/i18n/en.json @@ -1276,6 +1276,12 @@ "shell.settings.network.time.minutesAgo": "{count}m ago", "shell.settings.network.time.hoursAgo": "{count}h ago", "shell.settings.network.time.daysAgo": "{count}d ago", + "shell.analytics.betaNotice.title": "Beta usage analytics", + "shell.analytics.betaNotice.body": "While Brainstorm is in beta, we collect anonymous usage analytics and session replays so we can see which features people use and where they get stuck.", + "shell.analytics.betaNotice.detail": "We never send your vault contents, passwords, or note text. Analytics turns off automatically when we reach a 1.0 release. You can read more any time under Settings → Privacy.", + "shell.analytics.betaNotice.dismiss": "Got it", + "shell.settings.network.analytics.title": "Beta usage analytics", + "shell.settings.network.analytics.summary": "During the public beta, Brainstorm sends anonymous product analytics (feature usage and session replays) to help us improve the app. Your vault data and note contents are never included. Analytics stops when we ship version 1.0.", "shell.settings.network.feedback.title": "Send feedback", "shell.settings.network.feedback.summary": "Help us improve Brainstorm by sending anonymous bug reports, ideas and questions. The product works without ever phoning home — feedback is opt-in.", "shell.settings.network.feedback.toggle": "Send anonymized feedback when I submit a report", diff --git a/packages/shell/src/renderer/launcher/launcher.tsx b/packages/shell/src/renderer/launcher/launcher.tsx index 21c14cc5..b425faff 100644 --- a/packages/shell/src/renderer/launcher/launcher.tsx +++ b/packages/shell/src/renderer/launcher/launcher.tsx @@ -46,6 +46,8 @@ import { pushToast } from "../ui/toasts"; import { LauncherRowKind, buildRows } from "./grouped-results"; import type { LauncherAppRow, LauncherEntityRow, LauncherRow } from "./grouped-results"; import "./launcher.css"; +import { track } from "@brainstorm/sdk/analytics"; +import { launchApp } from "../analytics/track-app-launch"; import { prettyEntityType, sanitizeSnippet } from "./launcher-text"; export type LauncherProps = { @@ -232,6 +234,7 @@ export function Launcher({ open, onClose, initialQuery }: LauncherProps) { initialQueryRef.current = initialQuery ?? null; useEffect(() => { if (!open) return; + track("Launcher Opened"); setQuery(initialQueryRef.current ?? ""); setEntities([]); let cancelled = false; @@ -337,7 +340,7 @@ export function Launcher({ open, onClose, initialQuery }: LauncherProps) { } function activateApp(row: LauncherAppRow, onClose: () => void): void { - void window.brainstorm.apps.launch(row.app.id); + launchApp(row.app.id, "launcher"); onClose(); } @@ -353,12 +356,12 @@ function activateEntity(row: LauncherEntityRow, onClose: () => void): void { .then((result) => { pushExplainerToast(result); if (!result.handled && result.rung === undefined) { - void window.brainstorm.apps.launch(row.hit.ownerAppId); + launchApp(row.hit.ownerAppId, "launcher-entity-fallback"); } }) .catch((error: unknown) => { console.warn("[brainstorm] launcher: intent.open dispatch failed:", error); - void window.brainstorm.apps.launch(row.hit.ownerAppId); + launchApp(row.hit.ownerAppId, "launcher-entity-fallback"); }); onClose(); } diff --git a/packages/shell/src/renderer/main.tsx b/packages/shell/src/renderer/main.tsx index 2c388a74..60c23501 100644 --- a/packages/shell/src/renderer/main.tsx +++ b/packages/shell/src/renderer/main.tsx @@ -24,7 +24,12 @@ import { ErrorBoundary } from "./ui/error-boundary"; import "./ui/error-boundary.css"; import { installFocusNav } from "./focus-nav"; import { installErrorBridge } from "./ui/error-bridge"; +import { initAnalytics } from "@brainstorm/sdk/analytics"; import { VaultProvider } from "./vault-context"; +import { AnalyticsBetaNotice } from "./analytics/beta-notice"; +import { ShellTracking } from "./analytics/shell-tracking"; + +initAnalytics(); installErrorBridge(); // Keyboard-nav mode: focus rings only after a deliberate plain Tab (gated in @@ -56,6 +61,8 @@ createRoot(rootElement).render( * bridge resolves against the live theme tokens. */} + + diff --git a/packages/shell/src/renderer/settings/network-egress-panel.tsx b/packages/shell/src/renderer/settings/network-egress-panel.tsx index 1f4d8b79..929bf069 100644 --- a/packages/shell/src/renderer/settings/network-egress-panel.tsx +++ b/packages/shell/src/renderer/settings/network-egress-panel.tsx @@ -71,6 +71,7 @@ import { Popover } from "../ui/popover"; import { PopoverBodyPadding, PopoverSize } from "../ui/popover-types"; import { Segmented } from "../ui/segmented"; import { Select, TextField, TextFieldSize } from "../ui/text-field"; +import { isPublicBeta } from "@brainstorm/sdk/analytics"; import { BrowserPrivacyPanel } from "./browser-privacy-panel"; import "./network-egress-panel.css"; @@ -255,6 +256,8 @@ export function NetworkEgressPanel() { + + {editingProxy && ( @@ -1292,6 +1295,21 @@ function EmbedProvidersPlaceholder() { ); } +// --------------------------------------------------------------------- +// Section: Beta analytics disclosure +// --------------------------------------------------------------------- + +function AnalyticsBetaSection() { + const version = window.brainstorm?.version ?? "0.0.0"; + if (!isPublicBeta(version)) return null; + return ( +
+

{t("shell.settings.network.analytics.title")}

+

{t("shell.settings.network.analytics.summary")}

+
+ ); +} + // --------------------------------------------------------------------- // Section: Feedback opt-in (Feedback-1) // --------------------------------------------------------------------- diff --git a/packages/shell/src/renderer/vault-context.tsx b/packages/shell/src/renderer/vault-context.tsx index 94a76a63..b0e143c6 100644 --- a/packages/shell/src/renderer/vault-context.tsx +++ b/packages/shell/src/renderer/vault-context.tsx @@ -1,3 +1,4 @@ +import { track } from "@brainstorm/sdk/analytics"; import { type ReactNode, createContext, useCallback, useContext, useEffect, useState } from "react"; import type { CloudSyncWarning, CreateVaultOptions, VaultEntry } from "../preload"; import type { VaultDbKind, VaultRecovery } from "../shared/vault-recovery-wire-types"; @@ -85,6 +86,7 @@ export function VaultProvider({ children }: { children: ReactNode }) { async (options: CreateVaultOptions) => { try { const entry = await window.brainstorm.vaults.create(options); + track("Vault Created", { vault_id: entry.id }); await refresh(); return entry; } catch (error) { diff --git a/tools/new-app.ts b/tools/new-app.ts index 3f9bea88..bf820554 100644 --- a/tools/new-app.ts +++ b/tools/new-app.ts @@ -194,12 +194,15 @@ export function getBrainstorm(): ${comp}Runtime | null { `, "src/main.tsx": `import "@brainstorm/sdk/app-theme.css"; +import { initAnalytics } from "@brainstorm/sdk/analytics"; import { mountMenuHost } from "@brainstorm/sdk/menus"; import { StrictMode } from "react"; import { createRoot } from "react-dom/client"; import { ${comp} } from "./app"; import "./styles.css"; +initAnalytics(); + const root = document.getElementById("root"); if (!root) throw new Error("${displayName}: #root not found in index.html"); // Stand up the shared fancy-menus runtime (object / context menus). diff --git a/vitest.config.ts b/vitest.config.ts index 9cde78bf..c15b6fd5 100644 --- a/vitest.config.ts +++ b/vitest.config.ts @@ -203,6 +203,7 @@ export default defineConfig({ "@brainstorm/sdk/shortcut": resolve(__dirname, "packages/sdk/src/shortcut/index.ts"), "@brainstorm/sdk/nav-history": resolve(__dirname, "packages/sdk/src/nav-history/index.ts"), "@brainstorm/sdk/a11y": resolve(__dirname, "packages/sdk/src/a11y/index.ts"), + "@brainstorm/sdk/analytics": resolve(__dirname, "packages/sdk/src/analytics/index.ts"), "@brainstorm/sdk/date-pager": resolve(__dirname, "packages/sdk/src/date-pager/index.ts"), "@brainstorm/sdk/date-grid": resolve(__dirname, "packages/sdk/src/date-grid/index.ts"), "@brainstorm/sdk/calendar": resolve(__dirname, "packages/sdk/src/calendar/index.ts"),