mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-10-03 04:38:48 +00:00
Retire the telemetry payload banner on v6.4
Every v6.4 build still shows existing installs a one-time "Telemetry payload updated" banner with a Disable telemetry button, keyed on a fixed notice version in browser storage. It reappears for any browser that has not dismissed it, so upgrading to 6.4.5 would keep prompting operators to opt out even though the payload has not changed. Main retired it on 2026-09-02 (a70c96c8a). Backport only the retirement: the WhatsNewCard banner and its storage key, the telemetryAction deep link that could disable telemetry on arrival, and the notice copy in all three locales. The first-run setup telemetry choice from the same main commit stays on main. The security-privacy contract now carries main's rule that payload changes are disclosed in release notes and PRIVACY.md, and that an in-app notice is for changes in kind and never pairs a disclosure with a one-click disable. (cherry picked from commita70c96c8a, banner retirement only)
This commit is contained in:
parent
ef2a1c5cc0
commit
5a49da7bde
15 changed files with 76 additions and 324 deletions
|
|
@ -1571,12 +1571,10 @@ artifact-selection behaviour.
|
|||
later installed release. Automatic release communication is limited to a
|
||||
compact non-blocking update notice; the detailed changelog may open only
|
||||
after explicit operator action. Preparing that notice records the version
|
||||
immediately so a reload cannot turn it into a recurring prompt. When the
|
||||
one-time telemetry disclosure owns the same session, it suppresses the
|
||||
lower-priority release notice instead of creating consecutive notices.
|
||||
immediately so a reload cannot turn it into a recurring prompt.
|
||||
`frontend-modern/src/utils/localStorage.ts` owns that browser-session notice
|
||||
reservation boundary so the release notice, telemetry disclosure, and
|
||||
GitHub gratitude prompt cannot create a one-two sequence. The post-update
|
||||
reservation boundary so the release notice and the GitHub gratitude prompt
|
||||
cannot create a one-two sequence. The post-update
|
||||
surface must not reuse the Highlights summary as its content,
|
||||
and must stay silent for a first baseline, malformed or development
|
||||
versions, missing releases, and releases without categorized changes.
|
||||
|
|
@ -1584,14 +1582,14 @@ artifact-selection behaviour.
|
|||
keeps it to at most three short plain-text bullets of no more than 140
|
||||
characters each, with links, code, issue references, and nested structure
|
||||
reserved for the categorized or full release notes.
|
||||
The same post-update communication boundary owns the one-time schema-v2
|
||||
telemetry payload notice. It must use a non-blocking shared notice banner,
|
||||
appear only for existing installations on a published build, stay silent
|
||||
for fresh installs and development/source builds, persist acknowledgement,
|
||||
and provide direct payload-preview, disable, and privacy-disclosure actions.
|
||||
The corresponding next-release disclosure must enumerate the added coarse
|
||||
signal categories and exclusions without inventing a release version before
|
||||
the packet is cut.
|
||||
The post-update communication boundary does not announce telemetry payload
|
||||
changes. Those are disclosed in the dated `Payload changes` section of
|
||||
`docs/PRIVACY.md` and in the release notes of the first release that carries
|
||||
them, so `frontend-modern/src/components/WhatsNewCard.tsx` renders only the
|
||||
release notice and the notice reservation recognizes only the release notice
|
||||
and the GitHub gratitude prompt as owners. A retired payload-update banner
|
||||
paired a disclosure with a one-click disable action; `security-privacy` now
|
||||
owns when an in-app telemetry notice is warranted and forbids that pairing.
|
||||
5. Add or change local dev-runtime orchestration, managed ownership, browser-runtime proof wiring, frontend/backend coherence diagnostics, canonical developer entry wrappers, deterministic dev auth seeding, dependency manifest floors, frontend build chunking, or dev-runtime helper control surfaces through `scripts/hot-dev.sh`, `scripts/hot-dev-bg.sh`, `scripts/lib/hot-dev-runtime.sh`, `scripts/lib/hot-dev-auth.sh`, `scripts/dev-deploy-agent.sh`, `Makefile`, `package.json`, `package-lock.json`, `frontend-modern/package.json`, `frontend-modern/package-lock.json`, `frontend-modern/vite.config.ts`, `go.mod`, `go.sum`, `scripts/dev-check.sh`, `scripts/toggle-mock.sh`, `scripts/clean-mock-alerts.sh`, `scripts/dev-launchd-setup.sh`, `scripts/dev-launchd-wrapper.sh`, `scripts/run_demo_public_browser_smoke.sh`, `scripts/demo_public_browser_smoke.cjs`, `scripts/com.pulse.hot-dev.plist.template`, `tests/integration/scripts/managed-dev-runtime.mjs`, `tests/integration/playwright.config.ts`, `tests/integration/tests/helpers.ts`, `tests/integration/tests/runtime-defaults.ts`, `tests/integration/README.md`, and `tests/integration/QUICK_START.md`
|
||||
First-run browser helpers are part of that dev-runtime proof boundary. They
|
||||
must preserve the setup-created API token in the shared runtime state, prefer
|
||||
|
|
|
|||
|
|
@ -972,7 +972,9 @@ AGENT_SURFACE_ID_PULSE_MCP)` and `getAgentSurfaceToolPosturePresentation`,
|
|||
may be named, while hostnames, credentials, infrastructure identifiers,
|
||||
URLs, paths, locale, browser events, prompts, chat messages, command text,
|
||||
action output, token values, and personal information must stay explicitly
|
||||
excluded.
|
||||
excluded. The shared settings shell no longer accepts a `telemetryAction`
|
||||
deep link that changes the preference on arrival; the preference changes
|
||||
only from the panel.
|
||||
8. `frontend-modern/src/components/Settings/SecurityAuthPanel.tsx` shared with `security-privacy`: the authentication settings surface is both a security/privacy control surface and a canonical settings-shell presentation boundary.
|
||||
9. `frontend-modern/src/components/Settings/SecurityOverviewPanel.tsx` shared with `security-privacy`: the security overview settings surface is both a security/privacy control surface and a canonical settings-shell presentation boundary.
|
||||
These settings panels consume the privileged security-status projection,
|
||||
|
|
|
|||
|
|
@ -1693,12 +1693,17 @@ That same telemetry trust boundary must remain operator-inspectable in-product:
|
|||
the shared system settings surface may preview only the exact runtime payload
|
||||
Pulse would send, and it must allow an operator to rotate the local telemetry
|
||||
install ID immediately without waiting for the scheduled 30-day window.
|
||||
An existing installation's first published schema-v2 upgrade must also receive
|
||||
a one-time, non-blocking notice that names the coarse payload expansion and
|
||||
links directly to the exact preview, the disable action, and the governed
|
||||
privacy disclosure. Fresh installs stay silent because setup already presents
|
||||
the current disclosure. Acknowledging the notice may persist locally, but it
|
||||
must not change the operator's telemetry preference by itself.
|
||||
Payload changes are disclosed through the dated `Payload changes` section of
|
||||
that same governed privacy disclosure and through the release notes of the
|
||||
first release that carries them, and every change must bump the schema
|
||||
version. Existing installations are not interrupted with an in-app notice for
|
||||
a new counter inside an already-disclosed category; the Settings payload
|
||||
preview is the live disclosure. An in-app notice is reserved for a change in
|
||||
kind (a new identifier, a new class of data, or a change to retention or
|
||||
handling), and such a notice must not carry a one-click disable action: a
|
||||
disable control attached to a disclosure reads as a prompt to opt out rather
|
||||
than as information. The v6.4 line retires the one-time schema-v2 payload
|
||||
banner on that basis; the first-run setup choice stays on main.
|
||||
That same governed privacy disclosure must also state the current server-side
|
||||
telemetry retention and handling rules plainly. If the license-server path
|
||||
retains telemetry rows for a fixed window or uses client IPs transiently for
|
||||
|
|
|
|||
|
|
@ -1,27 +1,29 @@
|
|||
{
|
||||
"version": 1,
|
||||
"base_sha": "a1b4e2f7532c8d7ba1cbfcb01ce0971fef42c179",
|
||||
"verified_at": "2026-09-27T10:10:29Z",
|
||||
"base_sha": "ef2a1c5cc0c00a8de32bf54ef3f603120395fe59",
|
||||
"verified_at": "2026-09-28T00:15:00Z",
|
||||
"result": "passed",
|
||||
"changed_paths": [
|
||||
"frontend-modern/src/components/Workloads/GuestDrawerOverview.tsx",
|
||||
"frontend-modern/src/components/Workloads/GuestPhysicalDisks.tsx",
|
||||
"frontend-modern/src/hooks/useWorkloads.ts",
|
||||
"frontend-modern/src/types/workloads.ts"
|
||||
"frontend-modern/src/components/Settings/Settings.tsx",
|
||||
"frontend-modern/src/components/WhatsNewCard.tsx",
|
||||
"frontend-modern/src/i18n/messages.de.ts",
|
||||
"frontend-modern/src/i18n/messages.es.ts",
|
||||
"frontend-modern/src/i18n/messages.ts",
|
||||
"frontend-modern/src/utils/localStorage.ts"
|
||||
],
|
||||
"content_sha256": {
|
||||
"frontend-modern/src/components/Workloads/GuestDrawerOverview.tsx": "f4f8903e442d92a622fa2517f54288fb0dfa97882136e268e8e0f540390127fe",
|
||||
"frontend-modern/src/components/Workloads/GuestPhysicalDisks.tsx": "1a0b15141ee41e45dc884406bc4689828b034704c80f914fc0d2baa93d0b6edd",
|
||||
"frontend-modern/src/hooks/useWorkloads.ts": "cf1b15b6554858c647108818d07f0f8c31655fdc8e9b8531fb8f9df4c7c41aa5",
|
||||
"frontend-modern/src/types/workloads.ts": "4d779f40a56b83b83c8bbcb4b49f88e1db3432d071b8f6326094fa081547c1e6"
|
||||
"frontend-modern/src/components/Settings/Settings.tsx": "72682c86dd055761cbd68d4f57b684afe4d978a0b53748859d0721e711ce321a",
|
||||
"frontend-modern/src/components/WhatsNewCard.tsx": "90543f19d61e9ba6d93251a7bcc9c179fa8d7ccb6ab3364e364d0ea13b2c31f0",
|
||||
"frontend-modern/src/i18n/messages.de.ts": "f79a2d86dd2484b414706eb05215c3c75696d496b9963f937998eaeef87f6635",
|
||||
"frontend-modern/src/i18n/messages.es.ts": "d43643d4bd6b4480569bf0a23c82217f3c91823f2a020bd8252a7f42c2a6fbf9",
|
||||
"frontend-modern/src/i18n/messages.ts": "d025eb5cdaa327804c447c35ace272d9e40c3e9263ce3d3c15f14ed70acdcbfd",
|
||||
"frontend-modern/src/utils/localStorage.ts": "19a532f55271b2fc696e3ba4ae6ba098e759f0f9df207015c803f7fcc0050bd6"
|
||||
},
|
||||
"routes": [
|
||||
"/browser-tests/guest-storage-2263.html"
|
||||
],
|
||||
"routes": ["/", "/settings/system-general", "/settings/system-general?telemetryAction=disable"],
|
||||
"viewports": [
|
||||
{
|
||||
"width": 1440,
|
||||
"height": 900
|
||||
"width": 1024,
|
||||
"height": 768
|
||||
},
|
||||
{
|
||||
"width": 390,
|
||||
|
|
@ -29,18 +31,18 @@
|
|||
}
|
||||
],
|
||||
"states": [
|
||||
"Linked guest: one physical disk and a clean RAID1 array visible without loading SMART detail",
|
||||
"Expanded disk while lazy detail request is held: loading status visible and keyboard focus retained",
|
||||
"Loaded SMART overview and History: health attributes and empty-history chart placeholders visible",
|
||||
"No-agent guest: no disk card and no child-resource request",
|
||||
"HTTP 503 child-resource query: unavailable status visible instead of an empty disk card"
|
||||
"Local build of this branch stamped v6.4.5, signed in with a test account",
|
||||
"Simulated upgrade from 6.4.4: pulseWhatsNewLastSeen set to 6.4.4 and pulseTelemetryPayloadNoticeSeen absent, the exact condition under which the retired banner appeared",
|
||||
"After reload the post-update check ran (last seen advanced to 6.4.5) and no Telemetry payload updated banner or Disable telemetry action rendered",
|
||||
"Retired telemetryAction=disable deep link opened: telemetry stayed enabled per /api/system/settings and no banner rendered",
|
||||
"390px reload with the payload notice still unacknowledged: no banner, no page-level horizontal overflow",
|
||||
"Desktop and phone screenshots inspected for stray notice placement and overflow"
|
||||
],
|
||||
"interactions": [
|
||||
"Expand by keyboard, hold and release the SMART module, then switch History and Overview tabs",
|
||||
"Collapse and reopen by pointer without a second SMART module request",
|
||||
"Switch to no-agent and failing-query fixture states; assert exact parent-filtered disk requests only",
|
||||
"Inspect desktop and phone screenshots for disk/RAID placement and overflow"
|
||||
],
|
||||
"command": "pulse-worker-browser dist/browser-proof-2263/browser-run.cjs from assigned release/v6.4 workspace root (Playwright 1.56.1, Chromium 141.0.7390.37)",
|
||||
"notes": "Offline synthetic fixture from byte-identical, lock-matched source copy under /workspace/dist/browser-proof-2263/frontend-modern; browser-run.cjs differs from tracked guest-storage-2263.cjs only in the absolute root path. Passed log and 12 inspected desktop/phone screenshots are retained in this release-line worker output. This is not installed or field acceptance."
|
||||
"Signed in through the login form",
|
||||
"Seeded the upgrade baseline in localStorage and reloaded the app",
|
||||
"Opened General settings and read the telemetry preference from the settings API",
|
||||
"Opened the retired telemetryAction=disable deep link and re-read the preference",
|
||||
"Resized to 390x844 and reloaded"
|
||||
]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -167,41 +167,6 @@ const SettingsWorkspace: Component<SettingsProps> = (props) => {
|
|||
}
|
||||
return settingsPanelRegistry()[currentTab];
|
||||
});
|
||||
let handledTelemetryActionHref = '';
|
||||
|
||||
createEffect(() => {
|
||||
if (activeTab() !== 'system-general' || !infrastructureSettings.initialLoadComplete()) {
|
||||
return;
|
||||
}
|
||||
|
||||
const action = new URLSearchParams(location.search).get('telemetryAction');
|
||||
if (action !== 'preview' && action !== 'disable') {
|
||||
return;
|
||||
}
|
||||
|
||||
const actionHref = `${location.pathname}${location.search}${location.hash}`;
|
||||
if (actionHref === handledTelemetryActionHref) {
|
||||
return;
|
||||
}
|
||||
handledTelemetryActionHref = actionHref;
|
||||
|
||||
queueMicrotask(() => {
|
||||
document.getElementById('usage-telemetry')?.scrollIntoView({
|
||||
behavior: 'smooth',
|
||||
block: 'start',
|
||||
});
|
||||
if (action === 'preview') {
|
||||
void systemSettings.handleLoadTelemetryPreview();
|
||||
} else {
|
||||
void systemSettings.handleTelemetryEnabledChange(false);
|
||||
}
|
||||
navigate('/settings/system-general#usage-telemetry', {
|
||||
replace: true,
|
||||
scroll: false,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
createEffect(() => {
|
||||
activeTab();
|
||||
queueMicrotask(() => window.scrollTo({ top: 0, behavior: 'auto' }));
|
||||
|
|
|
|||
|
|
@ -915,10 +915,11 @@ describe('settings architecture guardrails', () => {
|
|||
expect(generalSettingsPanelSource).toContain('settings.general.telemetry.payloadAriaLabel');
|
||||
expect(generalSettingsPanelSource).toContain('settings.general.telemetry.resetId');
|
||||
expect(generalSettingsPanelSource).toContain('id="usage-telemetry"');
|
||||
expect(settingsSource).toContain("get('telemetryAction')");
|
||||
expect(settingsSource).toContain('systemSettings.handleLoadTelemetryPreview()');
|
||||
expect(settingsSource).toContain('systemSettings.handleTelemetryEnabledChange(false)');
|
||||
expect(settingsSource).toContain("navigate('/settings/system-general#usage-telemetry'");
|
||||
// The payload-update banner and its one-click disable deep link were
|
||||
// retired: payload changes are disclosed in release notes and the dated
|
||||
// PRIVACY.md changelog, and the preference is changed only from the panel.
|
||||
expect(settingsSource).not.toContain('telemetryAction');
|
||||
expect(settingsSource).not.toContain('handleTelemetryEnabledChange(false)');
|
||||
expect(generalSettingsPanelSource).not.toContain('license_tier');
|
||||
expect(generalSettingsPanelSource).not.toContain('api_tokens');
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,7 +1,5 @@
|
|||
import { Show, createEffect, createSignal } from 'solid-js';
|
||||
import { useNavigate } from '@solidjs/router';
|
||||
import CheckCircleIcon from 'lucide-solid/icons/check-circle';
|
||||
import InfoIcon from 'lucide-solid/icons/info';
|
||||
import XIcon from 'lucide-solid/icons/x';
|
||||
import { updateStore } from '@/stores/updates';
|
||||
import { UpdatesAPI } from '@/api/updates';
|
||||
|
|
@ -13,13 +11,8 @@ import { InlineNotice } from '@/components/shared/InlineNotice';
|
|||
import { buildReleaseNotesUrl, normalizeReleaseVersion } from '@/components/updateVersion';
|
||||
import { extractChangelog, isReleaseVersion } from '@/components/whatsNewModel';
|
||||
import { renderMarkdown } from '@/components/AI/aiChatUtils';
|
||||
import { t } from '@/i18n';
|
||||
import { PRIVACY_DOC_URL } from '@/utils/docsLinks';
|
||||
import { logger } from '@/utils/logger';
|
||||
|
||||
const TELEMETRY_PAYLOAD_NOTICE_VERSION = '2';
|
||||
const TELEMETRY_SETTINGS_SECTION_ID = 'usage-telemetry';
|
||||
|
||||
const readLastSeenVersion = (): string | null => {
|
||||
try {
|
||||
return localStorage.getItem(STORAGE_KEYS.WHATS_NEW_LAST_SEEN);
|
||||
|
|
@ -36,25 +29,6 @@ const markVersionSeen = (version: string) => {
|
|||
}
|
||||
};
|
||||
|
||||
const readTelemetryPayloadNoticeVersion = (): string | null => {
|
||||
try {
|
||||
return localStorage.getItem(STORAGE_KEYS.TELEMETRY_PAYLOAD_NOTICE_SEEN);
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
const markTelemetryPayloadNoticeSeen = () => {
|
||||
try {
|
||||
localStorage.setItem(
|
||||
STORAGE_KEYS.TELEMETRY_PAYLOAD_NOTICE_SEEN,
|
||||
TELEMETRY_PAYLOAD_NOTICE_VERSION,
|
||||
);
|
||||
} catch {
|
||||
// Private mode / storage disabled: setup and Settings remain the fallback disclosure.
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Post-update "What's New" notice. A compact non-blocking notice appears once
|
||||
* after the running version changes and only when that release has categorized
|
||||
|
|
@ -62,26 +36,16 @@ const markTelemetryPayloadNoticeSeen = () => {
|
|||
* explicit action. Preparing the notice (or finding no categorized entries)
|
||||
* records the version so reloads stay quiet until the next update.
|
||||
*
|
||||
* This release communication boundary also owns the one-time, non-blocking
|
||||
* telemetry schema v2 notice. Existing installations see it once; fresh
|
||||
* installs stay quiet because setup already presents the current disclosure.
|
||||
* Telemetry payload changes are not announced here. They are disclosed in
|
||||
* release notes and the dated "Payload changes" section of docs/PRIVACY.md;
|
||||
* the Settings payload preview always shows the exact current contract.
|
||||
*/
|
||||
export function WhatsNewCard() {
|
||||
const navigate = useNavigate();
|
||||
const [noticeVisible, setNoticeVisible] = createSignal(false);
|
||||
const [dialogVisible, setDialogVisible] = createSignal(false);
|
||||
const [telemetryNoticeVisible, setTelemetryNoticeVisible] = createSignal(false);
|
||||
const [version, setVersion] = createSignal('');
|
||||
const [changelogHtml, setChangelogHtml] = createSignal('');
|
||||
const hadPriorReleaseBaseline = readLastSeenVersion() !== null;
|
||||
const telemetryNoticeAlreadySeen =
|
||||
readTelemetryPayloadNoticeVersion() === TELEMETRY_PAYLOAD_NOTICE_VERSION;
|
||||
const telemetryNoticeNeedsSession = hadPriorReleaseBaseline && !telemetryNoticeAlreadySeen;
|
||||
if (telemetryNoticeNeedsSession) {
|
||||
reserveLowPriorityNoticeSession('telemetry-update');
|
||||
}
|
||||
let checked = false;
|
||||
let telemetryNoticeChecked = false;
|
||||
|
||||
const loadNotes = async (currentVersion: string, noticeSlotReserved: boolean) => {
|
||||
try {
|
||||
|
|
@ -142,28 +106,6 @@ export function WhatsNewCard() {
|
|||
void loadNotes(currentVersion, noticeSlotReserved);
|
||||
});
|
||||
|
||||
createEffect(() => {
|
||||
const info = updateStore.versionInfo();
|
||||
if (!info || telemetryNoticeChecked) return;
|
||||
telemetryNoticeChecked = true;
|
||||
|
||||
if (info.isDevelopment || info.isSourceBuild || !isReleaseVersion(info.version)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!hadPriorReleaseBaseline) {
|
||||
// Setup already showed the current telemetry disclosure on a fresh install.
|
||||
markTelemetryPayloadNoticeSeen();
|
||||
return;
|
||||
}
|
||||
|
||||
if (readTelemetryPayloadNoticeVersion() === TELEMETRY_PAYLOAD_NOTICE_VERSION) {
|
||||
return;
|
||||
}
|
||||
|
||||
setTelemetryNoticeVisible(true);
|
||||
});
|
||||
|
||||
const dismissNotice = () => {
|
||||
setNoticeVisible(false);
|
||||
};
|
||||
|
|
@ -177,63 +119,8 @@ export function WhatsNewCard() {
|
|||
setDialogVisible(false);
|
||||
};
|
||||
|
||||
const dismissTelemetryNotice = () => {
|
||||
markTelemetryPayloadNoticeSeen();
|
||||
setTelemetryNoticeVisible(false);
|
||||
};
|
||||
|
||||
const openTelemetrySettings = (action: 'preview' | 'disable') => {
|
||||
dismissTelemetryNotice();
|
||||
navigate(`/settings/system-general?telemetryAction=${action}#${TELEMETRY_SETTINGS_SECTION_ID}`);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Show when={telemetryNoticeVisible()}>
|
||||
<InlineNotice
|
||||
role="status"
|
||||
data-testid="telemetry-payload-update-notice"
|
||||
tone="info"
|
||||
layout="banner"
|
||||
icon={<InfoIcon class="h-4 w-4" aria-hidden="true" />}
|
||||
onDismiss={dismissTelemetryNotice}
|
||||
dismissLabel={t('settings.general.telemetry.notice.dismissLabel')}
|
||||
dismissTitle={t('settings.general.telemetry.notice.dismissTitle')}
|
||||
>
|
||||
<div class="flex flex-col gap-2 sm:flex-row sm:items-center sm:justify-between">
|
||||
<p class="leading-relaxed">
|
||||
<span class="font-semibold">{t('settings.general.telemetry.notice.title')}.</span>{' '}
|
||||
{t('settings.general.telemetry.notice.description')}
|
||||
</p>
|
||||
<div class="flex shrink-0 flex-wrap items-center gap-2">
|
||||
<Button
|
||||
type="button"
|
||||
variant="secondary"
|
||||
size="settingsActionXs"
|
||||
onClick={() => openTelemetrySettings('preview')}
|
||||
>
|
||||
{t('settings.general.telemetry.notice.preview')}
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
variant="secondary"
|
||||
size="settingsActionXs"
|
||||
onClick={() => openTelemetrySettings('disable')}
|
||||
>
|
||||
{t('settings.general.telemetry.notice.disable')}
|
||||
</Button>
|
||||
<ExternalTextLink
|
||||
href={PRIVACY_DOC_URL}
|
||||
variant="compactInherit"
|
||||
onClick={dismissTelemetryNotice}
|
||||
>
|
||||
{t('settings.general.telemetry.notice.privacy')}
|
||||
</ExternalTextLink>
|
||||
</div>
|
||||
</div>
|
||||
</InlineNotice>
|
||||
</Show>
|
||||
|
||||
<Show when={noticeVisible()}>
|
||||
<aside
|
||||
class="fixed bottom-[var(--pulse-mobile-nav-height)] left-4 right-4 z-30 max-w-sm md:right-auto md:bottom-4"
|
||||
|
|
|
|||
|
|
@ -4,11 +4,6 @@ import { STORAGE_KEYS } from '@/utils/localStorage';
|
|||
|
||||
const versionInfoMock = vi.hoisted(() => vi.fn());
|
||||
const getReleaseNotesMock = vi.hoisted(() => vi.fn());
|
||||
const navigateMock = vi.hoisted(() => vi.fn());
|
||||
|
||||
vi.mock('@solidjs/router', () => ({
|
||||
useNavigate: () => navigateMock,
|
||||
}));
|
||||
|
||||
vi.mock('@/stores/updates', () => ({
|
||||
updateStore: {
|
||||
|
|
@ -30,7 +25,6 @@ describe('WhatsNewCard', () => {
|
|||
beforeEach(() => {
|
||||
versionInfoMock.mockReset();
|
||||
getReleaseNotesMock.mockReset();
|
||||
navigateMock.mockReset();
|
||||
localStorage.clear();
|
||||
sessionStorage.clear();
|
||||
});
|
||||
|
|
@ -52,13 +46,11 @@ describe('WhatsNewCard', () => {
|
|||
await renderCard();
|
||||
|
||||
expect(screen.queryByTestId('whats-new-modal')).not.toBeInTheDocument();
|
||||
expect(screen.queryByTestId('telemetry-payload-update-notice')).not.toBeInTheDocument();
|
||||
expect(getReleaseNotesMock).not.toHaveBeenCalled();
|
||||
expect(localStorage.getItem(STORAGE_KEYS.WHATS_NEW_LAST_SEEN)).toBe('6.1.0-rc.1');
|
||||
expect(localStorage.getItem(STORAGE_KEYS.TELEMETRY_PAYLOAD_NOTICE_SEEN)).toBe('2');
|
||||
});
|
||||
|
||||
it('shows the telemetry payload update once to an existing installation', async () => {
|
||||
it('never shows a telemetry payload notice to an existing installation', async () => {
|
||||
localStorage.setItem(STORAGE_KEYS.WHATS_NEW_LAST_SEEN, '6.1.0-rc.1');
|
||||
versionInfoMock.mockReturnValue({
|
||||
version: '6.1.0-rc.1',
|
||||
|
|
@ -68,68 +60,13 @@ describe('WhatsNewCard', () => {
|
|||
|
||||
await renderCard();
|
||||
|
||||
expect(screen.getByTestId('telemetry-payload-update-notice')).toBeInTheDocument();
|
||||
expect(screen.getByText('Telemetry payload updated.')).toBeInTheDocument();
|
||||
expect(screen.getByRole('button', { name: 'Preview payload' })).toBeInTheDocument();
|
||||
expect(screen.getByRole('button', { name: 'Disable telemetry' })).toBeInTheDocument();
|
||||
expect(screen.getByRole('link', { name: 'Privacy details' })).toHaveAttribute(
|
||||
'href',
|
||||
'/docs/PRIVACY',
|
||||
);
|
||||
});
|
||||
|
||||
it('opens the exact payload preview and permanently dismisses the notice', async () => {
|
||||
localStorage.setItem(STORAGE_KEYS.WHATS_NEW_LAST_SEEN, '6.1.0-rc.1');
|
||||
versionInfoMock.mockReturnValue({
|
||||
version: '6.1.0-rc.1',
|
||||
isDevelopment: false,
|
||||
isSourceBuild: false,
|
||||
});
|
||||
|
||||
await renderCard();
|
||||
fireEvent.click(screen.getByRole('button', { name: 'Preview payload' }));
|
||||
|
||||
expect(navigateMock).toHaveBeenCalledWith(
|
||||
'/settings/system-general?telemetryAction=preview#usage-telemetry',
|
||||
);
|
||||
expect(localStorage.getItem(STORAGE_KEYS.TELEMETRY_PAYLOAD_NOTICE_SEEN)).toBe('2');
|
||||
expect(screen.queryByTestId('telemetry-payload-update-notice')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('opens the disable action and permanently dismisses the notice', async () => {
|
||||
localStorage.setItem(STORAGE_KEYS.WHATS_NEW_LAST_SEEN, '6.1.0-rc.1');
|
||||
versionInfoMock.mockReturnValue({
|
||||
version: '6.1.0-rc.1',
|
||||
isDevelopment: false,
|
||||
isSourceBuild: false,
|
||||
});
|
||||
|
||||
await renderCard();
|
||||
fireEvent.click(screen.getByRole('button', { name: 'Disable telemetry' }));
|
||||
|
||||
expect(navigateMock).toHaveBeenCalledWith(
|
||||
'/settings/system-general?telemetryAction=disable#usage-telemetry',
|
||||
);
|
||||
expect(localStorage.getItem(STORAGE_KEYS.TELEMETRY_PAYLOAD_NOTICE_SEEN)).toBe('2');
|
||||
});
|
||||
|
||||
it('does not show the telemetry notice after it has been acknowledged', async () => {
|
||||
localStorage.setItem(STORAGE_KEYS.WHATS_NEW_LAST_SEEN, '6.1.0-rc.1');
|
||||
localStorage.setItem(STORAGE_KEYS.TELEMETRY_PAYLOAD_NOTICE_SEEN, '2');
|
||||
versionInfoMock.mockReturnValue({
|
||||
version: '6.1.0-rc.1',
|
||||
isDevelopment: false,
|
||||
isSourceBuild: false,
|
||||
});
|
||||
|
||||
await renderCard();
|
||||
|
||||
expect(screen.queryByTestId('telemetry-payload-update-notice')).not.toBeInTheDocument();
|
||||
expect(screen.queryByRole('button', { name: 'Disable telemetry' })).not.toBeInTheDocument();
|
||||
expect(screen.queryByRole('status')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('announces the running release without opening a blocking dialog', async () => {
|
||||
localStorage.setItem(STORAGE_KEYS.WHATS_NEW_LAST_SEEN, '6.0.5');
|
||||
localStorage.setItem(STORAGE_KEYS.TELEMETRY_PAYLOAD_NOTICE_SEEN, '2');
|
||||
versionInfoMock.mockReturnValue({
|
||||
version: '6.1.0-rc.1',
|
||||
isDevelopment: false,
|
||||
|
|
@ -189,7 +126,6 @@ describe('WhatsNewCard', () => {
|
|||
|
||||
it('dismisses the compact notice without reopening it on reload', async () => {
|
||||
localStorage.setItem(STORAGE_KEYS.WHATS_NEW_LAST_SEEN, '6.0.5');
|
||||
localStorage.setItem(STORAGE_KEYS.TELEMETRY_PAYLOAD_NOTICE_SEEN, '2');
|
||||
versionInfoMock.mockReturnValue({
|
||||
version: '6.1.0-rc.1',
|
||||
isDevelopment: false,
|
||||
|
|
@ -219,8 +155,9 @@ describe('WhatsNewCard', () => {
|
|||
expect(screen.queryByRole('dialog')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('keeps the release notice quiet when the telemetry disclosure owns the session', async () => {
|
||||
it('yields the notice slot to a higher-priority owner already holding the session', async () => {
|
||||
localStorage.setItem(STORAGE_KEYS.WHATS_NEW_LAST_SEEN, '6.0.5');
|
||||
sessionStorage.setItem('pulse-low-priority-notice-owner', 'github-star');
|
||||
versionInfoMock.mockReturnValue({
|
||||
version: '6.1.0-rc.1',
|
||||
isDevelopment: false,
|
||||
|
|
@ -236,15 +173,9 @@ describe('WhatsNewCard', () => {
|
|||
await renderCard();
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId('telemetry-payload-update-notice')).toBeInTheDocument();
|
||||
expect(localStorage.getItem(STORAGE_KEYS.WHATS_NEW_LAST_SEEN)).toBe('6.1.0-rc.1');
|
||||
});
|
||||
expect(screen.queryByTestId('whats-new-notice')).not.toBeInTheDocument();
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: 'Dismiss telemetry payload update' }));
|
||||
|
||||
expect(screen.queryByTestId('telemetry-payload-update-notice')).not.toBeInTheDocument();
|
||||
expect(screen.queryByTestId('whats-new-notice')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('stays quiet when a release has only a highlights summary', async () => {
|
||||
|
|
@ -280,9 +211,7 @@ describe('WhatsNewCard', () => {
|
|||
await renderCard();
|
||||
|
||||
expect(screen.queryByTestId('whats-new-modal')).not.toBeInTheDocument();
|
||||
expect(screen.queryByTestId('telemetry-payload-update-notice')).not.toBeInTheDocument();
|
||||
expect(getReleaseNotesMock).not.toHaveBeenCalled();
|
||||
expect(localStorage.getItem(STORAGE_KEYS.WHATS_NEW_LAST_SEEN)).toBe('6.0.5');
|
||||
expect(localStorage.getItem(STORAGE_KEYS.TELEMETRY_PAYLOAD_NOTICE_SEEN)).toBeNull();
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -246,13 +246,13 @@ describe('isReleaseVersion', () => {
|
|||
});
|
||||
|
||||
describe('post-update telemetry disclosure', () => {
|
||||
it('keeps the schema-v2 notice on the shared non-blocking release boundary', () => {
|
||||
expect(whatsNewCardSource).toContain('TELEMETRY_PAYLOAD_NOTICE_VERSION');
|
||||
expect(whatsNewCardSource).toContain('data-testid="telemetry-payload-update-notice"');
|
||||
expect(whatsNewCardSource).toContain('layout="banner"');
|
||||
expect(whatsNewCardSource).toContain("openTelemetrySettings('preview')");
|
||||
expect(whatsNewCardSource).toContain("openTelemetrySettings('disable')");
|
||||
expect(whatsNewCardSource).toContain('PRIVACY_DOC_URL');
|
||||
it('does not announce telemetry payload changes from the release boundary', () => {
|
||||
// Payload changes are disclosed in release notes and the dated
|
||||
// PRIVACY.md changelog. A banner that pairs "we now collect more" with a
|
||||
// one-click disable button reads as an opt-out prompt, so it was retired.
|
||||
expect(whatsNewCardSource).not.toContain('TELEMETRY_PAYLOAD_NOTICE_VERSION');
|
||||
expect(whatsNewCardSource).not.toContain('telemetry-payload-update-notice');
|
||||
expect(whatsNewCardSource).not.toContain('openTelemetrySettings');
|
||||
});
|
||||
|
||||
it('keeps release details opt-in and coordinates the automatic notice session', () => {
|
||||
|
|
|
|||
|
|
@ -230,8 +230,6 @@ describe('i18n foundation', () => {
|
|||
|
||||
for (const locale of FIRST_LOCALIZATION_LOCALES) {
|
||||
const telemetryDescription = I18N_MESSAGES[locale]['settings.general.telemetry.description'];
|
||||
const telemetryUpdateNotice =
|
||||
I18N_MESSAGES[locale]['settings.general.telemetry.notice.description'];
|
||||
|
||||
expect(I18N_MESSAGES[locale]['settings.general.language.description']).toContain('API');
|
||||
expect(telemetryDescription).toContain('Pulse');
|
||||
|
|
@ -241,8 +239,6 @@ describe('i18n foundation', () => {
|
|||
for (const term of telemetryPrivacyTerms[locale]) {
|
||||
expect(telemetryDescription, `${locale}:${term}`).toContain(term);
|
||||
}
|
||||
expect(telemetryUpdateNotice).toContain('Pulse');
|
||||
expect(telemetryUpdateNotice).not.toMatch(/anonymous/i);
|
||||
expect(I18N_MESSAGES[locale]['settings.general.telemetry.copyJson']).toContain('JSON');
|
||||
expect(
|
||||
t(
|
||||
|
|
|
|||
|
|
@ -394,14 +394,6 @@ export const DE_MESSAGE_OVERRIDES = {
|
|||
'settings.general.telemetry.disabledPreview':
|
||||
'Telemetrie ist derzeit deaktiviert. Diese Vorschau zeigt den Payload, den Pulse senden wuerde, wenn Sie sie aktivieren.',
|
||||
'settings.general.telemetry.fullDetails': 'Details',
|
||||
'settings.general.telemetry.notice.description':
|
||||
'Pulse fuegt jetzt grobe Signale zu Bereitstellung, Lebenszyklus, Bestandsgroesse sowie aggregierte Alarm- und Benachrichtigungsergebnisse hinzu, wenn ausgehende Nutzungstelemetrie aktiviert ist. Persoenliche Daten, Infrastruktur-IDs, Inhalte, Browser-Ereignisse und Clickstream-Daten bleiben ausgeschlossen.',
|
||||
'settings.general.telemetry.notice.disable': 'Telemetrie deaktivieren',
|
||||
'settings.general.telemetry.notice.dismissLabel': 'Hinweis zum Telemetrie-Payload schliessen',
|
||||
'settings.general.telemetry.notice.dismissTitle': 'Dauerhaft schliessen',
|
||||
'settings.general.telemetry.notice.preview': 'Payload anzeigen',
|
||||
'settings.general.telemetry.notice.privacy': 'Datenschutzdetails',
|
||||
'settings.general.telemetry.notice.title': 'Telemetrie-Payload aktualisiert',
|
||||
'settings.general.telemetry.payloadAriaLabel': 'Telemetrie-Payload-Vorschau',
|
||||
'settings.general.telemetry.payloadTitle': 'Aktueller Heartbeat-Payload',
|
||||
'settings.general.telemetry.previewPayload': 'Payload anzeigen',
|
||||
|
|
|
|||
|
|
@ -386,15 +386,6 @@ export const ES_MESSAGE_OVERRIDES = {
|
|||
'settings.general.telemetry.disabledPreview':
|
||||
'La telemetría está desactivada. Esta vista previa muestra el payload que Pulse enviaría si la activas.',
|
||||
'settings.general.telemetry.fullDetails': 'Detalles completos',
|
||||
'settings.general.telemetry.notice.description':
|
||||
'Pulse ahora añade señales generales de despliegue, ciclo de vida y tamaño de la instalación, además de resultados agregados de alertas y notificaciones, cuando la telemetría de uso saliente está activada. Sigue excluyendo datos personales, identificadores de infraestructura, contenido, eventos del navegador y datos de clickstream.',
|
||||
'settings.general.telemetry.notice.disable': 'Desactivar telemetría',
|
||||
'settings.general.telemetry.notice.dismissLabel':
|
||||
'Descartar aviso de actualización del payload de telemetría',
|
||||
'settings.general.telemetry.notice.dismissTitle': 'Descartar permanentemente',
|
||||
'settings.general.telemetry.notice.preview': 'Vista previa del payload',
|
||||
'settings.general.telemetry.notice.privacy': 'Detalles de privacidad',
|
||||
'settings.general.telemetry.notice.title': 'Payload de telemetría actualizado',
|
||||
'settings.general.telemetry.payloadAriaLabel': 'Vista previa del payload de telemetría',
|
||||
'settings.general.telemetry.payloadTitle': 'Payload de heartbeat actual',
|
||||
'settings.general.telemetry.previewPayload': 'Vista previa del payload',
|
||||
|
|
|
|||
|
|
@ -377,14 +377,6 @@ export const EN_MESSAGES = {
|
|||
'settings.general.telemetry.disabledPreview':
|
||||
'Telemetry is currently disabled. This preview shows the payload Pulse would send if you enable it.',
|
||||
'settings.general.telemetry.fullDetails': 'Full details',
|
||||
'settings.general.telemetry.notice.description':
|
||||
'Pulse now adds four content-free, session-deduplicated workload-history adoption counts when outbound usage telemetry is enabled. It still excludes personal details, infrastructure IDs, content, raw browser events, and event-level clickstream data.',
|
||||
'settings.general.telemetry.notice.disable': 'Disable telemetry',
|
||||
'settings.general.telemetry.notice.dismissLabel': 'Dismiss telemetry payload update',
|
||||
'settings.general.telemetry.notice.dismissTitle': 'Dismiss permanently',
|
||||
'settings.general.telemetry.notice.preview': 'Preview payload',
|
||||
'settings.general.telemetry.notice.privacy': 'Privacy details',
|
||||
'settings.general.telemetry.notice.title': 'Telemetry payload updated',
|
||||
'settings.general.telemetry.payloadAriaLabel': 'Telemetry payload preview',
|
||||
'settings.general.telemetry.payloadTitle': 'Current heartbeat payload',
|
||||
'settings.general.telemetry.previewPayload': 'Preview payload',
|
||||
|
|
@ -578,13 +570,6 @@ export const SETTINGS_GENERAL_MIGRATED_MESSAGE_KEYS = [
|
|||
'settings.general.telemetry.description',
|
||||
'settings.general.telemetry.disabledPreview',
|
||||
'settings.general.telemetry.fullDetails',
|
||||
'settings.general.telemetry.notice.description',
|
||||
'settings.general.telemetry.notice.disable',
|
||||
'settings.general.telemetry.notice.dismissLabel',
|
||||
'settings.general.telemetry.notice.dismissTitle',
|
||||
'settings.general.telemetry.notice.preview',
|
||||
'settings.general.telemetry.notice.privacy',
|
||||
'settings.general.telemetry.notice.title',
|
||||
'settings.general.telemetry.payloadAriaLabel',
|
||||
'settings.general.telemetry.payloadTitle',
|
||||
'settings.general.telemetry.previewPayload',
|
||||
|
|
|
|||
|
|
@ -101,7 +101,7 @@ describe('localStorage signals', () => {
|
|||
});
|
||||
|
||||
it('rejects a second low-priority notice owner in the same session', () => {
|
||||
expect(reserveLowPriorityNoticeSession('telemetry-update')).toBe(true);
|
||||
expect(reserveLowPriorityNoticeSession('release-update')).toBe(true);
|
||||
expect(reserveLowPriorityNoticeSession('github-star')).toBe(false);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -138,7 +138,7 @@ export function createLocalStorageStringSignal(
|
|||
);
|
||||
}
|
||||
|
||||
export type LowPriorityNoticeOwner = 'github-star' | 'release-update' | 'telemetry-update';
|
||||
export type LowPriorityNoticeOwner = 'github-star' | 'release-update';
|
||||
|
||||
export const SESSION_STORAGE_KEYS = {
|
||||
LOW_PRIORITY_NOTICE_OWNER: 'pulse-low-priority-notice-owner',
|
||||
|
|
@ -190,7 +190,6 @@ export const STORAGE_KEYS = {
|
|||
// Updates
|
||||
UPDATES: 'pulse-updates',
|
||||
WHATS_NEW_LAST_SEEN: 'pulseWhatsNewLastSeen',
|
||||
TELEMETRY_PAYLOAD_NOTICE_SEEN: 'pulseTelemetryPayloadNoticeSeen',
|
||||
|
||||
// Alert settings
|
||||
ALERT_HISTORY_TIME_FILTER: 'alertHistoryTimeFilter',
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue