Skip to content

Commit 095955b

Browse files
committed
fix: count installs driven through openscreen mcp as active
The anonymous daily ping only ran from the GUI startup path, so an install used only by an agent through `openscreen mcp` never counted. The MCP server now sends the same ping once it is listening, then every 6 hours like the app. It shares the GUI's state file in userData, so an install used through both on one UTC day counts once, under whichever surface pinged first, and it honours the same opt-outs (tray toggle, DO_NOT_TRACK, CI, dev builds). It writes nothing to stdout, which is the MCP protocol stream. Each event now carries `surface`: "app" or "mcp". The other CLI commands stay silent: they run in scripts and CI more often than for a person. The FAQ and privacy page list the new field in every locale.
1 parent e9c5ed9 commit 095955b

21 files changed

Lines changed: 161 additions & 17 deletions

File tree

‎electron/cli/cliMain.test.ts‎

Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
// `openscreen mcp` sends the GUI's anonymous usage ping: an install driven only by an agent
2+
// counts as active. runCli runs for real here; only Electron and the heavy services around
3+
// the MCP server are stubbed.
4+
5+
import { mkdtempSync, rmSync } from "node:fs";
6+
import os from "node:os";
7+
import path from "node:path";
8+
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
9+
import { sendUsagePing, setUsagePingEnabled } from "../usage-ping";
10+
import { runCli } from "./cliMain";
11+
12+
const mocks = vi.hoisted(() => ({ userData: "", serve: vi.fn(async () => undefined) }));
13+
14+
vi.mock("electron", () => ({
15+
app: {
16+
getPath: () => mocks.userData,
17+
getVersion: () => "2.1.0",
18+
exit: vi.fn(),
19+
on: vi.fn(),
20+
whenReady: () => Promise.resolve(),
21+
commandLine: { appendSwitch: vi.fn() },
22+
},
23+
BrowserWindow: class {},
24+
ipcMain: { handle: vi.fn() },
25+
Menu: { setApplicationMenu: vi.fn(), buildFromTemplate: vi.fn() },
26+
session: {
27+
defaultSession: {
28+
setPermissionCheckHandler: vi.fn(),
29+
setPermissionRequestHandler: vi.fn(),
30+
setDisplayMediaRequestHandler: vi.fn(),
31+
},
32+
},
33+
systemPreferences: {},
34+
}));
35+
vi.mock("../install-channel", () => ({ getInstallChannel: () => "nsis" }));
36+
vi.mock("../ipc/handlers", () => ({
37+
getSelectedDesktopSource: () => null,
38+
registerIpcHandlers: () => ({ agentServices: {} }),
39+
}));
40+
vi.mock("../mcp/hidden-video", () => ({ openVideoInHiddenWindow: vi.fn() }));
41+
vi.mock("../mcp/stdio-server", () => ({ serveMcpOverStdio: mocks.serve }));
42+
vi.mock("../stt", () => ({ registerSttIpc: vi.fn(), shutdownStt: vi.fn() }));
43+
vi.mock("../windows", () => ({ createRunnerWindow: vi.fn() }));
44+
vi.mock("./runnerHost", () => ({ createRunnerHost: () => ({}) }));
45+
vi.mock("./sessionData", () => ({ isolateCliSessionData: vi.fn() }));
46+
47+
const fetchMock = vi.fn(async (_url: string, _init: RequestInit) => ({ ok: true }));
48+
const savedConsole = { ...console };
49+
let stdoutWrite: ReturnType<typeof vi.spyOn>;
50+
51+
beforeEach(() => {
52+
mocks.userData = mkdtempSync(path.join(os.tmpdir(), "cli-usage-ping-"));
53+
mocks.serve.mockClear();
54+
fetchMock.mockClear();
55+
vi.stubGlobal("fetch", fetchMock);
56+
vi.stubEnv("DO_NOT_TRACK", "");
57+
vi.stubEnv("CI", "");
58+
// The darwin branch touches the dock and menu, which this test is not about.
59+
vi.spyOn(process, "platform", "get").mockReturnValue("linux");
60+
// runCli's fatal-error handlers would outlive the test and exit the worker.
61+
vi.spyOn(process, "on").mockReturnValue(process);
62+
stdoutWrite = vi.spyOn(process.stdout, "write");
63+
});
64+
65+
afterEach(() => {
66+
vi.unstubAllGlobals();
67+
vi.unstubAllEnvs();
68+
vi.restoreAllMocks();
69+
Object.assign(console, savedConsole);
70+
rmSync(mocks.userData, { recursive: true, force: true });
71+
});
72+
73+
/** Runs `openscreen mcp` up to the point its server is listening and the ping went out. */
74+
async function runMcp() {
75+
runCli({ kind: "mcp" });
76+
await vi.waitFor(() => expect(mocks.serve).toHaveBeenCalled());
77+
await new Promise((resolve) => setTimeout(resolve, 0));
78+
}
79+
80+
describe("openscreen mcp usage ping", () => {
81+
it("pings once, as the mcp surface, and writes nothing to stdout", async () => {
82+
await runMcp();
83+
expect(fetchMock).toHaveBeenCalledTimes(1);
84+
const body = JSON.parse(String(fetchMock.mock.calls[0][1].body));
85+
expect(body.properties).toMatchObject({
86+
version: "2.1.0",
87+
channel: "nsis",
88+
surface: "mcp",
89+
});
90+
expect(stdoutWrite).not.toHaveBeenCalled();
91+
});
92+
93+
it("honours the tray toggle and DO_NOT_TRACK", async () => {
94+
setUsagePingEnabled(mocks.userData, false);
95+
await runMcp();
96+
setUsagePingEnabled(mocks.userData, true);
97+
vi.stubEnv("DO_NOT_TRACK", "1");
98+
await runMcp();
99+
expect(fetchMock).not.toHaveBeenCalled();
100+
});
101+
102+
it("counts a day the app already counted only once", async () => {
103+
const facts = { version: "2.1.0", os: "linux", arch: "x64", channel: "nsis" } as const;
104+
await sendUsagePing(mocks.userData, { ...facts, surface: "app" });
105+
fetchMock.mockClear();
106+
await runMcp();
107+
expect(fetchMock).not.toHaveBeenCalled();
108+
});
109+
});

