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
85 changes: 82 additions & 3 deletions electron/usage-ping.test.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import { mkdtempSync, rmSync } from "node:fs";
import { mkdtempSync, rmSync, statSync, writeFileSync } from "node:fs";
import os from "node:os";
import path from "node:path";
import { afterEach, describe, expect, it } from "vitest";
import {
installWeekFrom,
loadUsagePingState,
periodKeys,
planUsagePing,
Expand All @@ -25,7 +26,13 @@ function tmp(): string {
return dir;
}

const fresh: UsagePingState = { enabled: true, day: null, week: null, month: null };
const fresh: UsagePingState = {
enabled: true,
day: null,
week: null,
month: null,
installWeek: null,
};
const facts: UsagePingFacts = {
version: "2.0.0",
os: "win32",
Expand Down Expand Up @@ -95,7 +102,9 @@ describe("sendUsagePing", () => {
await sendUsagePing(dir, facts, {}, new Date("2026-10-15T09:00:00Z"), ok);
expect(bodies).toHaveLength(2);
expect(bodies[0].distinct_id).not.toBe(bodies[1].distinct_id);
expect(bodies[0].properties).toEqual({
const { install_week, ...rest } = bodies[0].properties as Record<string, unknown>;
expect(install_week).toMatch(/^\d{4}-W\d{2}$/);
expect(rest).toEqual({
$process_person_profile: false,
$geoip_disable: true,
version: "2.0.0",
Expand All @@ -106,6 +115,12 @@ describe("sendUsagePing", () => {
first_this_week: true,
first_this_month: true,
});
// Nothing ID-like rides along: no UUID, no hash, no long opaque token.
for (const body of bodies)
for (const value of Object.values(body.properties as Record<string, unknown>))
expect(String(value)).not.toMatch(
/[0-9a-f]{8}-[0-9a-f]{4}|[0-9a-f]{16,}|[A-Za-z0-9+/=]{24,}/i,
);
});

it("keeps the checkbox off when it is unticked while a ping is in flight", async () => {
Expand All @@ -117,3 +132,67 @@ describe("sendUsagePing", () => {
expect(loadUsagePingState(dir)).toMatchObject({ enabled: false, day: "2026-10-14" });
});
});

describe("install week", () => {
it("dates the install by the userData directory's birthtime, never its mtime", () => {
expect(installWeekFrom({ birthtimeMs: Date.parse("2025-05-07T12:00:00Z") })).toBe("2025-W19");
// Filesystems that record no birthtime report 0; mtime would move, so no fallback.
const mtimeMs = Date.parse("2025-05-07T12:00:00Z");
for (const birthtimeMs of [0, Number.NaN]) {
const stats = { birthtimeMs, mtimeMs };
expect(installWeekFrom(stats)).toBe("unknown");
}
// Positive but past the last representable Date.
expect(installWeekFrom({ birthtimeMs: 8.64e15 + 1 })).toBe("unknown");
expect(installWeekFrom({ birthtimeMs: Number.POSITIVE_INFINITY })).toBe("unknown");
// Representable, but a five-digit year the loader would reject.
expect(installWeekFrom({ birthtimeMs: Date.UTC(10000, 0, 5) })).toBe("unknown");
expect(installWeekFrom(null)).toBe("unknown");
});

const sent = async (dir: string, now: Date) => {
let properties: Record<string, unknown> = {};
await sendUsagePing(dir, facts, {}, now, async (_url, init) => {
properties = JSON.parse(String(init.body)).properties;
return { ok: true };
});
return properties.install_week;
};

it("gives a fresh install the current week, then keeps it on later pings", async () => {
const dir = tmp();
// From the directory, not the clock, so a UTC Monday between the two cannot flake.
const thisWeek = installWeekFrom(statSync(dir));
expect(await sent(dir, new Date())).toBe(thisWeek);
expect(loadUsagePingState(dir).installWeek).toBe(thisWeek);
expect(await sent(dir, new Date(Date.now() + 40 * 86_400_000))).toBe(thisWeek);
});

it('keeps the week an existing install already has, "unknown" included', async () => {
for (const installWeek of ["2025-W19", "unknown"]) {
const dir = tmp();
const state = { ...fresh, ...periodKeys(new Date("2026-10-13T09:00:00Z")), installWeek };
writeFileSync(path.join(dir, "usage-ping.json"), JSON.stringify(state));
expect(await sent(dir, wednesday)).toBe(installWeek);
expect(loadUsagePingState(dir).installWeek).toBe(installWeek);
}
});

it("re-derives a stored week that is not a valid ISO week", async () => {
for (const installWeek of ["2026-W54", "2026-W00", "abc", "2026-W4"]) {
const dir = tmp();
writeFileSync(path.join(dir, "usage-ping.json"), JSON.stringify({ ...fresh, installWeek }));
expect(loadUsagePingState(dir).installWeek).toBeNull();
}
const dir = tmp();
writeFileSync(
path.join(dir, "usage-ping.json"),
JSON.stringify({ ...fresh, installWeek: "2026-W53" }),
);
expect(loadUsagePingState(dir).installWeek).toBe("2026-W53");
});

it('sends "unknown" when the directory cannot be dated', async () => {
expect(await sent(path.join(tmp(), "gone"), wednesday)).toBe("unknown");
});
});
58 changes: 52 additions & 6 deletions electron/usage-ping.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,14 @@
// PostHog builds no person from it. Weekly and monthly actives are counted the way Brave
// counts them, without an ID: the app remembers locally when it last pinged and says
// whether this is its first ping of the week and of the month, so WAU and MAU are plain
// counts of those flags. GeoIP is off on every event, and the PostHog project discards IP
// addresses. The website FAQ ("Does OpenScreen work offline?") lists this payload; keep it
// in step.
// counts of those flags. To measure retention without an ID either, each ping also carries
// the ISO week the app was installed: a cohort shared by every install of that week, never
// finer than a week, and not an identifier. GeoIP is off on every event, and the PostHog
// project discards IP addresses. The website FAQ ("Does OpenScreen work offline?") and the
// privacy policy list this payload; keep them in step.

import { randomUUID } from "node:crypto";
import { readFileSync, writeFileSync } from "node:fs";
import { readFileSync, statSync, writeFileSync } from "node:fs";
import path from "node:path";
import type { InstallChannel } from "./install-channel";

Expand All @@ -27,6 +29,8 @@ export interface UsagePingState {
day: string | null;
week: string | null;
month: string | null;
/** ISO week of the installation, or "unknown" when it could not be dated. */
installWeek: string | null;
}

export interface UsagePingFacts {
Expand All @@ -39,7 +43,13 @@ export interface UsagePingFacts {
surface: "app" | "mcp";
}

const DEFAULT_STATE: UsagePingState = { enabled: true, day: null, week: null, month: null };
const DEFAULT_STATE: UsagePingState = {
enabled: true,
day: null,
week: null,
month: null,
installWeek: null,
};

/** An environment flag counts as set unless it is empty, "0" or "false": `CI=false` is off. */
const isSet = (value: string | undefined) =>
Expand All @@ -65,6 +75,31 @@ export function periodKeys(now: Date): { day: string; week: string; month: strin
};
}

/** "2026-W44" (weeks 01 to 53) or "unknown"; anything else is re-derived. */
const INSTALL_WEEK = /^(\d{4}-W(0[1-9]|[1-4]\d|5[0-3])|unknown)$/;
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Resolved

/** The install week, dated by when the userData directory was created. Electron creates it
* on first launch, so a fresh install gets the current week, and an install older than
* this field, even one that never pinged, gets its real one. Where the filesystem records
* no birthtime (0), the week is "unknown": mtime moves whenever the folder changes, and the
* week is kept for good once sent. */
export function installWeekFrom(stats: { birthtimeMs: number } | null): string {
const date = new Date(stats?.birthtimeMs ?? 0);
// Beyond ±8.64e15 ms a Date is invalid, and toISOString would throw outside any try.
if (!(date.getTime() > 0)) return "unknown";
// A week the loader would reject (year 10000 and on) would be re-derived on every ping.
const { week } = periodKeys(date);
return INSTALL_WEEK.test(week) ? week : "unknown";
}

function statOrNull(dir: string) {
try {
return statSync(dir);
} catch {
return null;
}
}

/** The ping to send now, or null. Pure, so the whole matrix pins from any platform. */
export function planUsagePing(
state: UsagePingState,
Expand Down Expand Up @@ -95,6 +130,10 @@ export function loadUsagePingState(userData: string): UsagePingState {
day: text(raw.day),
week: text(raw.week),
month: text(raw.month),
installWeek:
typeof raw.installWeek === "string" && INSTALL_WEEK.test(raw.installWeek)
? raw.installWeek
: null,
};
} catch {
return { ...DEFAULT_STATE };
Expand Down Expand Up @@ -127,6 +166,8 @@ export async function sendUsagePing(
const state = loadUsagePingState(userData);
const plan = planUsagePing(state, facts.channel, env, now);
if (!plan) return;
// Kept once known, "unknown" included, so an install never changes cohort.
const installWeek = state.installWeek ?? installWeekFrom(statOrNull(userData));
try {
const response = await fetchImpl(CAPTURE_URL, {
method: "POST",
Expand All @@ -145,13 +186,18 @@ export async function sendUsagePing(
surface: facts.surface,
first_this_week: plan.firstThisWeek,
first_this_month: plan.firstThisMonth,
install_week: installWeek,
},
}),
signal: AbortSignal.timeout(15_000),
});
// Re-read rather than reuse `state`: the tray checkbox may have changed while this ran.
if (response.ok)
saveUsagePingState(userData, { ...loadUsagePingState(userData), ...plan.keys });
saveUsagePingState(userData, {
...loadUsagePingState(userData),
...plan.keys,
installWeek,
});
} catch {
// Offline, blocked or timed out: the next attempt tries again.
}
Expand Down
4 changes: 2 additions & 2 deletions website/docs/faq.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,15 +36,15 @@ OpenScreen is a free, MIT-licensed screen recorder and video editor for Windows,
- **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.
- **github.com and api.github.com.** Builds that update themselves check for a new release a minute after launch, then every six hours, and when you ask. They download it in the background and show an update button; nothing is installed until you click it.
- **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.
- **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`.
- **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, whether it is the first count of the week and of the month, and the week the app was installed. 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`.
- **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).

## Does OpenScreen collect analytics or crash reports?

**One anonymous daily count, nothing else.** The app contains no analytics or crash-reporting SDK, and nothing records what you do in it.

- Once a day, the app reports that one installation was active. [Does OpenScreen work offline?](#does-openscreen-work-offline) lists exactly what that count holds and how to turn it off.
- The count carries no installation identifier: each one gets a new random ID, and nothing else in it is specific to your computer.
- The count carries no installation identifier: each one gets a new random ID, and nothing else in it is specific to your computer. The install week it carries, such as `2026-W44`, is the week of installation, the same for everyone who installed that week, not an identifier.
- AI provider keys are stored encrypted with Electron's `safeStorage`. If encryption is unavailable, the key is not saved.

## Is OpenScreen safe to install?
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,15 +36,15 @@ OpenScreen ist ein kostenloser Bildschirmrekorder und Videoeditor unter MIT-Lize
- **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.
- **github.com und api.github.com.** Builds, die sich selbst aktualisieren, suchen eine Minute nach dem Start, dann alle sechs Stunden und auf deine Anfrage nach einer neuen Version. Sie laden sie im Hintergrund herunter und zeigen eine Update-Schaltfläche; installiert wird erst, wenn du darauf klickst.
- **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.
- **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`.
- **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, ob es die erste Zählung der Woche und des Monats ist und in welcher Woche die App installiert wurde. 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`.
- **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).

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

**Eine anonyme tägliche Zählung, sonst nichts.** Die App enthält kein SDK für Analysen oder Absturzberichte, und nichts zeichnet auf, was du in ihr tust.

- Einmal täglich meldet die App, dass eine Installation aktiv war. [Funktioniert OpenScreen offline?](#does-openscreen-work-offline) listet genau auf, was diese Zählung enthält und wie du sie abschaltest.
- Die Zählung trägt keine Installationskennung: Jede erhält eine neue Zufalls-ID, und nichts anderes darin ist spezifisch für deinen Computer.
- Die Zählung trägt keine Installationskennung: Jede erhält eine neue Zufalls-ID, und nichts anderes darin ist spezifisch für deinen Computer. Die Installationswoche darin, etwa `2026-W44`, ist die Woche der Installation, für alle gleich, die in dieser Woche installiert haben, und keine Kennung.
- API-Schlüssel für KI-Anbieter werden verschlüsselt mit Electrons `safeStorage` gespeichert. Ist keine Verschlüsselung verfügbar, wird der Schlüssel nicht gespeichert.

## Ist die Installation von OpenScreen sicher? {#is-openscreen-safe-to-install}
Expand Down
7 changes: 4 additions & 3 deletions website/i18n/de/docusaurus-plugin-content-pages/privacy.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ description: "Was die Desktop-App, die Website, der MCP-Server und das Agent-Plu

# Datenschutzerklärung

Zuletzt aktualisiert: 19. Oktober 2026.
Zuletzt aktualisiert: 4. November 2026.

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.

Expand All @@ -22,9 +22,10 @@ Höchstens einmal täglich meldet die App an PostHog, auf dessen EU-Servern, das
- die App-Version, das Betriebssystem und die Prozessorarchitektur;
- wie die App installiert wurde (Installer, Microsoft Store, AppImage, Flatpak und so weiter);
- ob die App direkt oder über ihren MCP-Server von einem KI-Agenten genutzt wurde;
- ob es die erste Zählung der Installation in der Woche und im Monat ist.
- ob es die erste Zählung der Installation in der Woche und im Monat ist;
- die Woche, in der die App installiert wurde, etwa `2026-W44`, oder `unknown`, wenn sie sich nicht bestimmen lässt. Jede Installation aus dieser Woche sendet denselben Wert: Es ist die Woche der Installation, keine Kennung.

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.
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, auf welchen Systemen und wie viele es Wochen nach der Installation noch nutzen; das ist das berechtigte Interesse des Projekts.

Um sie abzuschalten, entferne im Menü des Symbols im Infobereich das Häkchen bei **Anonyme Nutzungszählung senden**, oder setze die Umgebungsvariable `DO_NOT_TRACK=1`. Entwicklungs-Builds und CI-Läufe senden sie nie.

Expand Down
Loading
Loading