Throttle rapid ad clicks across campaigns and publishers - #256
Merged
Merged
Conversation
ThreatCrush Security Scan45 finding(s) HIGH/CRITICAL: 2 | MEDIUM: 28 | LOW: 15
Snippets are redacted; ThreatCrush never prints matched credential material. |
| it("admits exactly one of 32 concurrent cross-campaign clicks across connections", async () => { | ||
| const ip = key("ip"), visitor = key("visitor"); | ||
| const accepted = await Promise.all(Array.from({ length: 32 }, (_, i) => | ||
| clients[i % clients.length].eval(CLAIM_CLICK_LUA, 2, ip, visitor, CLICK_COOLDOWN_MS))); |
| expect(accepted.filter((n) => n === 1)).toHaveLength(1); | ||
| expect(await clients[0].pttl(ip)).toBeGreaterThan(4_000); | ||
| expect(await clients[0].pttl(ip)).toBeLessThanOrEqual(5_000); | ||
| expect(await clients[1].eval(CLAIM_CLICK_LUA, 2, ip, key("rotated-visitor"), 5_000)).toBe(0); |
| expect(await clients[0].pttl(ip)).toBeGreaterThan(4_000); | ||
| expect(await clients[0].pttl(ip)).toBeLessThanOrEqual(5_000); | ||
| expect(await clients[1].eval(CLAIM_CLICK_LUA, 2, ip, key("rotated-visitor"), 5_000)).toBe(0); | ||
| expect(await clients[1].eval(CLAIM_CLICK_LUA, 2, key("rotated-ip"), visitor, 5_000)).toBe(0); |
| expect(await clients[0].pttl(ip)).toBeLessThanOrEqual(5_000); | ||
| expect(await clients[1].eval(CLAIM_CLICK_LUA, 2, ip, key("rotated-visitor"), 5_000)).toBe(0); | ||
| expect(await clients[1].eval(CLAIM_CLICK_LUA, 2, key("rotated-ip"), visitor, 5_000)).toBe(0); | ||
| expect(await clients[1].eval(CLAIM_CLICK_LUA, 2, key("different-ip"), key("different-visitor"), 5_000)).toBe(1); |
| it("does not claim the other identity or extend expiry on rejection", async () => { | ||
| const ip = key("busy-ip"), visitor = key("new-visitor"); | ||
| await clients[0].set(ip, "1", "PX", 100); | ||
| expect(await clients[1].eval(CLAIM_CLICK_LUA, 2, ip, visitor, 5_000)).toBe(0); |
| expect(await clients[0].exists(visitor)).toBe(0); | ||
| expect(await clients[0].pttl(ip)).toBeLessThanOrEqual(100); | ||
| await new Promise((r) => setTimeout(r, 120)); | ||
| expect(await clients[1].eval(CLAIM_CLICK_LUA, 2, ip, visitor, 5_000)).toBe(1); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Rapid clicks on different campaigns could bypass campaign-only deduplication and inflate paper spend. A shared Redis admission now permits one accepted click per visitor or IP every five seconds across campaigns and publishers, including concurrent requests. Rejected clicks retain their redirect while charging neither cash nor paper spend. Free delivery also participates in the existing six-hour campaign deduplication, and failed validation withholds charges.
Railway click accounting uses the edge-provided client IP. Deployment requires the existing REDIS_URL and IP_HASH_SALT; both are configured and the production Redis connection passed a read-only health check. There is no database migration. Shared-IP visitors share the short cooldown; during Redis outages billing is withheld while redirects continue.
Validation: production build and TypeScript pass; full suite 2,327 passed, seven pre-existing skips. Real disposable Redis tests admit exactly one of 32 simultaneous requests across eight connections and cover expiry, changed identities and non-extending rejections. Accounting tests cover paid, promo and free tiers.