‎electron/cli/cliMain.ts‎

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,12 @@ import { app, BrowserWindow, ipcMain, Menu, session, systemPreferences } from "e
99
import type { CliProgressEvent, CliSourcesResult } from "../../src/lib/cliContracts";
1010
import { renameWithRetry } from "../atomic-rename";
1111
import { isDiagnosticModeEnabled } from "../diagnostics/main-log-buffer";
12+
import { getInstallChannel } from "../install-channel";
1213
import { getSelectedDesktopSource, registerIpcHandlers } from "../ipc/handlers";
1314
import { openVideoInHiddenWindow } from "../mcp/hidden-video";
1415
import { serveMcpOverStdio } from "../mcp/stdio-server";
1516
import { registerSttIpc, shutdownStt } from "../stt";
17+
import { sendUsagePing, USAGE_PING_INTERVAL_MS } from "../usage-ping";
1618
import { createRunnerWindow } from "../windows";
1719
import { CLI_USAGE, type CliCommand } from "./args";
1820
import { openGuiFromCli } from "./openGui";
@@ -409,6 +411,19 @@ export function runCli(command: CliCommand): void {
409411
onClose: () => void shutdownStt().finally(() => app.exit(0)),
410412
});
411413
milestone("mcp server listening on stdio");
414+
// An install driven only by an agent is an active install too. Same ping, state
415+
// file and opt-outs as the GUI's, and it writes nothing: stdout is the protocol.
416+
// The other commands stay silent: they are scripts and CI more often than people.
417+
const pingUsage = () =>
418+
void sendUsagePing(userDataDir, {
419+
version: app.getVersion(),
420+
os: process.platform,
421+
arch: process.arch,
422+
channel: getInstallChannel(),
423+
surface: "mcp",
424+
});
425+
pingUsage();
426+
setInterval(pingUsage, USAGE_PING_INTERVAL_MS).unref();
412427
return;
413428
}
414429

‎electron/main.ts‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -689,6 +689,7 @@ function runUsagePing() {
689689
os: process.platform,
690690
arch: process.arch,
691691
channel: getInstallChannel(),
692+
surface: "app",
692693
});
693694
}
694695

