fix: use stable site hash for install telemetry deduplication (#298)
* fix: use stable site hash for install telemetry deduplication (#297) generateSiteHash() used Date.now() as the hash seed, producing a different hash on every call. Since the installs table uses PRIMARY KEY (plugin_id, site_hash), the same site could insert unlimited rows, inflating install counts and making "Most Popular" sorting meaningless. Fix: use the site's request origin as a stable hash seed. The same origin always produces the same hash, so the marketplace deduplicates correctly. Also denormalizes install_count on the plugins table to avoid a COUNT(*) subquery per row in searchPlugins(). The count is recalculated atomically on each upsertInstall() call. Fixes #297 * chore: add changeset for install telemetry fix * fix: address review feedback on install telemetry - Replace crypto.subtle fallback with FNV-1a hash to avoid origin leakage and collisions from truncated seed strings - Remove duplicate p.install_count from SELECT (p.* already includes it) - Use explicit p.install_count in ORDER BY clause - Use db.batch() for atomic upsert + count recomputation instead of separate statements with misleading meta.changes check
This commit is contained in:
@@ -21,6 +21,7 @@ import {
|
||||
} from "../../../src/plugins/marketplace.js";
|
||||
|
||||
const HEX_64_PATTERN = /^[a-f0-9]{64}$/;
|
||||
const HEX_16_PATTERN = /^[a-f0-9]{16}$/;
|
||||
|
||||
// ── Helpers ───────────<E29480><E29480><EFBFBD>────────────────────────────────────────────
|
||||
|
||||
@@ -432,6 +433,40 @@ describe("MarketplaceClient", () => {
|
||||
// Should not throw
|
||||
await client.reportInstall("test-seo", "1.0.0");
|
||||
});
|
||||
|
||||
it("sends a stable site hash across multiple calls", async () => {
|
||||
const clientWithOrigin = createMarketplaceClient(BASE_URL, "https://myblog.example.com");
|
||||
|
||||
fetchSpy.mockResolvedValue(new Response("OK", { status: 200 }));
|
||||
|
||||
await clientWithOrigin.reportInstall("test-seo", "1.0.0");
|
||||
await clientWithOrigin.reportInstall("test-seo", "1.0.0");
|
||||
|
||||
const calls = fetchSpy.mock.calls;
|
||||
expect(calls.length).toBe(2);
|
||||
|
||||
const body1 = JSON.parse(calls[0]![1]!.body as string);
|
||||
const body2 = JSON.parse(calls[1]![1]!.body as string);
|
||||
|
||||
// Same origin produces the same hash every time
|
||||
expect(body1.siteHash).toBe(body2.siteHash);
|
||||
expect(body1.siteHash).toMatch(HEX_16_PATTERN);
|
||||
});
|
||||
|
||||
it("produces different hashes for different site origins", async () => {
|
||||
const client1 = createMarketplaceClient(BASE_URL, "https://site-a.example.com");
|
||||
const client2 = createMarketplaceClient(BASE_URL, "https://site-b.example.com");
|
||||
|
||||
fetchSpy.mockResolvedValue(new Response("OK", { status: 200 }));
|
||||
|
||||
await client1.reportInstall("test-seo", "1.0.0");
|
||||
await client2.reportInstall("test-seo", "1.0.0");
|
||||
|
||||
const body1 = JSON.parse(fetchSpy.mock.calls[0]![1]!.body as string);
|
||||
const body2 = JSON.parse(fetchSpy.mock.calls[1]![1]!.body as string);
|
||||
|
||||
expect(body1.siteHash).not.toBe(body2.siteHash);
|
||||
});
|
||||
});
|
||||
|
||||
describe("trailing slash handling", () => {
|
||||
|
||||
Reference in New Issue
Block a user