Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
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
158 changes: 158 additions & 0 deletions assets/js/ads-popup-guard.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
export const AD_POPUP_CAP_DURATION_MS = 20 * 60 * 1000
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

Use a 30-minute cap, not 20 minutes.

The PR objective is a 30-minute cooldown. Keeping this at 20 minutes shortens the block window, lets ad scripts come back 10 minutes early, and makes the guard enforce the wrong contract.

🔧 Suggested fix
-export const AD_POPUP_CAP_DURATION_MS = 20 * 60 * 1000
+export const AD_POPUP_CAP_DURATION_MS = 30 * 60 * 1000
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
export const AD_POPUP_CAP_DURATION_MS = 20 * 60 * 1000
export const AD_POPUP_CAP_DURATION_MS = 30 * 60 * 1000
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@assets/js/ads-popup-guard.ts` at line 1, The constant
AD_POPUP_CAP_DURATION_MS is set to 20 minutes but should be a 30-minute
cooldown; update the value of AD_POPUP_CAP_DURATION_MS to 30 * 60 * 1000 (30
minutes in milliseconds) so the guard enforces the intended 30-minute cap.

export const IN_PAGE_PUSH_SEARCH_PARAM_PREFIXES = ['inpage.'] as const

export type PopupOpenKind = 'popunder' | 'in-page-push'
export type PopupClassification = {
kind: PopupOpenKind
hostnames?: string[]
searchParamPrefixes?: readonly string[]
}

export type AdPopupCapLogDetails = {
lastPopupAt: number | null
cappedUntil: number | null
remainingMs: number | null
}

export type PopupGuardDecision = {
event: 'allow-in-page-push' | 'allow-popunder' | 'block-capped-popunder'
shouldAllow: boolean
shouldRecordPopupAt: boolean
capLogDetails?: AdPopupCapLogDetails
cappedUntil?: number
}

export type TrustedPopupBypassDecision = {
shouldBypassCurrentOpen: boolean
nextShouldBypass: boolean
}

function hostnameMatches(hostname: string, allowedHostnames: string[]): boolean {
return allowedHostnames.some(allowedHostname => {
return hostname === allowedHostname || hostname.endsWith(`.${allowedHostname}`)
})
}

function hasMatchingSearchParamPrefix(parsedUrl: URL, searchParamPrefixes: readonly string[]): boolean {
for (const searchParamKey of parsedUrl.searchParams.keys()) {
if (searchParamPrefixes.some(prefix => searchParamKey.startsWith(prefix))) {
return true
}
}

return false
}

function matchesPopupClassification(parsedUrl: URL, popupClassification: PopupClassification): boolean {
if (popupClassification.hostnames && hostnameMatches(parsedUrl.hostname, popupClassification.hostnames)) {
return true
}

if (popupClassification.searchParamPrefixes && hasMatchingSearchParamPrefix(parsedUrl, popupClassification.searchParamPrefixes)) {
return true
}

return false
}

export function getTrustedPopupBypassDecision(shouldBypassNextWindowOpenGuard: boolean): TrustedPopupBypassDecision {
return {
shouldBypassCurrentOpen: shouldBypassNextWindowOpenGuard,
nextShouldBypass: false
}
}

export function getPopupOpenKind(
requestedUrl: string | null,
{
baseUrl,
popupClassifications
}: {
baseUrl: string
popupClassifications: readonly PopupClassification[]
}
): PopupOpenKind {
if (!requestedUrl) {
return 'popunder'
}

try {
const parsedUrl = new URL(requestedUrl, baseUrl)

for (const popupClassification of popupClassifications) {
if (matchesPopupClassification(parsedUrl, popupClassification)) {
return popupClassification.kind
}
}
} catch {
// Ignore malformed vendor URLs and keep the default popunder classification.
}

return 'popunder'
}

export function isAdPopupCapActive(
lastPopupAt: number | null,
now: number,
capDurationMs = AD_POPUP_CAP_DURATION_MS
): boolean {
return lastPopupAt !== null && now - lastPopupAt < capDurationMs
}

export function getAdPopupCapLogDetails(
lastPopupAt: number | null,
now: number,
capDurationMs = AD_POPUP_CAP_DURATION_MS
): AdPopupCapLogDetails {
if (lastPopupAt === null) {
return {
lastPopupAt: null,
cappedUntil: null,
remainingMs: null
}
}

const cappedUntil = lastPopupAt + capDurationMs

return {
lastPopupAt,
cappedUntil,
remainingMs: Math.max(0, cappedUntil - now)
}
}

export function getPopupGuardDecision({
popupOpenKind,
lastPopupAt,
now,
capDurationMs = AD_POPUP_CAP_DURATION_MS
}: {
popupOpenKind: PopupOpenKind
lastPopupAt: number | null
now: number
capDurationMs?: number
}): PopupGuardDecision {
if (popupOpenKind === 'in-page-push') {
return {
event: 'allow-in-page-push',
shouldAllow: true,
shouldRecordPopupAt: false
}
}

if (isAdPopupCapActive(lastPopupAt, now, capDurationMs)) {
return {
event: 'block-capped-popunder',
shouldAllow: false,
shouldRecordPopupAt: false,
capLogDetails: getAdPopupCapLogDetails(lastPopupAt, now, capDurationMs)
}
}

return {
event: 'allow-popunder',
shouldAllow: true,
shouldRecordPopupAt: true,
cappedUntil: now + capDurationMs
}
}
2 changes: 1 addition & 1 deletion components/layout/FeedbackButton.vue
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
import { project } from '@/config/project'

function handleClick(event: Event) {
window.open(`https://feedback.${project.urls.production.hostname}`, '_blank')
openTrustedWindow(`https://feedback.${project.urls.production.hostname}`, '_blank')
}
</script>

Expand Down
2 changes: 1 addition & 1 deletion components/pages/posts/post/PostSource.vue
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@
return
}

window.open(url, '_blank')
openTrustedWindow(url, '_blank')
}

function onMenuOpen() {
Expand Down
Loading