‎electron/usage-ping.test.ts‎

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,13 @@ function tmp(): string {
2626
}
2727

2828
const fresh: UsagePingState = { enabled: true, day: null, week: null, month: null };
29-
const facts: UsagePingFacts = { version: "2.0.0", os: "win32", arch: "x64", channel: "store" };
29+
const facts: UsagePingFacts = {
30+
version: "2.0.0",
31+
os: "win32",
32+
arch: "x64",
33+
channel: "store",
34+
surface: "app",
35+
};
3036
// Wednesday 14 October 2026.
3137
const wednesday = new Date("2026-10-14T09:00:00Z");
3238

@@ -96,6 +102,7 @@ describe("sendUsagePing", () => {
96102
os: "win32",
97103
arch: "x64",
98104
channel: "store",
105+
surface: "app",
99106
first_this_week: true,
100107
first_this_month: true,
101108
});

‎electron/usage-ping.ts‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,9 @@ export interface UsagePingFacts {
3434
os: NodeJS.Platform;
3535
arch: string;
3636
channel: InstallChannel;
37+
/** What the install was used through: the app's window, or `openscreen mcp` for an agent.
38+
* Both share one state file, so a day counts once, under whichever pinged first. */
39+
surface: "app" | "mcp";
3740
}
3841

3942
const DEFAULT_STATE: UsagePingState = { enabled: true, day: null, week: null, month: null };
@@ -139,6 +142,7 @@ export async function sendUsagePing(
139142
os: facts.os,
140143
arch: facts.arch,
141144
channel: facts.channel,
145+
surface: facts.surface,
142146
first_this_week: plan.firstThisWeek,
143147
first_this_month: plan.firstThisMonth,
144148
},

‎website/docs/faq.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ OpenScreen is a free, MIT-licensed screen recorder and video editor for Windows,
3636
- **github.com, once per language.** On a machine with a GPU, the first English or French transcription also downloads the model that re-times each word: about 110 MB for English, 350 MB for French.
3737
- **github.com and api.github.com.** Builds that update themselves check for a new release every 24 hours, and when you ask. By default they only tell you one is available.
3838
- **fonts.googleapis.com, only for a font added in 1.x.** A custom annotation font added in an earlier version is still fetched at launch, though 2.x no longer draws it.
39-
- **PostHog, once a day.** An anonymous count of active installs, sent to PostHog's EU servers. It holds the app version, the operating system, the processor architecture, how the app was installed, and whether it is the first count of the week and of the month. That is all: no installation identifier (each count gets a new random ID), no IP address kept, nothing about your recordings. Untick **Send Anonymous Usage Count** in the tray menu to turn it off, or set `DO_NOT_TRACK=1`.
39+
- **PostHog, once a day.** An anonymous count of active installs, sent to PostHog's EU servers. It holds the app version, the operating system, the processor architecture, how the app was installed, whether it was used directly or through its MCP server by an AI agent, and whether it is the first count of the week and of the month. That is all: no installation identifier (each count gets a new random ID), no IP address kept, nothing about your recordings. Untick **Send Anonymous Usage Count** in the tray menu to turn it off, or set `DO_NOT_TRACK=1`.
4040
- **Your AI provider, only if you connect one.** Chat editing sends your messages and the project data it reads, such as the timeline and transcript. Caption translation sends the caption text. Both stay off until you connect a provider. See [AI editing](./ai-editing.md).
4141

4242
## Does OpenScreen collect analytics or crash reports?

