mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-10-19 10:33:29 +00:00
Add transparent telemetry payload update notice
This commit is contained in:
parent
2323c96385
commit
a082d5588f
16 changed files with 419 additions and 61 deletions
|
|
@ -3,6 +3,9 @@
|
|||
Pulse release notes live on GitHub:
|
||||
<https://github.com/rcourtman/Pulse/releases>
|
||||
|
||||
For the next release disclosure draft (version assigned at release cut), see:
|
||||
- `docs/releases/RELEASE_NOTES_NEXT_DRAFT.md`
|
||||
|
||||
For historical v4 notes that previously lived in this repo, see:
|
||||
`docs/releases/RELEASE_NOTES_v4.md`
|
||||
|
||||
|
|
|
|||
|
|
@ -540,6 +540,14 @@ TLS floor in the dynamic config.
|
|||
while the post-update card may show that same section once per later
|
||||
installed release and must stay silent for a first baseline, malformed or
|
||||
development versions, missing releases, and releases without highlights.
|
||||
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.
|
||||
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
|
||||
|
|
|
|||
|
|
@ -2187,6 +2187,11 @@ default` instead of fusing provider and badge text such as
|
|||
“IPs are never seen,” the summary copy in
|
||||
`GeneralSettingsPanel.tsx` must state those facts plainly instead of
|
||||
reverting to a stronger but inaccurate shorthand.
|
||||
The one-time schema-v2 upgrade disclosure must compose the shared
|
||||
`InlineNotice` banner layout on the existing post-update communication
|
||||
boundary, use shared button and external-link primitives, and route its
|
||||
Preview and Disable actions to the anchored General settings telemetry
|
||||
control rather than duplicating privacy state in shell-local UI.
|
||||
31. Keep maintainer commercial-event controls out of customer settings.
|
||||
The shared general settings privacy panel may expose outbound usage
|
||||
telemetry controls, preview, and reset affordances, but it must not render
|
||||
|
|
|
|||
|
|
@ -982,6 +982,12 @@ 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.
|
||||
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
|
||||
|
|
|
|||
51
docs/releases/RELEASE_NOTES_NEXT_DRAFT.md
Normal file
51
docs/releases/RELEASE_NOTES_NEXT_DRAFT.md
Normal file
|
|
@ -0,0 +1,51 @@
|
|||
# Next Pulse Release — Disclosure Draft
|
||||
|
||||
This draft records customer-visible disclosures that must be carried into the
|
||||
next Pulse release packet. The release version is assigned only when that
|
||||
packet is cut.
|
||||
|
||||
## Outbound usage telemetry schema v2
|
||||
|
||||
This release updates Pulse's legacy unversioned outbound usage telemetry
|
||||
payload to schema v2. Telemetry remains enabled by default unless an operator
|
||||
has disabled it; an existing enabled or disabled choice is preserved on
|
||||
upgrade. The purpose remains aggregate product and release understanding, and
|
||||
the payload remains pseudonymous rather than tied to a Pulse account or
|
||||
person.
|
||||
|
||||
Schema v2 adds these deliberately coarse signal categories:
|
||||
|
||||
- closed deployment-method, known-install-age, activation-stage,
|
||||
time-to-first-monitored-resource, and estate-size buckets;
|
||||
- authentication-configured and monitoring-active booleans, plus an aggregate
|
||||
configured-connection count;
|
||||
- aggregate alert fired, acknowledged, and resolved counts from the existing
|
||||
30-day local window;
|
||||
- aggregate notification attempt, delivery, and failure counts from the
|
||||
existing seven-day local window; and
|
||||
- a boolean indicating whether an operational outcome was observed in the
|
||||
existing 30-day local window.
|
||||
|
||||
The payload does not include names, email addresses, account IDs, hostnames,
|
||||
credentials, infrastructure or resource identifiers, IP addresses, URLs,
|
||||
paths, locale, recipients, notification endpoints, alert or notification
|
||||
content, prompts, chat messages, command text, action output, token values,
|
||||
browser events, or an event-level journey or clickstream. The rotating
|
||||
pseudonymous installation ID continues to rotate every 30 days. Telemetry rows
|
||||
are retained server-side for up to 90 days; request IP addresses are used only
|
||||
transiently for rate limiting and are not stored in telemetry rows.
|
||||
|
||||
Existing installations receive a one-time, non-blocking
|
||||
**Telemetry payload updated** notice after upgrade. It links directly to the
|
||||
exact payload preview, the disable action, and the full privacy disclosure.
|
||||
Fresh installations do not receive a duplicate banner because the current
|
||||
payload and controls are already disclosed during setup.
|
||||
|
||||
## Terminology correction
|
||||
|
||||
Earlier public website copy used an anonymity label that was too strong for a
|
||||
payload containing a rotating installation identifier. The accurate term is
|
||||
**pseudonymous**. The website wording was corrected in July 2026, before
|
||||
schema v2 reached a public Pulse release; the shipped privacy documentation
|
||||
and in-product control identify the rotating pseudonymous ID and the concrete
|
||||
data categories excluded from the payload.
|
||||
|
|
@ -216,6 +216,7 @@ export const GeneralSettingsPanel: Component<GeneralSettingsPanelProps> = (props
|
|||
|
||||
{/* Usage Data + Privacy Card */}
|
||||
<SettingsPanel
|
||||
id="usage-telemetry"
|
||||
title={t('settings.general.telemetry.section.title')}
|
||||
description={t('settings.general.telemetry.section.description')}
|
||||
noPadding
|
||||
|
|
|
|||
|
|
@ -154,6 +154,40 @@ 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();
|
||||
|
|
|
|||
|
|
@ -719,6 +719,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'");
|
||||
expect(generalSettingsPanelSource).not.toContain('license_tier');
|
||||
expect(generalSettingsPanelSource).not.toContain('api_tokens');
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,4 +1,6 @@
|
|||
import { Show, createEffect, createSignal } from 'solid-js';
|
||||
import { useNavigate } from '@solidjs/router';
|
||||
import InfoIcon from 'lucide-solid/icons/info';
|
||||
import XIcon from 'lucide-solid/icons/x';
|
||||
import { updateStore } from '@/stores/updates';
|
||||
import { UpdatesAPI } from '@/api/updates';
|
||||
|
|
@ -6,11 +8,17 @@ import { STORAGE_KEYS } from '@/utils/localStorage';
|
|||
import { ActionIconButton, Button } from '@/components/shared/Button';
|
||||
import { Dialog } from '@/components/shared/Dialog';
|
||||
import { ExternalTextLink } from '@/components/shared/ExternalTextLink';
|
||||
import { InlineNotice } from '@/components/shared/InlineNotice';
|
||||
import { buildReleaseNotesUrl, normalizeReleaseVersion } from '@/components/updateVersion';
|
||||
import { extractHighlights, 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);
|
||||
|
|
@ -27,17 +35,44 @@ 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" dialog. Shows once after the running version
|
||||
* changes, and only when that release has a curated `## Highlights` section
|
||||
* in its GitHub release notes. Dismissing (or a highlights-free release)
|
||||
* records the version so the dialog stays 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.
|
||||
*/
|
||||
export function WhatsNewCard() {
|
||||
const navigate = useNavigate();
|
||||
const [visible, setVisible] = createSignal(false);
|
||||
const [telemetryNoticeVisible, setTelemetryNoticeVisible] = createSignal(false);
|
||||
const [version, setVersion] = createSignal('');
|
||||
const [highlightsHtml, setHighlightsHtml] = createSignal('');
|
||||
const hadPriorReleaseBaseline = readLastSeenVersion() !== null;
|
||||
let checked = false;
|
||||
let telemetryNoticeChecked = false;
|
||||
|
||||
const loadNotes = async (currentVersion: string) => {
|
||||
try {
|
||||
|
|
@ -92,80 +127,162 @@ export function WhatsNewCard() {
|
|||
void loadNotes(currentVersion);
|
||||
});
|
||||
|
||||
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);
|
||||
});
|
||||
|
||||
// Any close path (button, backdrop, Escape) counts as seen.
|
||||
const dismiss = () => {
|
||||
markVersionSeen(version());
|
||||
setVisible(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={visible()}>
|
||||
<Dialog
|
||||
isOpen={visible()}
|
||||
onClose={dismiss}
|
||||
panelClass="max-w-xl"
|
||||
ariaLabelledBy="whats-new-title"
|
||||
>
|
||||
<div class="w-full" data-testid="whats-new-modal">
|
||||
<div class="px-6 py-4 border-b border-border">
|
||||
<div class="flex items-center justify-between gap-3">
|
||||
<div class="flex items-center gap-3 min-w-0">
|
||||
{/* Sparkle icon */}
|
||||
<svg
|
||||
class="w-5 h-5 flex-shrink-0 text-emerald-600 dark:text-emerald-400"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<path
|
||||
d="M12 3l1.9 5.1L19 10l-5.1 1.9L12 17l-1.9-5.1L5 10l5.1-1.9L12 3z"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
/>
|
||||
<path d="M19 15l.9 2.1L22 18l-2.1.9L19 21l-.9-2.1L16 18l2.1-.9L19 15z" />
|
||||
</svg>
|
||||
<div class="min-w-0">
|
||||
<h2 id="whats-new-title" class="text-lg font-semibold text-base-content truncate">
|
||||
What's new in v{version()}
|
||||
</h2>
|
||||
<p class="text-xs text-muted">Pulse updated successfully</p>
|
||||
</div>
|
||||
</div>
|
||||
<ActionIconButton
|
||||
onClick={dismiss}
|
||||
label="Dismiss what's new"
|
||||
title="Close"
|
||||
tone="muted"
|
||||
size="md"
|
||||
<>
|
||||
<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')}
|
||||
>
|
||||
<XIcon class="h-5 w-5" aria-hidden="true" />
|
||||
</ActionIconButton>
|
||||
{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>
|
||||
|
||||
<div
|
||||
class="px-6 py-4 max-h-[60vh] overflow-y-auto text-sm text-base-content [&_ul]:list-disc [&_ul]:pl-5 [&_ul]:space-y-2 [&_ol]:list-decimal [&_ol]:pl-5 [&_ol]:space-y-2 [&_p]:mt-2 [&_a]:underline [&_code]:font-mono [&_code]:text-xs"
|
||||
// eslint-disable-next-line solid/no-innerhtml -- renderMarkdown sanitizes via DOMPurify
|
||||
innerHTML={highlightsHtml()}
|
||||
/>
|
||||
<Show when={visible()}>
|
||||
<Dialog
|
||||
isOpen={visible()}
|
||||
onClose={dismiss}
|
||||
panelClass="max-w-xl"
|
||||
ariaLabelledBy="whats-new-title"
|
||||
>
|
||||
<div class="w-full" data-testid="whats-new-modal">
|
||||
<div class="px-6 py-4 border-b border-border">
|
||||
<div class="flex items-center justify-between gap-3">
|
||||
<div class="flex items-center gap-3 min-w-0">
|
||||
{/* Sparkle icon */}
|
||||
<svg
|
||||
class="w-5 h-5 flex-shrink-0 text-emerald-600 dark:text-emerald-400"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<path
|
||||
d="M12 3l1.9 5.1L19 10l-5.1 1.9L12 17l-1.9-5.1L5 10l5.1-1.9L12 3z"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
/>
|
||||
<path d="M19 15l.9 2.1L22 18l-2.1.9L19 21l-.9-2.1L16 18l2.1-.9L19 15z" />
|
||||
</svg>
|
||||
<div class="min-w-0">
|
||||
<h2
|
||||
id="whats-new-title"
|
||||
class="text-lg font-semibold text-base-content truncate"
|
||||
>
|
||||
What's new in v{version()}
|
||||
</h2>
|
||||
<p class="text-xs text-muted">Pulse updated successfully</p>
|
||||
</div>
|
||||
</div>
|
||||
<ActionIconButton
|
||||
onClick={dismiss}
|
||||
label="Dismiss what's new"
|
||||
title="Close"
|
||||
tone="muted"
|
||||
size="md"
|
||||
type="button"
|
||||
>
|
||||
<XIcon class="h-5 w-5" aria-hidden="true" />
|
||||
</ActionIconButton>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="px-6 py-4 bg-surface-alt border-t border-border flex items-center justify-between gap-3">
|
||||
<ExternalTextLink
|
||||
href={buildReleaseNotesUrl(version())}
|
||||
variant="inline"
|
||||
class="text-sm"
|
||||
>
|
||||
Full release notes →
|
||||
</ExternalTextLink>
|
||||
<Button onClick={dismiss} variant="primary" size="md" type="button">
|
||||
Got it
|
||||
</Button>
|
||||
<div
|
||||
class="px-6 py-4 max-h-[60vh] overflow-y-auto text-sm text-base-content [&_ul]:list-disc [&_ul]:pl-5 [&_ul]:space-y-2 [&_ol]:list-decimal [&_ol]:pl-5 [&_ol]:space-y-2 [&_p]:mt-2 [&_a]:underline [&_code]:font-mono [&_code]:text-xs"
|
||||
// eslint-disable-next-line solid/no-innerhtml -- renderMarkdown sanitizes via DOMPurify
|
||||
innerHTML={highlightsHtml()}
|
||||
/>
|
||||
|
||||
<div class="px-6 py-4 bg-surface-alt border-t border-border flex items-center justify-between gap-3">
|
||||
<ExternalTextLink
|
||||
href={buildReleaseNotesUrl(version())}
|
||||
variant="inline"
|
||||
class="text-sm"
|
||||
>
|
||||
Full release notes →
|
||||
</ExternalTextLink>
|
||||
<Button onClick={dismiss} variant="primary" size="md" type="button">
|
||||
Got it
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Dialog>
|
||||
</Show>
|
||||
</Dialog>
|
||||
</Show>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,6 +4,11 @@ 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: {
|
||||
|
|
@ -25,6 +30,7 @@ describe('WhatsNewCard', () => {
|
|||
beforeEach(() => {
|
||||
versionInfoMock.mockReset();
|
||||
getReleaseNotesMock.mockReset();
|
||||
navigateMock.mockReset();
|
||||
localStorage.clear();
|
||||
});
|
||||
|
||||
|
|
@ -45,8 +51,79 @@ 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 () => {
|
||||
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();
|
||||
|
||||
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.md',
|
||||
);
|
||||
});
|
||||
|
||||
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();
|
||||
});
|
||||
|
||||
it('shows curated highlights after the running release changes', async () => {
|
||||
|
|
@ -114,7 +191,9 @@ 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();
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import { describe, expect, it } from 'vitest';
|
||||
import { extractHighlights, isReleaseVersion } from '../whatsNewModel';
|
||||
import whatsNewCardSource from '../WhatsNewCard.tsx?raw';
|
||||
|
||||
describe('extractHighlights', () => {
|
||||
it('extracts the Highlights section from a release body', () => {
|
||||
|
|
@ -97,3 +98,14 @@ describe('isReleaseVersion', () => {
|
|||
expect(isReleaseVersion('')).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
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');
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -199,6 +199,8 @@ 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');
|
||||
|
|
@ -208,6 +210,8 @@ 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(
|
||||
|
|
|
|||
|
|
@ -336,6 +336,14 @@ 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',
|
||||
|
|
|
|||
|
|
@ -328,6 +328,15 @@ 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',
|
||||
|
|
|
|||
|
|
@ -321,6 +321,14 @@ 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 coarse deployment, lifecycle, estate-size, and aggregate alert and notification outcome signals when outbound usage telemetry is enabled. It still excludes personal details, infrastructure IDs, content, browser events, and 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',
|
||||
|
|
@ -513,6 +521,13 @@ 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',
|
||||
|
|
|
|||
|
|
@ -166,6 +166,7 @@ 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