Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions changelog.d/telemetry-all-settings.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
### Changed
- Anonymous usage counts now also report the light and dark palettes System mode switches between,
the interface font size, the diff layout and whether the usage meters' accounts were ever
reordered (a yes or no, never the accounts), and no longer report the settings removed in this
release.
2 changes: 1 addition & 1 deletion docs/guide.md
Original file line number Diff line number Diff line change
Expand Up @@ -488,7 +488,7 @@ This is for Linux and macOS. On Windows, and for a remote PC, update herdr on th

The app tells its maintainer how many installs there are, which versions they run, which countries the messages come from and which app settings people choose. It sends one message when it is first installed and one each time it runs a new version, never in between. The first time you open the app, a line under the header says so, with **What is sent**, which opens **Settings → About** and its switch. Nothing is sent before that line has been shown, and the first message waits ten minutes after it, so turning the switch off there stops it. An install that saw an older notice sees the expanded-settings notice once more, and sends nothing until then. An existing choice to turn counts off stays off.

A message holds the event (`install` or `update`), a random ID made on this PC, the app's version and the version an update replaced, the OS and CPU architecture, how the app was installed (herdr plugin, `bun run start`, or `bun run server`), and an allowlisted `settings` snapshot. The snapshot contains actual choices for appearance, terminal and chat behavior, notifications, language, voice controls, usage-meter display and the file viewer, including defaults. Custom fonts, shortcuts, key-bar layouts and quick replies are represented by customization flags, not their text or key combinations; quick replies and hidden usage accounts also have counts. Account identifiers and ordering, paths, addresses, credentials, commands, terminal content, conversations and files are not collected.
A message holds the event (`install` or `update`), a random ID made on this PC, the app's version and the version an update replaced, the OS and CPU architecture, how the app was installed (herdr plugin, `bun run start`, or `bun run server`), and an allowlisted `settings` snapshot. The snapshot contains actual choices for appearance, terminal and chat behavior, notifications, language, voice controls, usage-meter display and the file viewer, including defaults. Custom fonts, shortcuts, key-bar layouts, quick replies and the usage meters' account order are represented by customization flags, not their text, key combinations or account names; quick replies and hidden usage accounts also have counts. Account identifiers and the order itself, paths, addresses, credentials, commands, terminal content, conversations and files are not collected.

The snapshot is the last one a browser reported to this server, not a list of devices or a complete picture of a person. Changing a setting updates that local snapshot but does not send another telemetry event. Before a browser reports, `settings` is `null`. The receiver ([`telemetry/`](../telemetry/README.md)) keeps the event fields, the day and the country the message came from (two letters, such as `KR`, worked out by its host from the connection), and never stores your IP address or anything finer than the country. The random install ID and settings can link events from an installation; they do not guarantee anonymity.