‎website/i18n/de/docusaurus-plugin-content-docs/current/faq.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ OpenScreen ist ein kostenloser Bildschirmrekorder und Videoeditor unter MIT-Lize
3636
- **github.com, einmal pro Sprache.** Auf einem Rechner mit GPU lädt die erste englische oder französische Transkription zusätzlich das Modell herunter, das jedes Wort neu zeitlich ausrichtet: etwa 110 MB für Englisch, 350 MB für Französisch.
3737
- **github.com und api.github.com.** Builds, die sich selbst aktualisieren, suchen alle 24 Stunden und auf deine Anfrage nach einer neuen Version. Standardmäßig melden sie nur, dass eine verfügbar ist.
3838
- **fonts.googleapis.com, nur für eine in 1.x hinzugefügte Schrift.** Eine eigene Schrift für Annotationen, die in einer früheren Version hinzugefügt wurde, wird beim Start weiterhin geladen, obwohl 2.x sie nicht mehr zeichnet.
39-
- **PostHog, einmal täglich.** Eine anonyme Zählung aktiver Installationen, gesendet an die EU-Server von PostHog. Sie enthält die App-Version, das Betriebssystem, die Prozessorarchitektur, die Art der Installation und ob es die erste Zählung der Woche und des Monats ist. Mehr nicht: keine Installationskennung (jede Zählung erhält eine neue Zufalls-ID), keine gespeicherte IP-Adresse, nichts über deine Aufnahmen. Entferne im Menü des Symbols im Infobereich das Häkchen bei **Anonyme Nutzungszählung senden**, um sie abzuschalten, oder setze `DO_NOT_TRACK=1`.
39+
- **PostHog, einmal täglich.** Eine anonyme Zählung aktiver Installationen, gesendet an die EU-Server von PostHog. Sie enthält die App-Version, das Betriebssystem, die Prozessorarchitektur, die Art der Installation, ob die App direkt oder über ihren MCP-Server von einem KI-Agenten genutzt wurde, und ob es die erste Zählung der Woche und des Monats ist. Mehr nicht: keine Installationskennung (jede Zählung erhält eine neue Zufalls-ID), keine gespeicherte IP-Adresse, nichts über deine Aufnahmen. Entferne im Menü des Symbols im Infobereich das Häkchen bei **Anonyme Nutzungszählung senden**, um sie abzuschalten, oder setze `DO_NOT_TRACK=1`.
4040
- **Dein KI-Anbieter, nur wenn du einen verbindest.** Die Chat-Bearbeitung sendet deine Nachrichten und die Projektdaten, die sie liest, etwa Zeitleiste und Transkript. Die Untertitelübersetzung sendet den Untertiteltext. Beides bleibt aus, bis du einen Anbieter verbindest. Siehe [KI-Bearbeitung](./ai-editing.md).
4141

4242
## Sammelt OpenScreen Analysedaten oder Absturzberichte? {#does-openscreen-collect-analytics-or-crash-reports}

‎website/i18n/de/docusaurus-plugin-content-pages/privacy.mdx‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ description: "Was die Desktop-App, die Website, der MCP-Server und das Agent-Plu
55

66
# Datenschutzerklärung
77

8-
Zuletzt aktualisiert: 18. Oktober 2026.
8+
Zuletzt aktualisiert: 19. Oktober 2026.
99

1010
OpenScreen ist ein kostenloser Open-Source-Bildschirmrekorder und Videoeditor. Es hat kein Konto und keine eigenen Server: Deine Aufnahmen, Projekte und Transkripte werden auf deinem Computer gespeichert, und keine Funktion lädt eine Aufnahme hoch. Diese Seite listet alles auf, was deinen Rechner tatsächlich verlässt, von der Desktop-App, dieser Website, dem MCP-Server und dem Agent-Plugin, einschließlich der Projektdaten, die die KI-Funktionen an den Anbieter senden, den du wählst.
1111

@@ -21,6 +21,7 @@ Höchstens einmal täglich meldet die App an PostHog, auf dessen EU-Servern, das
2121

2222
- die App-Version, das Betriebssystem und die Prozessorarchitektur;
2323
- wie die App installiert wurde (Installer, Microsoft Store, AppImage, Flatpak und so weiter);
24+
- ob die App direkt oder über ihren MCP-Server von einem KI-Agenten genutzt wurde;
2425
- ob es die erste Zählung der Installation in der Woche und im Monat ist.
2526

2627
Sonst enthält sie nichts: keine Installationskennung (jede Zählung erhält eine neue Zufalls-ID), keinen Namen, keine Dateinamen, nichts über deine Aufnahmen. Die Standortbestimmung ist abgeschaltet, und das PostHog-Projekt ist so eingestellt, dass IP-Adressen verworfen werden. Der Zweck ist zu wissen, wie viele Menschen OpenScreen nutzen und auf welchen Systemen; das ist das berechtigte Interesse des Projekts.

