Add transparent telemetry payload update notice

This commit is contained in:
rcourtman 2026-07-23 10:23:13 +01:00
parent 2323c96385
commit a082d5588f
16 changed files with 419 additions and 61 deletions

View file

@ -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`

View file

@ -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

View file

@ -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

View file

@ -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

View 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.

View file

@ -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

View file

@ -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();

View file

@ -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');
});

View file

@ -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>
</>
);
}

View file

@ -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();
});
});

View file

@ -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');
});
});

View file

@ -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(

View file

@ -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',

View file

@ -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',

View file

@ -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',

View file

@ -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',