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
1 change: 1 addition & 0 deletions convex/crons.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,7 @@ describe("crons", () => {
{
mode: "current",
dryRun: true,
archiveDryRunSignals: true,
candidateLimit: 1_000,
batchSize: 50,
maxPages: 20,
Expand Down
1 change: 1 addition & 0 deletions convex/crons.ts
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,7 @@ if (process.env.CLAWHUB_DISABLE_CRONS !== "1") {
{
mode: "current",
dryRun: true,
archiveDryRunSignals: true,
candidateLimit: 1_000,
batchSize: 50,
maxPages: 20,
Expand Down
31 changes: 24 additions & 7 deletions convex/lib/publisherAbuseScoring.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -121,12 +121,12 @@ describe("publisher abuse scoring", () => {
expect(potentialBan).toBeGreaterThan(review);
});

it("escalates one P99 temporal hit as a potential ban candidate", () => {
it("keeps P99 temporal hits as review-only signals", () => {
expect(
labelForTemporalPublisherAbuse({ highTemporalSkillCount: 1, p99TemporalSkillCount: 1 }),
).toBe("potential_ban_candidate");
).toBe("review");
expect(
labelForTemporalPublisherAbuse({ highTemporalSkillCount: 1, p99TemporalSkillCount: 0 }),
labelForTemporalPublisherAbuse({ highTemporalSkillCount: 2, p99TemporalSkillCount: 2 }),
).toBe("review");
});

Expand Down Expand Up @@ -485,6 +485,20 @@ describe("publisher abuse scoring", () => {
expect(score.reasonCodes).toContain("temporal_installs_track_downloads");
});

it("flags recent install/download ratios at least twice the observed high end", () => {
const todayDay = 100;
const score = computeCurrentSkillTemporalAbuseScore({
todayDay,
dailyStats: dailyRange(94, 7, { downloads: 100, installs: 10 }),
});

expect(score.recent7Downloads).toBe(700);
expect(score.recent7Installs).toBe(70);
expect(score.installDownloadRatio7).toBeCloseTo(0.1);
expect(score.nearConversion).toBe(true);
expect(score.reasonCodes).toContain("temporal_installs_track_downloads");
});

it("keeps low-volume one-to-one install traffic below close-ratio thresholds", () => {
const todayDay = 100;
const score = computeCurrentSkillTemporalAbuseScore({
Expand All @@ -510,7 +524,7 @@ describe("publisher abuse scoring", () => {
expect(score.reasonCodes).not.toContain("temporal_installs_track_downloads");
});

it("requires installs to be close to downloads, not just statistically elevated", () => {
it("requires installs to clear the doubled observed high-end ratio", () => {
const todayDay = 100;
const score = computeCurrentSkillTemporalAbuseScore({
todayDay,
Expand All @@ -529,12 +543,15 @@ describe("publisher abuse scoring", () => {
const todayDay = 100;
const score = computeCurrentSkillTemporalAbuseScore({
todayDay,
dailyStats: dailyRange(71, 30, { downloads: 100, installs: 80 }),
dailyStats: [
...dailyRange(71, 23, { downloads: 100, installs: 13 }),
...dailyRange(94, 7, { downloads: 100, installs: 3 }),
],
});

expect(score.nearConversion).toBe(true);
expect(score.installDownloadRatio7).toBeCloseTo(0.8);
expect(score.installDownloadRatio30).toBeCloseTo(0.8);
expect(score.installDownloadRatio7).toBeCloseTo(0.03);
expect(score.installDownloadRatio30).toBeCloseTo(0.1067, 4);
expect(score.nearConversionWindowStartDay).toBe(71);
expect(score.nearConversionWindowEndDay).toBe(100);
});
Expand Down
18 changes: 8 additions & 10 deletions convex/lib/publisherAbuseScoring.ts
Original file line number Diff line number Diff line change
Expand Up @@ -127,12 +127,13 @@ const TEMPORAL_SUSTAINED_DAYS = 30;
const TEMPORAL_MAX_SPIKE_INSTALLS = 2;
const TEMPORAL_MAX_SUSTAINED_INSTALLS = 5;
const TEMPORAL_MIN_BASELINE_7_DOWNLOADS = 100;
const TEMPORAL_MIN_NEAR_CONVERSION_7_DOWNLOADS = 1_000;
const TEMPORAL_MIN_NEAR_CONVERSION_30_DOWNLOADS = 2_000;
const TEMPORAL_MIN_NEAR_CONVERSION_INSTALLS = 500;
const TEMPORAL_MIN_NEAR_CONVERSION_7_DOWNLOADS = 500;
const TEMPORAL_MIN_NEAR_CONVERSION_30_DOWNLOADS = 1_000;
const TEMPORAL_MIN_NEAR_CONVERSION_7_INSTALLS = 50;
const TEMPORAL_MIN_NEAR_CONVERSION_30_INSTALLS = 100;
const TEMPORAL_EXPECTED_INSTALL_DOWNLOAD_RATIO = 0.012;
const TEMPORAL_MIN_INSTALL_DOWNLOAD_RATIO = 0.5;
const TEMPORAL_MIN_INSTALL_DOWNLOAD_EXCESS_Z_SCORE = 50;
const TEMPORAL_MIN_INSTALL_DOWNLOAD_RATIO = 0.1;
const TEMPORAL_MIN_INSTALL_DOWNLOAD_EXCESS_Z_SCORE = 10;

export function labelForPublisherAbuseZScore(
zScore: number,
Expand Down Expand Up @@ -402,9 +403,6 @@ export function labelForTemporalPublisherAbuse(input: {
highTemporalSkillCount: number;
p99TemporalSkillCount?: number;
}): PublisherAbuseLabel {
if ((input.p99TemporalSkillCount ?? 0) >= 1 || input.highTemporalSkillCount >= 2) {
return "potential_ban_candidate";
}
if (input.highTemporalSkillCount >= 1) return "review";
return "pass";
}
Expand Down Expand Up @@ -547,12 +545,12 @@ function computeSkillTemporalAbuseScoreForWindows(input: {
});
const nearConversion7 =
recent7.downloads >= TEMPORAL_MIN_NEAR_CONVERSION_7_DOWNLOADS &&
recent7.installs >= TEMPORAL_MIN_NEAR_CONVERSION_INSTALLS &&
recent7.installs >= TEMPORAL_MIN_NEAR_CONVERSION_7_INSTALLS &&
installDownloadRatio7 >= TEMPORAL_MIN_INSTALL_DOWNLOAD_RATIO &&
installDownloadExcessZScore7 >= TEMPORAL_MIN_INSTALL_DOWNLOAD_EXCESS_Z_SCORE;
const nearConversion30 =
recent30.downloads >= TEMPORAL_MIN_NEAR_CONVERSION_30_DOWNLOADS &&
recent30.installs >= TEMPORAL_MIN_NEAR_CONVERSION_INSTALLS &&
recent30.installs >= TEMPORAL_MIN_NEAR_CONVERSION_30_INSTALLS &&
installDownloadRatio30 >= TEMPORAL_MIN_INSTALL_DOWNLOAD_RATIO &&
installDownloadExcessZScore30 >= TEMPORAL_MIN_INSTALL_DOWNLOAD_EXCESS_Z_SCORE;
const nearConversion = nearConversion7 || nearConversion30;
Expand Down
6 changes: 6 additions & 0 deletions convex/lib/retentionPolicy.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,12 @@ describe("retention policies", () => {
});
});

it("documents publisher abuse signals as durable review evidence", () => {
expect(getRetentionPolicy("publisherAbuseSignals")).toMatchObject({
classification: "permanent",
});
});

it("documents package stat events as processed-event retention", () => {
expect(getRetentionPolicy("packageStatEvents")).toMatchObject({
classification: "ephemeral",
Expand Down
2 changes: 2 additions & 0 deletions convex/lib/retentionPolicy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -203,6 +203,8 @@ export const RETENTION_POLICIES = {
publisherAbuseScores: permanent("Abuse score history used for review decisions."),
publisherAbuseReviewNominations: permanent("Abuse review workflow state."),
publisherAbuseReviewEvents: permanent("Abuse review event history."),
publisherAbuseSignals: permanent("Durable publisher abuse signal archive for staff review."),
publisherAbuseSignalReviewEvents: permanent("Abuse signal review event history."),
vtScanLogs: permanent("VirusTotal scan log history."),
apiTokens: permanent("User API tokens until revoked."),
cliDeviceCodes: ephemeral("CLI device codes expire quickly.", {
Expand Down
Loading
Loading