‎website/i18n/es/docusaurus-plugin-content-docs/current/faq.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ OpenScreen es un grabador de pantalla y editor de video gratis, con licencia MIT
3636
- **github.com, una vez por idioma.** En un equipo con GPU, la primera transcripción en inglés o en francés también descarga el modelo que vuelve a sincronizar cada palabra: unos 110 MB para el inglés, 350 MB para el francés.
3737
- **github.com y api.github.com.** Las compilaciones que se actualizan solas buscan una nueva versión cada 24 horas, y cuando tú lo pides. De forma predeterminada, solo te avisan de que hay una disponible.
3838
- **fonts.googleapis.com, solo para una fuente añadida en 1.x.** Una fuente personalizada para anotaciones añadida en una versión anterior se sigue descargando al iniciar, aunque la 2.x ya no la dibuja.
39-
- **PostHog, una vez al día.** Un recuento anónimo de instalaciones activas, enviado a los servidores de PostHog en la UE. Contiene la versión de la app, el sistema operativo, la arquitectura del procesador, cómo se instaló la app y si es el primer recuento de la semana y del mes. Nada más: ningún identificador de instalación (cada recuento recibe un ID aleatorio nuevo), ninguna dirección IP conservada, nada sobre tus grabaciones. Desmarca **Enviar recuento de uso anónimo** en el menú de la bandeja para desactivarlo, o define `DO_NOT_TRACK=1`.
39+
- **PostHog, una vez al día.** Un recuento anónimo de instalaciones activas, enviado a los servidores de PostHog en la UE. Contiene la versión de la app, el sistema operativo, la arquitectura del procesador, cómo se instaló la app, si se usó directamente o a través de su servidor MCP por un agente de IA, y si es el primer recuento de la semana y del mes. Nada más: ningún identificador de instalación (cada recuento recibe un ID aleatorio nuevo), ninguna dirección IP conservada, nada sobre tus grabaciones. Desmarca **Enviar recuento de uso anónimo** en el menú de la bandeja para desactivarlo, o define `DO_NOT_TRACK=1`.
4040
- **Tu proveedor de IA, solo si conectas uno.** La edición por chat envía tus mensajes y los datos del proyecto que lee, como la línea de tiempo y la transcripción. La traducción de subtítulos envía el texto de los subtítulos. Ambas permanecen desactivadas hasta que conectas un proveedor. Consulta [Edición con IA](./ai-editing.md).
4141

4242
## ¿OpenScreen recopila analíticas o informes de fallos? {#does-openscreen-collect-analytics-or-crash-reports}

‎website/i18n/es/docusaurus-plugin-content-pages/privacy.mdx‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ description: "Qué envían fuera de tu equipo la app de escritorio, el sitio web
55

66
# Política de privacidad
77

8-
Última actualización: 18 de octubre de 2026.
8+
Última actualización: 19 de octubre de 2026.
99

1010
OpenScreen es un grabador de pantalla y editor de vídeo gratuito y de código abierto. No tiene cuenta ni servidores propios: tus grabaciones, proyectos y transcripciones se guardan en tu equipo, y ninguna función sube una grabación. Esta página enumera todo lo que sí sale de tu equipo, desde la app de escritorio, este sitio web, el servidor MCP y el plugin para agentes, incluidos los datos del proyecto que las funciones de IA envían al proveedor que elijas.
1111

@@ -21,6 +21,7 @@ Como mucho una vez al día, la app informa a PostHog, en sus servidores de la UE
2121

2222
- la versión de la app, el sistema operativo y la arquitectura del procesador;
2323
- cómo se instaló la app (instalador, Microsoft Store, AppImage, Flatpak, etc.);
24+
- si la app se usó directamente o a través de su servidor MCP por un agente de IA;
2425
- si es el primer recuento de la instalación en la semana y en el mes.
2526

2627
No contiene nada más: ningún identificador de instalación (cada recuento recibe un ID aleatorio nuevo), ningún nombre, ningún nombre de archivo, nada sobre tus grabaciones. La geolocalización está desactivada, y el proyecto de PostHog está configurado para descartar las direcciones IP. La finalidad es saber cuántas personas usan OpenScreen y en qué sistemas, lo que constituye el interés legítimo del proyecto.

0 commit comments

Comments
 (0)