Skip to content
Open
Changes from 1 commit
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
1c4ea14
fix: enforce first-party popup cap for ads
AlejandroAkbal Mar 20, 2026
a83dc82
refactor: add popup guard debug instrumentation
AlejandroAkbal Mar 20, 2026
6734b52
refactor: expose popup guard match reasons
AlejandroAkbal Mar 20, 2026
6c9ec6e
fix: harden popup cap state handling
AlejandroAkbal Mar 20, 2026
8c1dfa8
fix: prefer latest popup timestamp across tabs
AlejandroAkbal Mar 20, 2026
2e02ceb
fix: spend popup cap on first vendor attempt
AlejandroAkbal Mar 20, 2026
91eba7f
fix: exempt in-page push opens from popup cap
AlejandroAkbal Mar 20, 2026
85d891e
fix: defer ad arming until after first click
AlejandroAkbal Mar 20, 2026
e37699d
fix: ignore capped vendor click handlers
AlejandroAkbal Mar 20, 2026
e2b542b
chore: enable popup guard debug in development
AlejandroAkbal Mar 20, 2026
b5a0607
refactor: simplify popup cap guard
AlejandroAkbal Mar 20, 2026
beb5218
Revert "fix: defer ad arming until after first click"
AlejandroAkbal Mar 20, 2026
6edda36
fix: bypass popup guard for trusted app opens
AlejandroAkbal Mar 20, 2026
90820bf
chore: restore dev popup guard logs
AlejandroAkbal Mar 20, 2026
0cace6b
fix: exempt push provider opens from popup cap
AlejandroAkbal Mar 20, 2026
fe6d0a0
fix: treat hotsoz push redirects as in-page opens
AlejandroAkbal Mar 20, 2026
06177ee
chore: log skipped ad scripts while capped
AlejandroAkbal Mar 20, 2026
19cca55
refactor: centralize ad provider metadata
AlejandroAkbal Mar 20, 2026
db4bfe8
refactor: restore ad provider notes
AlejandroAkbal Mar 20, 2026
94bf27c
test: cover popup guard decisions
AlejandroAkbal Mar 21, 2026
92c9fda
fix: correct popunder provider URL
AlejandroAkbal Mar 21, 2026
f42c131
test: assert required popup cap duration
AlejandroAkbal Mar 21, 2026
3e88a92
fix: abort blocked ad popup fallbacks
AlejandroAkbal Mar 21, 2026
6644249
Merge remote-tracking branch 'origin/main' into fix/ad-popup-cap-30m
AlejandroAkbal Mar 21, 2026
6b4ffbb
test: cover popup guard browser flow
AlejandroAkbal Mar 22, 2026
38697f0
test: tighten capped popup browser assertions
AlejandroAkbal Mar 22, 2026
031a706
refactor: simplify ad provider flattening
AlejandroAkbal Mar 23, 2026
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
147 changes: 147 additions & 0 deletions composables/useAdvertisements.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,155 @@
import { default as randomWeightedChoice } from 'random-weighted-choice'

const AD_POPUP_CAP_DURATION_MS = 30 * 60 * 1000
const AD_LAST_POPUP_AT_STORAGE_KEY = 'ads-last-popup-at'
const STACK_URL_REGEX = /https?:\/\/[^\s)]+/g

type WindowOpenArgs = Parameters<Window['open']>
type WindowOpenResult = ReturnType<Window['open']>

function getScriptUrlsFromStack(stack: string): URL[] {
const matches = stack.match(STACK_URL_REGEX)

if (!matches) {
return []
}

const urls: URL[] = []

for (const rawMatch of matches) {
const normalizedUrl = rawMatch
.replace(/[),]$/, '')
.replace(/:\d+:\d+$/, '')

try {
urls.push(new URL(normalizedUrl))
} catch {
// Ignore malformed URLs from stack traces
}
}

return urls
}
Comment thread
AlejandroAkbal marked this conversation as resolved.

function isLikelyVendorPopupCall(stack: string | undefined, hasUserActivation: boolean): boolean {
if (!stack) {
return !hasUserActivation
}

const callerScriptUrls = getScriptUrlsFromStack(stack)

if (callerScriptUrls.length === 0) {
return !hasUserActivation
}

const currentOrigin = window.location.origin

for (const scriptUrl of callerScriptUrls) {
if (scriptUrl.origin !== currentOrigin) {
return true
}

// Keep the heuristic broad: treat same-origin static /js scripts as likely ad/vendor callers.
if (scriptUrl.pathname.startsWith('/js/')) {
return true
}
}

return !hasUserActivation
}