Expand Down
38 changes: 16 additions & 22 deletions shared/telemetry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,15 +37,17 @@ export interface TelemetrySettings {
agentPins: boolean;
quietOpenedDone: boolean;
agentMarks: "color" | "mono";
palette: "amber" | "report" | "charcoal" | "catppuccin" | "lilac" | "forest";
palette: "amber" | "charcoal" | "catppuccin";
/** the pair System mode switches between */
paletteLight: "amber" | "charcoal" | "catppuccin";
paletteDark: "amber" | "charcoal" | "catppuccin";
terminalFontSize: number;
terminalCursorStyle: "block" | "bar" | "underline";
terminalCursorBlink: boolean;
terminalWheelSpeed: number;
paneClipboard: boolean;
releasePaneAway: boolean;
narrowPaneFont: boolean;
chatFontSize: number | null;
/** null: the size the screen chose */
uiFontSize: number | null;
chatWidth: "narrow" | "default" | "wide" | "full";
enterSends: boolean;
autoSendQueued: boolean;
Expand All @@ -58,31 +60,26 @@ export interface TelemetrySettings {
alertInApp: boolean;
alertSound: boolean;
showQuickReplies: boolean;
showSuggestionChip: boolean;
showUsage: boolean;
usageCount: "used" | "left";
usagePace: boolean;
usageGlance: "week" | "session";
defaultView: "auto" | "chat" | "terminal";
voiceInput: "auto" | "on" | "off";
voiceLanguage: "auto" | "ar-SA" | "cs-CZ" | "da-DK" | "de-DE" | "el-GR" | "en-GB" | "en-US" | "es-ES" | "fi-FI" | "fr-FR" | "he-IL" | "hi-IN" | "hu-HU" | "id-ID" | "it-IT" | "ja-JP" | "ko-KR" | "nb-NO" | "nl-NL" | "pl-PL" | "pt-BR" | "pt-PT" | "ro-RO" | "ru-RU" | "sk-SK" | "sv-SE" | "th-TH" | "tr-TR" | "uk-UA" | "vi-VN" | "zh-CN" | "zh-TW";
voicePolishChat: boolean;
voicePolishTerminal: boolean;
wrapCode: boolean;
highlightCode: boolean;
markdownWidth: "readable" | "full";
diffStyle: "unified" | "split";
terminalFontCustom: boolean;
chatFontCustom: boolean;
shortcutsCustom: boolean;
keyBarCustom: boolean;
quickRepliesCustom: boolean;
quickReplyCount: number;
usageHiddenCount: number;
/** paging shortcuts (#811): lines kept between pages, and plain PageUp/PageDown while composing */
pageOverlapRows: number;
chatPageKeys: boolean;
/** files one paste, pick or drop may attach (#863) */
attachLimit: number;
/** the usage meters' accounts were put in an order of the user's own */
usageOrderCustom: boolean;
}

/** The browser's full, non-sensitive snapshot. */
Expand All @@ -102,6 +99,7 @@ const nullableIntegerBetween = (min: number, max: number) => (value: unknown): b
value === null || integerBetween(min, max)(value);

// This explicit shared schema must track the producer without importing frontend code at runtime.
const PALETTES = ["amber", "charcoal", "catppuccin"] as const;
const TELEMETRY_SETTINGS_VALIDATORS = {
terminalInputMode: oneOf(["auto", "line", "direct"]),
theme: oneOf(["dark", "light", "system"]),
Expand All @@ -112,15 +110,15 @@ const TELEMETRY_SETTINGS_VALIDATORS = {
agentPins: booleanValue,
quietOpenedDone: booleanValue,
agentMarks: oneOf(["color", "mono"]),
palette: oneOf(["amber", "report", "charcoal", "catppuccin", "lilac", "forest"]),
palette: oneOf(PALETTES),
paletteLight: oneOf(PALETTES),
paletteDark: oneOf(PALETTES),
terminalFontSize: integerBetween(10, 22),
terminalCursorStyle: oneOf(["block", "bar", "underline"]),
terminalCursorBlink: booleanValue,
terminalWheelSpeed: integerBetween(1, 10),
paneClipboard: booleanValue,
releasePaneAway: booleanValue,
narrowPaneFont: booleanValue,
chatFontSize: nullableIntegerBetween(11, 24),
uiFontSize: nullableIntegerBetween(11, 20),
chatWidth: oneOf(["narrow", "default", "wide", "full"]),
enterSends: booleanValue,
autoSendQueued: booleanValue,
Expand All @@ -133,29 +131,25 @@ const TELEMETRY_SETTINGS_VALIDATORS = {
alertInApp: booleanValue,
alertSound: booleanValue,
showQuickReplies: booleanValue,
showSuggestionChip: booleanValue,
showUsage: booleanValue,
usageCount: oneOf(["used", "left"]),
usagePace: booleanValue,
usageGlance: oneOf(["week", "session"]),
defaultView: oneOf(["auto", "chat", "terminal"]),
voiceInput: oneOf(["auto", "on", "off"]),
voiceLanguage: oneOf(["auto", "ar-SA", "cs-CZ", "da-DK", "de-DE", "el-GR", "en-GB", "en-US", "es-ES", "fi-FI", "fr-FR", "he-IL", "hi-IN", "hu-HU", "id-ID", "it-IT", "ja-JP", "ko-KR", "nb-NO", "nl-NL", "pl-PL", "pt-BR", "pt-PT", "ro-RO", "ru-RU", "sk-SK", "sv-SE", "th-TH", "tr-TR", "uk-UA", "vi-VN", "zh-CN", "zh-TW"]),
voicePolishChat: booleanValue,
voicePolishTerminal: booleanValue,
wrapCode: booleanValue,
highlightCode: booleanValue,
markdownWidth: oneOf(["readable", "full"]),
diffStyle: oneOf(["unified", "split"]),
terminalFontCustom: booleanValue,
chatFontCustom: booleanValue,
shortcutsCustom: booleanValue,
keyBarCustom: booleanValue,
quickRepliesCustom: booleanValue,
quickReplyCount: integerBetween(0, 12),
usageHiddenCount: integerBetween(0, 64),
pageOverlapRows: integerBetween(0, 100),
chatPageKeys: booleanValue,
attachLimit: integerBetween(1, 50),
usageOrderCustom: booleanValue,
} satisfies Record<keyof TelemetrySettings, (value: unknown) => boolean>;

/** Every key the Worker (telemetry/worker.ts) accepts in a snapshot, and requires. */
Expand Down
2 changes: 1 addition & 1 deletion src/lib/api.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ it("posts the full safe telemetry snapshot and current notice version", async ()
const body = JSON.parse(String(requestInit?.body)) as Record<string, unknown>;
expect(body).toEqual(change);
expect(body).not.toHaveProperty("changed_settings");
expect(Object.keys(body.settings as object)).toHaveLength(53);
expect(Object.keys(body.settings as object)).toHaveLength(49);
expect(body.settings).not.toHaveProperty("terminalFontFamily");
expect(body.settings).not.toHaveProperty("chatFontFamily");
expect(body.settings).not.toHaveProperty("usageHidden");
Expand Down
12 changes: 10 additions & 2 deletions src/lib/settings.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,10 @@ describe("telemetry settings snapshot", () => {
voiceLanguage: "ko-KR",
wrapCode: true,
markdownWidth: "full",
paletteLight: "charcoal",
paletteDark: "amber",
uiFontSize: 18,
diffStyle: "split",
});
const snapshot = telemetrySettingsSnapshot(settings);

Expand All @@ -101,8 +105,6 @@ describe("telemetry settings snapshot", () => {
agentMarks: "mono",
palette: "catppuccin",
terminalFontSize: 22,
terminalCursorStyle: "block",
terminalCursorBlink: true,
terminalWheelSpeed: 10,
chatFontSize: 11,
chatWidth: "wide",
Expand All @@ -116,6 +118,10 @@ describe("telemetry settings snapshot", () => {
voiceLanguage: "ko-KR",
wrapCode: true,
markdownWidth: "full",
paletteLight: "charcoal",
paletteDark: "amber",
uiFontSize: 18,
diffStyle: "split",
});
expect(telemetrySettings(snapshot)).toEqual(snapshot);
expect(telemetrySettingsSnapshot(DEFAULT_SETTINGS)).toMatchObject({
Expand All @@ -126,6 +132,7 @@ describe("telemetry settings snapshot", () => {
quickRepliesCustom: false,
quickReplyCount: DEFAULT_SETTINGS.quickReplies.length,
usageHiddenCount: 0,
usageOrderCustom: false,
});
});

Expand All @@ -151,6 +158,7 @@ describe("telemetry settings snapshot", () => {
quickRepliesCustom: true,
quickReplyCount: 1,
usageHiddenCount: 1,
usageOrderCustom: true,
});
for (const key of ["keyBarExtras", "keyBarItems", "shortcutOverrides", "terminalFontFamily", "chatFontFamily", "quickReplies", "usageOrder", "usageHidden"]) {
expect(snapshot).not.toHaveProperty(key);
Expand Down
22 changes: 6 additions & 16 deletions src/lib/settings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -164,11 +164,7 @@ export type DiffStyle = (typeof DIFF_STYLES)[number];
* Settings field in the projection makes a new field require an explicit collection decision. */
type TelemetrySourceSettings = Omit<Settings,
"keyBarExtras" | "keyBarItems" | "shortcutOverrides" | "terminalFontFamily" | "chatFontFamily"
| "quickReplies" | "usageOrder" | "usageHidden"
// not collected: the report's schema (shared/telemetry.ts) has no field for it yet
| "diffStyle" | "uiFontSize"
// not collected: only System mode reads them, and `palette` is already reported
| "paletteLight" | "paletteDark">;
| "quickReplies" | "usageOrder" | "usageHidden">;

/** Compare structured custom settings by content against this app's built-in defaults. */
function differsFromDefault(value: unknown, defaultValue: unknown): boolean {
Expand All @@ -188,15 +184,14 @@ export function telemetrySettingsSnapshot(settings: Settings): TelemetrySettings
quietOpenedDone: settings.quietOpenedDone,
agentMarks: settings.agentMarks,
palette: settings.palette,
paletteLight: settings.paletteLight,
paletteDark: settings.paletteDark,
terminalFontSize: settings.terminalFontSize,
// removed settings the report's schema still names: their fixed values, so the Worker needs no redeploy
terminalCursorStyle: "block",
terminalCursorBlink: true,
terminalWheelSpeed: settings.terminalWheelSpeed,
paneClipboard: settings.paneClipboard,
releasePaneAway: settings.releasePaneAway,
narrowPaneFont: true,
chatFontSize: settings.chatFontSize,
uiFontSize: settings.uiFontSize,
chatWidth: settings.chatWidth,
enterSends: settings.enterSends,
autoSendQueued: settings.autoSendQueued,
Expand All @@ -209,19 +204,17 @@ export function telemetrySettingsSnapshot(settings: Settings): TelemetrySettings
alertInApp: settings.alertInApp,
alertSound: settings.alertSound,
showQuickReplies: settings.showQuickReplies,
showSuggestionChip: true,
showUsage: settings.showUsage,
usageCount: settings.usageCount,
usagePace: false,
usageGlance: settings.usageGlance,
defaultView: settings.defaultView,
voiceInput: settings.voiceInput,
voiceLanguage: settings.voiceLanguage,
voicePolishChat: settings.voicePolishChat,
voicePolishTerminal: settings.voicePolishTerminal,
wrapCode: settings.wrapCode,
highlightCode: true,
markdownWidth: settings.markdownWidth,
diffStyle: settings.diffStyle,
terminalFontCustom: settings.terminalFontFamily !== DEFAULT_SETTINGS.terminalFontFamily,
chatFontCustom: settings.chatFontFamily !== DEFAULT_SETTINGS.chatFontFamily,
shortcutsCustom: differsFromDefault(settings.shortcutOverrides, DEFAULT_SETTINGS.shortcutOverrides),
Expand All @@ -230,10 +223,7 @@ export function telemetrySettingsSnapshot(settings: Settings): TelemetrySettings
quickRepliesCustom: differsFromDefault(settings.quickReplies, DEFAULT_SETTINGS.quickReplies),
quickReplyCount: settings.quickReplies.length,
usageHiddenCount: settings.usageHidden.length,
pageOverlapRows: 1,
chatPageKeys: true,
// the app takes 100 files at once; the schema stops at 50
attachLimit: 50,
usageOrderCustom: settings.usageOrder.length > 0,
};
}

Expand Down
2 changes: 1 addition & 1 deletion telemetry/deployed.json
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
{
"sha256": "a6d21ff852456adf7ed361d11bf28bc640b52c4e3fad2b4165d01c69665a30ba",
"sha256": "7e4530b493ff8e74f9ea99f5810319b7a3a995d5d398f85ceb4d95ff17b25729",
"about": "sha256 of telemetry/worker.ts + shared/telemetry.ts as last deployed; written by bun scripts/release-guards.ts --worker-deployed after bunx wrangler deploy"
}
14 changes: 5 additions & 9 deletions telemetry/worker.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,14 +20,14 @@ const settings: TelemetrySettings = {
quietOpenedDone: true,
agentMarks: "mono",
palette: "catppuccin",
paletteLight: "amber",
paletteDark: "catppuccin",
terminalFontSize: 22,
terminalCursorStyle: "underline",
terminalCursorBlink: true,
terminalWheelSpeed: 10,
paneClipboard: false,
releasePaneAway: true,
narrowPaneFont: false,
chatFontSize: 24,
uiFontSize: null,
chatWidth: "narrow",
enterSends: true,
autoSendQueued: false,
Expand All @@ -40,29 +40,25 @@ const settings: TelemetrySettings = {
alertInApp: true,
alertSound: false,
showQuickReplies: true,
showSuggestionChip: false,
showUsage: true,
usageCount: "left",
usagePace: true,
usageGlance: "session",
defaultView: "terminal",
voiceInput: "auto",
voiceLanguage: "zh-TW",
voicePolishChat: true,
voicePolishTerminal: false,
wrapCode: true,
highlightCode: false,
markdownWidth: "full",
diffStyle: "split",
terminalFontCustom: true,
chatFontCustom: false,
shortcutsCustom: true,
keyBarCustom: false,
quickRepliesCustom: true,
quickReplyCount: 12,
usageHiddenCount: 64,
pageOverlapRows: 1,
chatPageKeys: false,
attachLimit: 20,
usageOrderCustom: true,
};
const now = new Date("2026-10-08T13:45:00Z");

Expand Down
Loading