feat(plugins): classify install beacons by kind and client id

This commit is contained in:
t8y2 committed 2026-09-19 10:56:01 +08:00
1 parent 280af02120
commit 3100a918cc
5 files changed
+150 -14

No files matched your search

@@ -204,7 +204,7 @@ async function installListing(listing: MarketplacePluginListing): Promise<Plugin
version: listing.plugin.latestVersion,
});
notifyPluginRuntimeReplaced(result.plugin.manifest.id);
beaconPluginInstall(listing.plugin.id, listing.plugin.latestVersion);
beaconPluginInstall(listing.plugin.id, listing.plugin.latestVersion, listing.status === "update" ? "update" : "install");
return result;
}
@@ -0,0 +1,71 @@
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { beaconPluginInstall } from "@/lib/plugins/pluginMarketplace";
function lastBeaconBody(): Record<string, unknown> {
const calls = vi.mocked(globalThis.fetch).mock.calls;
const body = calls[calls.length - 1]?.[1]?.body;
return typeof body === "string" ? JSON.parse(body) : {};
}
function storedInstallationId(): string {
return localStorage.getItem("dbx-installation-id") ?? "";
}
describe("beaconPluginInstall", () => {
const backing = new Map<string, string>();
beforeEach(() => {
backing.clear();
vi.stubGlobal("localStorage", {
getItem: (key: string) => backing.get(key) ?? null,
setItem: (key: string, value: string) => void backing.set(key, value),
removeItem: (key: string) => void backing.delete(key),
});
vi.stubGlobal("fetch", vi.fn().mockResolvedValue(undefined));
});
afterEach(() => {
vi.unstubAllGlobals();
});
it("sends id, version and install kind by default", () => {
beaconPluginInstall("io.dbx.ssh", "1.2.3");
expect(lastBeaconBody()).toMatchObject({ id: "io.dbx.ssh", version: "1.2.3", kind: "install" });
});
it("forwards the update kind so updates cannot inflate install counts", () => {
beaconPluginInstall("io.dbx.ssh", "1.2.4", "update");
expect(lastBeaconBody()).toMatchObject({ kind: "update" });
});
it("generates a uuid installation id once and reuses it across beacons", () => {
beaconPluginInstall("io.dbx.ssh", "1.2.3");
const first = lastBeaconBody().clientId;
expect(first).toBe(storedInstallationId());
expect(String(first)).toMatch(/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/);
beaconPluginInstall("io.dbx.ldap", "2.0.0");
expect(lastBeaconBody().clientId).toBe(first);
});
it("regenerates a malformed stored installation id instead of sending it", () => {
backing.set("dbx-installation-id", "not-a-uuid");
beaconPluginInstall("io.dbx.ssh", "1.2.3");
const clientId = String(lastBeaconBody().clientId);
expect(clientId).toMatch(/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/);
expect(clientId).toBe(storedInstallationId());
});
it("sends an empty clientId when storage is unavailable", () => {
vi.stubGlobal("localStorage", {
getItem: () => {
throw new Error("unavailable");
},
setItem: () => {
throw new Error("unavailable");
},
removeItem: () => undefined,
});
beaconPluginInstall("io.dbx.ssh", "1.2.3");
expect(lastBeaconBody().clientId).toBe("");
});
});
@@ -65,14 +65,46 @@ export function listingRepositoryCanVerify(repository: PluginRepository): boolea
}
const INSTALL_BEACON_URL = "https://dbxio.com/api/plugins/install";
const INSTALLATION_ID_STORAGE_KEY = "dbx-installation-id";
const INSTALLATION_ID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/;
// Fire-and-forget install beacon for marketplace statistics; never blocks or fails the install.
export function beaconPluginInstall(pluginId: string, version: string): void {
// Anonymous, purely random per-installation id so server-side stats can count
// distinct machines without any user or hardware fingerprint. Clearing local
// storage (or reinstalling) regenerates it, which is acceptable for
// decorative statistics.
function installationClientId(): string {
try {
if (typeof localStorage === "undefined") return "";
let id = localStorage.getItem(INSTALLATION_ID_STORAGE_KEY);
if (!id || !INSTALLATION_ID_PATTERN.test(id)) {
id = typeof crypto.randomUUID === "function" ? crypto.randomUUID() : randomUuidFallback();
localStorage.setItem(INSTALLATION_ID_STORAGE_KEY, id);
}
return id;
} catch {
return "";
}
}
function randomUuidFallback(): string {
const hex = () =>
Math.floor(Math.random() * 0x10000)
.toString(16)
.padStart(4, "0");
return `${hex()}${hex()}-${hex()}-${hex()}-${hex()}-${hex()}${hex()}${hex()}`;
}
export type PluginInstallBeaconKind = "install" | "update";
// Fire-and-forget install beacon for marketplace statistics; never blocks or
// fails the install. `kind` separates fresh installs from version updates so
// update traffic cannot inflate the install numbers.
export function beaconPluginInstall(pluginId: string, version: string, kind: PluginInstallBeaconKind = "install"): void {
try {
void fetch(INSTALL_BEACON_URL, {
method: "POST",
headers: { "Content-Type": "text/plain" },
body: JSON.stringify({ id: pluginId, version }),
body: JSON.stringify({ id: pluginId, version, kind, clientId: installationClientId() }),
keepalive: true,
}).catch(() => undefined);
} catch {