export default function () {
const popunderScript = useState<string>('popunder-script', () => '')
const pushScript = useState<string>('push-notification-script', () => '')
const isPopupGuardInstalled = useState<boolean>('ads-popup-guard-installed', () => false)
const isPopupGuardArmed = useState<boolean>('ads-popup-guard-armed', () => false)
const lastAdPopupAtInMemory = useState<number | null>('ads-last-popup-at-in-memory', () => null)

if (!import.meta.client) {
return
}

function getLastAdPopupAt(): number | null {
if (lastAdPopupAtInMemory.value !== null) {
return lastAdPopupAtInMemory.value
}

try {
const rawLastPopupAt = window.localStorage.getItem(AD_LAST_POPUP_AT_STORAGE_KEY)

if (rawLastPopupAt) {
const parsedLastPopupAt = Number.parseInt(rawLastPopupAt, 10)

if (Number.isFinite(parsedLastPopupAt) && parsedLastPopupAt > 0) {
lastAdPopupAtInMemory.value = parsedLastPopupAt
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated
return parsedLastPopupAt
}
}
} catch {
// Ignore storage failures and use in-memory fallback
}

return null
}

function recordAdPopupOpened(at = Date.now()) {
lastAdPopupAtInMemory.value = at

try {
window.localStorage.setItem(AD_LAST_POPUP_AT_STORAGE_KEY, String(at))
} catch {
// Ignore storage failures and keep the in-memory fallback
}
}

function isAdPopupCapActive(): boolean {
const lastAdPopupAt = getLastAdPopupAt()

if (!lastAdPopupAt) {
return false
}

return Date.now() - lastAdPopupAt < AD_POPUP_CAP_DURATION_MS
}

if (!isPopupGuardInstalled.value) {
const originalWindowOpen = window.open.bind(window)

window.open = (...args: WindowOpenArgs): WindowOpenResult => {
if (!isPopupGuardArmed.value) {
return originalWindowOpen(...args)
}

const userActivation = (window.navigator as Navigator & {
userActivation?: { isActive: boolean }
}).userActivation

const hasUserActivation = userActivation?.isActive ?? true
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated
const stack = new Error().stack

if (!isLikelyVendorPopupCall(stack, hasUserActivation)) {
return originalWindowOpen(...args)
}

if (isAdPopupCapActive()) {
return null
}

recordAdPopupOpened()

return originalWindowOpen(...args)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check git history for recordAdPopupOpened ordering changes
git log --oneline -10 -- composables/useAdvertisements.ts
git show 6c9ec6e --stat 2>/dev/null || echo "Commit 6c9ec6e not found in history"

Repository: Rule-34/App

Length of output: 433


🏁 Script executed:

# Show the actual current content at lines 145-151
sed -n '145,151p' composables/useAdvertisements.ts

# Show the diff from commit 6c9ec6e to see what it changed
git show 6c9ec6e -- composables/useAdvertisements.ts | head -200

Repository: Rule-34/App

Length of output: 5555


🏁 Script executed:

# Get more context around the actual problematic code pattern
rg -A 10 -B 5 "recordAdPopupOpened" composables/useAdvertisements.ts

# Also check the exact content to understand if code was reverted
git log --oneline --all -- composables/useAdvertisements.ts | head -20

Repository: Rule-34/App

Length of output: 1493


🏁 Script executed:

# Show what the most recent commit changed
git show 90820bf -- composables/useAdvertisements.ts

# Get line numbers for the problematic pattern
rg -n "recordAdPopupOpened()" composables/useAdvertisements.ts

Repository: Rule-34/App

Length of output: 10223


🏁 Script executed:

# Get exact line numbers for the problematic pattern
rg -n "if \(isAdPopupCapActive" composables/useAdvertisements.ts -A 10

Repository: Rule-34/App

Length of output: 646


Record popup cap only after confirming popup opens successfully.

At line 166, recordAdPopupOpened() is called before originalWindowOpen(...args) executes. If the browser blocks the popup (returning null), the 30-minute cap is still triggered. This was previously fixed in commit 6c9ec6e but was reintroduced when commit 90820bf replaced the file.

Proposed fix: record only after successful open
      if (isAdPopupCapActive()) {
        logAdPopupGuard('block-capped-popunder')

        return null
      }

-    recordAdPopupOpened()
+    const popupHandle = originalWindowOpen(...args)

-    logAdPopupGuard('allow-popunder', {
-      cappedUntil: Date.now() + AD_POPUP_CAP_DURATION_MS
-    })
+    if (popupHandle) {
+      recordAdPopupOpened()
+
+      logAdPopupGuard('allow-popunder', {
+        cappedUntil: Date.now() + AD_POPUP_CAP_DURATION_MS
+      })
+    }

-    return originalWindowOpen(...args)
+    return popupHandle
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@composables/useAdvertisements.ts` around lines 145 - 151, The popup cap is
being recorded before verifying the popup actually opened; change the flow in
the wrapper around originalWindowOpen so you first call
originalWindowOpen(...args), capture its return (e.g., const popup =
originalWindowOpen(...args)), then if popup is non-null/truthy call
recordAdPopupOpened() and finally return the captured popup; retain the early
exit when isAdPopupCapActive() is true and only record after a successful open.

}

isPopupGuardInstalled.value = true
}

// Phase 1: stop injecting ad scripts while the 30-minute popup cap is active.
if (isAdPopupCapActive()) {
return
}

// Phase 2 (arming): once scripts load, guard vendor-like popups with the first-party cap.
isPopupGuardArmed.value = true
Comment thread
AlejandroAkbal marked this conversation as resolved.

const popunderAds = [
/**
Expand Down