chore: update telemetry link and add privacy link to feedback form (#15864)

* chore: update telemetry link and add privacy link to feedback form

Bending the rules a little with 1 PR to resolve 2 related issues as they touch 2
of the same files.

We have had feedback from RH legal on two things:
- #15862: The telemetry infoLink is not a _privacy_ statement and shouldn't be
  called one. The proposed change is from 'Read our privacy statement' to 'For
  more information read our statement'.
- #15861: The feedback form offers to collect your email address, so it should
  link to a separate url that is a privacy statement.

This change:
- Updates the infoLink to the proposed text.
- Adds a privacyLink and privacyURL to product.json.
- Adds the new privacy link to the feedback form (identical to telemetry link
  in WelcomePage).

Used onMount instead of $derived in FeedbackForm because I do not want to trigger
Svelte 5 migration as part of this issue.

Fixes #15862.
Fixes #15861.

Signed-off-by: Tim deBoer <git@tdeboer.ca>

* chore: change telemetry info and privacy to objects

We had infoLink/infoURL and privacyLink/privacyURL. This changes them both to be
simple objects with link/url properties.

There is a bunch of change, but overall it is slightly simpler and better
structured since info and privacy are still optional, but once you provide them
the properties aren't.

Signed-off-by: Tim deBoer <git@tdeboer.ca>

---------

Signed-off-by: Tim deBoer <git@tdeboer.ca>
This commit is contained in:
Tim deBoer 2026-01-22 14:57:55 -05:00 committed by GitHub
parent 7dd6e28464
commit 9cdef69faa
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 116 additions and 32 deletions

View file

@ -18,6 +18,12 @@
export interface TelemetryMessages {
acceptMessage: string;
infoLink?: string;
infoURL?: string;
info?: {
link: string;
url: string;
};
privacy?: {
link: string;
url: string;
};
}

View file

@ -399,8 +399,10 @@ describe('aggregateTrack', () => {
expect(messages.acceptMessage).toBe(
'Help improve Podman Desktop by allowing Red Hat to collect anonymous usage data.',
);
expect(messages.infoLink).toBe('Read our privacy statement');
expect(messages.infoURL).toBe('https://developers.redhat.com/article/tool-data-collection');
expect(messages.info?.link).toBe('For more information read our statement');
expect(messages.info?.url).toBe('https://developers.redhat.com/article/tool-data-collection');
expect(messages.privacy?.link).toBe('Read our privacy statement');
expect(messages.privacy?.url).toBe('https://www.redhat.com/en/about/privacy-policy');
});
test('should register telemetry preference correctly', async () => {
@ -411,7 +413,7 @@ describe('aggregateTrack', () => {
expect.objectContaining({
properties: expect.objectContaining({
'telemetry.enabled': expect.objectContaining({
markdownDescription: `${messages.acceptMessage} [${messages.infoLink}](${messages.infoURL})`,
markdownDescription: `${messages.acceptMessage} [${messages.info?.link}](${messages.info?.url})`,
}),
}),
}),
@ -420,13 +422,17 @@ describe('aggregateTrack', () => {
test('should return custom telemetry message', () => {
vi.mocked(product).telemetry.acceptMessage = 'Accept message';
vi.mocked(product).telemetry.infoLink = 'Privacy message';
vi.mocked(product).telemetry.infoURL = 'privacy-url';
vi.mocked(product).telemetry.info.link = 'Info message';
vi.mocked(product).telemetry.info.url = 'info-url';
vi.mocked(product).telemetry.privacy.link = 'Privacy message';
vi.mocked(product).telemetry.privacy.url = 'privacy-url';
const messages = telemetry.getTelemetryMessages();
expect(messages.acceptMessage).toBe('Accept message');
expect(messages.infoLink).toBe('Privacy message');
expect(messages.infoURL).toBe('privacy-url');
expect(messages.info?.link).toBe('Info message');
expect(messages.info?.url).toBe('info-url');
expect(messages.privacy?.link).toBe('Privacy message');
expect(messages.privacy?.url).toBe('privacy-url');
});
test('preference should be formatted correctly when no link is provided', async () => {

View file

@ -110,17 +110,16 @@ export class Telemetry {
async init(): Promise<void> {
const telemetryMessages = this.getTelemetryMessages();
const telemetryLink =
telemetryMessages.infoLink && telemetryMessages.infoURL
? ` [${telemetryMessages.infoLink}](${telemetryMessages.infoURL})`
: '';
const telemetryInfo = telemetryMessages.info
? ` [${telemetryMessages.info.link}](${telemetryMessages.info.url})`
: '';
const telemetryConfigurationNode: IConfigurationNode = {
id: 'preferences.telemetry',
title: 'Telemetry',
type: 'object',
properties: {
[TelemetrySettings.SectionName + '.' + TelemetrySettings.Enabled]: {
markdownDescription: `${telemetryMessages.acceptMessage}${telemetryLink}`,
markdownDescription: `${telemetryMessages.acceptMessage}${telemetryInfo}`,
type: 'boolean',
default: true,
},
@ -184,8 +183,18 @@ export class Telemetry {
getTelemetryMessages(): TelemetryMessages {
return {
acceptMessage: product.telemetry.acceptMessage,
infoLink: product.telemetry.infoLink,
infoURL: product.telemetry.infoURL,
info: product.telemetry.info
? {
link: product.telemetry.info?.link,
url: product.telemetry.info?.url,
}
: undefined,
privacy: product.telemetry.privacy
? {
link: product.telemetry.privacy?.link,
url: product.telemetry.privacy?.url,
}
: undefined,
} as TelemetryMessages;
}

View file

@ -1,5 +1,5 @@
/**********************************************************************
* Copyright (C) 2024 Red Hat, Inc.
* Copyright (C) 2024-2026 Red Hat, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@ -18,8 +18,11 @@
import '@testing-library/jest-dom/vitest';
import { render, screen } from '@testing-library/svelte';
import { expect, test } from 'vitest';
import { fireEvent, render, screen } from '@testing-library/svelte';
import { tick } from 'svelte';
import { expect, test, vi } from 'vitest';
import type { TelemetryMessages } from '/@api/telemetry';
import FeedbackForm from './FeedbackForm.svelte';
@ -29,3 +32,34 @@ test('something', () => {
expect(screen.getByLabelText('validation and buttons')).toBeInTheDocument();
expect(screen.getByLabelText('validation')).toBeInTheDocument();
});
test('Expect privacy statement is missing from the UI when not provided', async () => {
render(FeedbackForm);
await tick();
const privacyLink = screen.queryByRole('link');
expect(privacyLink).not.toBeInTheDocument();
});
test('Expect privacy statement is included when it exists', async () => {
const telem: TelemetryMessages = {
acceptMessage: 'Help improve the product',
privacy: {
link: 'Click here',
url: 'privacy-url',
},
};
vi.mocked(window.getTelemetryMessages).mockResolvedValue(telem);
render(FeedbackForm);
await tick();
const privacyLink = screen.getByRole('link');
expect(privacyLink).toBeInTheDocument();
expect(privacyLink.textContent).toEqual(telem.privacy?.link);
await fireEvent.click(privacyLink);
await vi.waitFor(() => expect(vi.mocked(window.openExternal)).toBeCalledWith(telem.privacy?.url));
});

View file

@ -1,6 +1,27 @@
<script lang="ts">
import { Link } from '@podman-desktop/ui-svelte';
import { onMount } from 'svelte';
import type { TelemetryMessages } from '/@api/telemetry';
let telemetryMessages: TelemetryMessages;
onMount(async () => {
telemetryMessages = await window.getTelemetryMessages();
});
</script>
<div>
<div class="relative max-h-80 overflow-auto text-[var(--pd-modal-text)] px-10 pb-4" aria-label="content">
<slot name="content" />
{#if telemetryMessages?.privacy}
<div class="pt-6">
<Link
on:click={async (): Promise<void> => {
await window.openExternal(telemetryMessages.privacy?.url ?? '');
}}>{telemetryMessages?.privacy.link}</Link>
</div>
{/if}
</div>
<div class="px-5 py-5 mt-2 flex flex-row w-full space-x-5" aria-label="validation and buttons">

View file

@ -1,5 +1,5 @@
/**********************************************************************
* Copyright (C) 2024 Red Hat, Inc.
* Copyright (C) 2024-2026 Red Hat, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@ -41,6 +41,7 @@ beforeAll(() => {
writeText: vi.fn(),
},
},
getTelemetryMessages: vi.fn(),
},
writable: true,
});

View file

@ -97,8 +97,10 @@ test('Expect that telemetry messages is visible', async () => {
test('Expect that telemetry link opens url', async () => {
const telem: TelemetryMessages = {
acceptMessage: 'Help improve the product',
infoLink: 'Click here',
infoURL: 'privacy-url',
info: {
link: 'Click here',
url: 'info-url',
},
};
vi.mocked(window.getTelemetryMessages).mockResolvedValue(telem);
@ -106,23 +108,22 @@ test('Expect that telemetry link opens url', async () => {
const accept = screen.getByText(telem.acceptMessage);
expect(accept).toBeInTheDocument();
const infoLink = screen.getByText(telem.infoLink ?? '');
const infoLink = screen.getByText(telem.info?.link ?? '');
expect(infoLink).toBeInTheDocument();
await fireEvent.click(infoLink);
await vi.waitFor(() => expect(vi.mocked(window.openExternal)).toBeCalledWith(telem.infoURL));
await vi.waitFor(() => expect(vi.mocked(window.openExternal)).toBeCalledWith(telem.info?.url));
});
test('Expect that telemetry link is missing when url is not provided', async () => {
test('Expect that telemetry link is missing when info is not provided', async () => {
const telem = {
acceptMessage: 'Help improve the product',
infoLink: 'Click here',
} as TelemetryMessages;
vi.mocked(window.getTelemetryMessages).mockResolvedValue(telem);
await waitRender({ showWelcome: true, showTelemetry: true });
const infoLink = screen.queryByText(telem.infoLink ?? '');
const infoLink = screen.queryByRole('link');
expect(infoLink).not.toBeInTheDocument();
});

View file

@ -166,11 +166,11 @@ function startOnboardingQueue(): void {
<div class="w-2/5 text-[var(--pd-content-card-text)]">
{#if telemetryMessages}
{telemetryMessages.acceptMessage}
{#if telemetryMessages?.infoLink && telemetryMessages?.infoURL}
{#if telemetryMessages?.info}
<Link
on:click={async (): Promise<void> => {
await window.openExternal(telemetryMessages.infoURL ?? '');
}}>{telemetryMessages?.infoLink}</Link>
await window.openExternal(telemetryMessages.info?.url ?? '');
}}>{telemetryMessages?.info.link}</Link>
{/if}
{/if}
</div>

View file

@ -16,8 +16,14 @@
"telemetry": {
"key": "Mhl7GXADk5M1vG6r9FXztbCqWRQY8XPy",
"acceptMessage": "Help improve Podman Desktop by allowing Red Hat to collect anonymous usage data.",
"infoLink": "Read our privacy statement",
"infoURL": "https://developers.redhat.com/article/tool-data-collection"
"info": {
"link": "For more information read our statement",
"url": "https://developers.redhat.com/article/tool-data-collection"
},
"privacy": {
"link": "Read our privacy statement",
"url": "https://www.redhat.com/en/about/privacy-policy"
}
},
"catalog": {
"default": "https://registry.podman-desktop.io/api/extensions.json"