
Popup Cro
- 76 installs
- 451 repo stars
- Updated July 21, 2026
- borghei/claude-skills
Popup CRO is a Claude skill that optimizes web and mobile popups (exit-intent, slide-in, banners, modals) for conversion using format selection, trigger engineering, targeting, frequency capping and A/B testing.
About
Popup CRO is a conversion framework for on-site popups and modals covering format selection, trigger engineering, audience targeting, frequency capping, copy design and A/B testing. A developer or marketer uses it when adding or tuning lead-capture, promotional, announcement or feedback popups on web and mobile. It ships Python helpers for A/B test math, ROI estimation and strategy auditing, and it flags GDPR/CCPA and Google intrusive-interstitial constraints.
- Format-selection matrix mapping goal to popup type and expected conversion rate
- Trigger engineering with optimal time-delay, scroll-depth and exit-intent settings
- Audience segmentation, frequency capping and A/B test framework for popups
Popup Cro by the numbers
- 76 all-time installs (skills.sh)
- Ranked #498 of 853 Sales & Marketing skills by installs in the Skillselion catalog
- Data as of Aug 5, 2026 (Skillselion catalog sync)
popup-cro capabilities & compatibility
- Capabilities
- pricing strategy · referral program · landing page cro
- Use cases
- marketing · copywriting
- Pricing
- Free
What popup-cro says it does
Popup and modal optimization for conversion. Covers exit-intent, slide-ins, banners, timing optimization, frequency capping, audience targeting,
| Time delay | 15-30 seconds | < 10s feels aggressive, > 60s misses visitors |
Full-screen overlay | Major promotions, welcome mats | Very high | No (Google penalizes)
npx skills add https://github.com/borghei/claude-skills --skill popup-croAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 76 |
|---|---|
| repo stars | ★ 451 |
| Last updated | July 21, 2026 |
| Repository | borghei/claude-skills ↗ |
What it does
Design and A/B-test exit-intent, slide-in and banner popups for lead capture, promotions and announcements without hurting UX.
Who is it for?
Lead-capture, promo and announcement popups where you need trigger, targeting and A/B-test discipline.
Skip if: Full pricing-page CRO or in-app paywall design.
When should I use this skill?
Adding, auditing or A/B-testing a popup, modal, slide-in or exit-intent offer.
What you get
A targeted popup strategy with the right format, trigger settings, suppression rules and an A/B test plan.
- popup format and trigger strategy
- audience targeting and suppression rules
- A/B test plan
By the numbers
- 7-format selection matrix
- ships 3 Python scripts (A/B calculator, ROI estimator, strategy auditor)
Files
Popup CRO
Production-grade popup optimization framework covering format selection, trigger engineering, audience targeting, frequency capping, copy design, compliance requirements, and structured A/B testing. Handles lead capture, promotional, announcement, and feedback popups across web and mobile.
---
Table of Contents
- Initial Assessment
- Format Selection Matrix
- Trigger Engineering
- Audience Targeting
- Frequency and Suppression Rules
- Popup Copy Architecture
- Strategy by Business Type
- Mobile Popup Design
- Compliance Requirements
- Multi-Popup Conflict Resolution
- A/B Test Framework
- Metrics and Benchmarks
- Output Artifacts
- Related Skills
---
Initial Assessment
Required Context
| Question | Why It Matters |
|---|---|
| What is the popup purpose? (lead capture, promotion, announcement, feedback) | Determines format and trigger |
| What existing popups are running? | Conflict resolution needed |
| What traffic sources? (paid, organic, direct) | Targeting and trigger strategy |
| New vs returning visitor split? | Personalization opportunity |
| Mobile vs desktop traffic split? | Mobile compliance requirements |
| Current popup performance? (if exists) | Baseline for optimization |
| Any compliance requirements? (GDPR, CCPA) | Legal constraints on data capture |
---
Format Selection Matrix
| Format | Best For | Intrusiveness | Mobile Friendly | Conversion Rate |
|---|---|---|---|---|
| Center modal | High-value offers, exit intent | High | With adaptation | 3-10% |
| Slide-in (corner) | Newsletter, content offers | Low | Yes | 1-5% |
| Top bar | Announcements, promotions | Very low | Yes | 0.5-2% |
| Bottom bar | Cookie consent, CTAs | Very low | Yes | 0.5-2% |
| Full-screen overlay | Major promotions, welcome mats | Very high | No (Google penalizes) | 2-8% |
| Inline expansion | Content upgrades within articles | Very low | Yes | 2-7% |
| Exit-intent modal | Final capture attempt | Medium | Desktop only | 2-5% |
Format Decision Tree
What is the goal?
├── Lead capture (email)
│ ├── Blog/content page → Slide-in (scroll trigger) or inline expansion
│ ├── Landing page → Exit-intent modal
│ └── Homepage → Time-delayed center modal
├── Promotion/discount
│ ├── E-commerce → Center modal (entry or timed)
│ └── SaaS → Top bar with countdown
├── Announcement
│ ├── New feature → Top bar (sticky)
│ └── Event/webinar → Slide-in or center modal
└── Feedback/survey
└── Post-interaction → Slide-in (bottom corner)---
Trigger Engineering
Trigger Types and Use Cases
| Trigger | How It Works | Best For | Risk |
|---|---|---|---|
| Exit intent | Mouse moves toward browser close/back | Last-chance capture | Desktop only |
| Time delay | Appears after N seconds | Low-commitment offers | Too early = annoying |
| Scroll depth | Appears at N% scroll | Content-engaged visitors | Must calibrate to content length |
| Page count | Appears after N page views | Multi-visit engagement | Requires cookie tracking |
| Click trigger | User clicks a specific element | Lead magnets, CTAs | Requires obvious trigger element |
| Inactivity | No interaction for N seconds | Re-engagement | Can feel intrusive |
Optimal Trigger Settings
| Trigger | Setting | Rationale |
|---|---|---|
| Time delay | 15-30 seconds | < 10s feels aggressive, > 60s misses visitors |
| Scroll depth | 50-70% | User has consumed enough content to be engaged |
| Page count | 2-3 pages | Visitor has shown interest beyond a single page |
| Exit intent | Mouse leaves viewport | Last opportunity before they leave |
| Click trigger | Prominent CTA button or text link | Explicit user intent |
Trigger Combinations
Layer triggers for better targeting:
| Combination | When to Use |
|---|---|
| Scroll 50% + Time 20s | Ensures both engagement and time on page |
| Page count 3 + Exit intent | Only show to visitors who have browsed multiple pages and are leaving |
| Click trigger + Email not captured | Only show form to non-subscribers |
---
Audience Targeting
Segmentation Rules
| Segment | Popup Strategy |
|---|---|
| New visitors (first visit) | Welcome offer, newsletter signup, content upgrade |
| Returning visitors (2-5 visits) | Deeper offer, free trial, demo request |
| Returning visitors (5+ visits) | Direct CTA, consultation offer |
| Email subscribers | Never show email capture popup |
| Existing customers | Feature announcements, upgrade offers only |
| Paid traffic visitors | Message-matched offer, no generic popup |
| Mobile visitors | Non-intrusive format only (slide-in or bottom bar) |
Exclusion Rules
Always exclude these segments from popups:
- Users who already converted (subscribed, signed up, purchased)
- Users who dismissed the same popup in this session
- Users who dismissed the same popup in the last 7 days
- Users in the checkout or payment flow
- Users on legal/compliance pages (privacy, terms)
---
Frequency and Suppression Rules
Frequency Caps
| Rule | Setting | Rationale |
|---|---|---|
| Max popups per session | 1 | Multiple popups per visit destroys trust |
| Cooldown after dismissal | 7 days minimum | Respect the user's "no" |
| Cooldown after close (X button) | 3-7 days | Less aggressive than dismissal |
| Max popups per month | 3-4 | More than this and users feel harassed |
| Post-conversion suppression | Permanent for that popup type | Never ask again once they converted |
Suppression Priority
If multiple popups compete for the same user in the same session, use this priority:
| Priority | Popup Type | Why |
|---|---|---|
| 1 | Cookie consent / legal | Required by law |
| 2 | Exit intent (if triggered) | Last chance, highest intent signal |
| 3 | Time-delayed / scroll-triggered | Planned engagement |
| 4 | Announcement bar | Lowest priority, always available |
---
Popup Copy Architecture
Anatomy of High-Converting Popup Copy
HEADLINE: What they get (benefit-focused, 6-10 words)
SUBHEADLINE: Why they should care (supporting detail, 1 sentence)
[FORM: Minimum fields]
[PRIMARY CTA: Action-oriented, 2-4 words]
[DECLINE TEXT: Respectful, neutral, 3-6 words]
[TRUST ELEMENT: Privacy or social proof, 1 line]Copy Examples by Type
Newsletter Signup:
- Headline: "Get Weekly Growth Tactics"
- Subheadline: "Join 15,000 marketers who get our Tuesday newsletter"
- CTA: "Subscribe"
- Decline: "No thanks"
- Trust: "Unsubscribe anytime. No spam."
Content Upgrade:
- Headline: "Get the Full SEO Checklist"
- Subheadline: "85 checks organized by priority. PDF download."
- CTA: "Send Me the Checklist"
- Decline: "I'll skip it"
- Trust: "Free. No credit card required."
Exit Intent (E-commerce):
- Headline: "Wait -- 15% Off Your First Order"
- Subheadline: "Use code WELCOME15 at checkout"
- CTA: "Claim My Discount"
- Decline: "I'll pay full price"
- Trust: "Valid for 24 hours"
Decline Text Rules
- Never use shame language ("No, I don't want to save money")
- Keep it neutral: "No thanks", "Maybe later", "Not now", "I'll skip it"
- Font size should be readable (not tiny text designed to be missed)
- Position clearly below or beside the CTA
---
Strategy by Business Type
E-commerce
| Popup | Trigger | Offer |
|---|---|---|
| Welcome discount | Entry (new visitors, 5-second delay) | 10-15% off first order |
| Exit intent | Mouse exit on product/cart page | Higher discount or free shipping |
| Cart abandonment | Return visit after cart abandonment | Reminder + incentive |
| Post-purchase | Thank you page | Referral offer or cross-sell |
B2B SaaS
| Popup | Trigger | Offer |
|---|---|---|
| Content upgrade | Scroll 50% on blog post | Related PDF, checklist, template |
| Demo request | Pricing page exit intent | "Talk to sales" with calendar link |
| Newsletter | Blog, 3rd page view | Weekly insights email |
| Feature announcement | Login, existing users | New feature with CTA to try it |
Content / Media
| Popup | Trigger | Offer |
|---|---|---|
| Newsletter | Scroll 60% on article | "Get articles like this weekly" |
| Content gate | After 3 free articles | Subscription or email for access |
| Social follow | Scroll bottom of article | Follow on social platforms |
---
Mobile Popup Design
Google Intrusive Interstitials Policy
Google penalizes mobile pages that show intrusive interstitials. Avoid:
- Full-screen popups that cover the main content
- Popups that the user must dismiss before accessing content
- Above-the-fold layouts where the popup pushes content below the fold
Mobile-Safe Formats
| Format | Mobile Safe? | Notes |
|---|---|---|
| Bottom bar | Yes | Small, non-blocking |
| Top bar | Yes | Small, dismissable |
| Slide-in (small) | Yes | Corner, < 30% of screen |
| Center modal (small) | Conditional | Only if easily dismissable and shown after engagement |
| Full-screen overlay | No | Penalized by Google |
| Exit intent | N/A | Not available on mobile |
Mobile Design Rules
- Touch targets minimum 44x44px for close button and CTA
- Close button (X) must be clearly visible and easily tappable
- Popup must not cover more than 50% of the screen
- Form inputs must trigger appropriate mobile keyboards
- Test on actual mobile devices (not just responsive preview)
---
Compliance Requirements
GDPR (EU)
- Checkbox for consent (pre-checked is not valid consent)
- Clear privacy policy link
- State what you will do with their email
- Easy unsubscribe in all subsequent emails
- Data processing purpose must be specified
CCPA (California)
- "Do Not Sell My Personal Information" link if applicable
- Privacy policy must be accessible
- Users can request data deletion
CAN-SPAM (US)
- Must honor unsubscribe requests within 10 business days
- Must include physical address in emails
- Subject lines cannot be deceptive
Cookie Consent (EU/UK)
- Cookie consent banner takes priority over all other popups
- Must not set non-essential cookies before consent
- Must offer granular consent options
---
Multi-Popup Conflict Resolution
Priority System
If your site runs multiple popups, implement these rules:
1. Legal popups first -- Cookie consent before any other popup 2. One popup per session -- After legal popup, only one marketing popup 3. Priority ranking -- Exit intent > Scroll-triggered > Time-delayed > Announcement bar 4. No stacking -- Never show two popups simultaneously 5. Queue system -- If multiple popups qualify, show highest priority and suppress the rest for this session
Implementation Rules
- Maintain a global popup state manager
- Each popup checks the state before rendering
- Record which popups the user has seen, dismissed, or converted on
- Share state across tabs if possible (localStorage)
---
A/B Test Framework
High-Impact Tests
| Test | Hypothesis | Metric |
|---|---|---|
| Trigger timing (15s vs 30s vs scroll) | Scroll trigger captures more engaged users | Conversion rate + bounce rate |
| Offer type (discount vs content) | Content offers attract higher-quality leads | Conversion rate + lead quality |
| Copy variation (benefit vs urgency) | Benefit-focused converts better long-term | Conversion rate |
| Format (modal vs slide-in) | Slide-in has lower bounce impact | Conversion rate + bounce rate |
| Decline text (neutral vs shame-free) | Neutral decline text maintains trust | Brand sentiment + repeat visits |
Test Rules
- Run each test for minimum 1,000 impressions per variant
- Track both popup conversion rate AND page-level metrics (bounce, time on page)
- A popup that converts 10% but increases bounce by 20% is a net negative
---
Metrics and Benchmarks
Key Metrics
| Metric | Formula | Benchmark |
|---|---|---|
| Popup conversion rate | Conversions / Impressions | 2-5% (good), 5-10% (excellent) |
| Popup view rate | Impressions / Eligible page views | Depends on trigger settings |
| Bounce rate impact | Bounce rate with popup - without | Should be < 5% increase |
| Email quality score | Popup leads who engage / Total popup leads | > 30% open subsequent emails |
| Revenue per popup lead | Revenue from popup leads / Total popup leads | Compare to other lead sources |
---
Output Artifacts
| Artifact | Format | Description |
|---|---|---|
| Popup Strategy Map | Type x trigger x audience x frequency table | Complete popup inventory with conflict rules |
| Complete Copy Set | Per-popup copy | Headline, subheadline, CTA, decline text, trust element |
| Mobile Adaptation Guide | Per-format recommendations | Mobile-safe formats, sizing, dismiss behavior |
| Compliance Checklist | Per-regulation requirements | GDPR, CCPA, CAN-SPAM, cookie consent |
| A/B Test Plan | Prioritized table | Hypotheses ranked by expected impact |
| Multi-Popup Priority Map | Priority ranking | Conflict resolution rules for concurrent popups |
---
Tool Reference
1. popup_strategy_auditor.py
Audits existing popup configurations for compliance, frequency conflicts, targeting gaps, and mobile safety. Reads a JSON inventory of popups and flags issues against best practices.
python scripts/popup_strategy_auditor.py popups.json --format text
python scripts/popup_strategy_auditor.py popups.json --format json| Flag | Type | Description |
|---|---|---|
popups.json | positional | Path to JSON file with popup inventory and rules |
--format | optional | Output format: text (default) or json |
2. popup_ab_test_calculator.py
Calculates statistical significance for popup A/B tests. Takes impressions and conversions for control and variant, computes conversion rates, relative lift, confidence level, and recommends whether to ship, continue testing, or abandon.
python scripts/popup_ab_test_calculator.py test.json --format text
python scripts/popup_ab_test_calculator.py test.json --format json| Flag | Type | Description |
|---|---|---|
test.json | positional | Path to JSON file with A/B test data |
--format | optional | Output format: text (default) or json |
3. popup_roi_estimator.py
Estimates revenue impact of popup lead capture by modeling lead volume, conversion rates, and customer lifetime value. Compares popup-sourced leads against other channels.
python scripts/popup_roi_estimator.py roi_data.json --format text
python scripts/popup_roi_estimator.py roi_data.json --format json| Flag | Type | Description |
|---|---|---|
roi_data.json | positional | Path to JSON file with popup performance and revenue data |
--format | optional | Output format: text (default) or json |
---
Troubleshooting
| Problem | Likely Cause | Resolution |
|---|---|---|
| Popup conversion rate below 2% | Wrong trigger timing, weak offer, or poor copy | Test scroll-triggered (50-70%) vs time-delayed; use benefit-focused headline; add countdown timer (lifts to 14.4% avg per 2026 benchmarks) |
| Bounce rate increases >5% after adding popup | Popup fires too early or covers too much screen on mobile | Increase time delay to 15-30s or switch to scroll trigger; use slide-in format instead of center modal |
| Email list quality drops (low open rates from popup leads) | Generic offer attracts low-intent subscribers | Switch to content upgrade offers specific to the page; add qualification question |
| Multiple popups fire in same session | No global popup state manager or priority system implemented | Implement session-level state tracking via localStorage; enforce one-popup-per-session rule with priority ranking |
| Mobile popup triggers Google penalty | Full-screen overlay or popup covers content before engagement | Switch to bottom bar, slide-in (<30% screen), or inline expansion format; test with Google Mobile-Friendly Tool |
| Exit-intent not working on mobile | Exit-intent relies on mouse movement which does not exist on mobile | Use scroll-up or inactivity trigger as mobile alternative; segment trigger rules by device type |
| Cookie consent popup conflicts with marketing popup | No priority system; both fire simultaneously | Legal popups always take priority; queue marketing popup to fire only after consent is given |
---
Success Criteria
- Popup conversion rate reaches 3-5% (good) or 5-10% (excellent) within 30 days of optimization
- Bounce rate impact stays below 5% increase compared to no-popup baseline
- Email lead quality maintains >30% open rate on subsequent emails from popup-sourced leads
- Mobile popup compliance passes Google Mobile-Friendly Test with zero warnings
- Frequency capping limits impressions to max 1 per session and 3-4 per month per user
- Zero GDPR/CCPA compliance violations in popup consent flows
- Gamified or countdown-enhanced popups reach 13%+ average conversion rate (2026 benchmark)
---
Scope & Limitations
In scope: Popup and modal format selection, trigger engineering, audience targeting, frequency capping, copy architecture, mobile-safe design, multi-popup conflict resolution, compliance requirements (GDPR, CCPA, CAN-SPAM, cookie consent), and structured A/B testing for lead capture, promotional, announcement, and feedback popups.
Out of scope: Form field-level optimization within popups (use form-cro), page-level conversion optimization around the popup (use page-cro), in-app onboarding modals and tooltips (use onboarding-cro), registration flows triggered by popups (use signup-flow-cro). Scripts operate on local data only -- no integrations with popup platforms (OptinMonster, Wisepops, etc.) or analytics tools.
Limitations: Conversion benchmarks are aggregate industry averages from 2025-2026 studies (Popupsmart 10K+ campaigns, Wisepops 1B+ displays) and vary significantly by industry, traffic source, and offer type. Exit-intent detection is desktop-only; mobile alternatives (scroll-up, inactivity) have different performance characteristics. Google intrusive interstitial penalties apply to mobile search traffic specifically -- direct/paid traffic is less affected.
---
Integration Points
- form-cro -- Popup form fields should follow form-cro field reduction and validation standards
- page-cro -- Page conversion should be optimized before layering popups; popup performance depends on page quality
- signup-flow-cro -- Popup-to-signup handoff should maintain context and pre-fill captured email
- paywall-upgrade-cro -- In-app upgrade modals share frequency capping and UX principles with marketing popups
- onboarding-cro -- Product onboarding tooltips and modals should use separate state management from marketing popups
- referral-program -- Post-purchase or post-conversion popups can trigger referral program prompts
---
Related Skills
- form-cro -- Use when the form inside the popup needs field-level optimization (field count, validation, layout).
- page-cro -- Use when the page surrounding the popup needs conversion optimization. Fix the page first, then add popups.
- onboarding-cro -- Use when popups/modals are part of in-app onboarding (tooltips, checklists, feature tours).
- signup-flow-cro -- Use when the popup leads into a registration flow that needs optimization.
#!/usr/bin/env python3
"""Popup A/B Test Calculator - Calculate statistical significance for popup experiments.
Takes impressions and conversions for control and variant, computes conversion rates,
relative lift, Z-score, p-value, and confidence level. Recommends ship, continue, or abandon.
Usage:
python popup_ab_test_calculator.py test.json
python popup_ab_test_calculator.py test.json --format json
"""
import argparse
import json
import math
import sys
from typing import Any
def safe_divide(num: float, den: float, default: float = 0.0) -> float:
"""Safely divide two numbers."""
return num / den if den != 0 else default
def normal_cdf(x: float) -> float:
"""Approximate the cumulative distribution function of the standard normal.
Uses the Abramowitz and Stegun approximation (error < 1.5e-7).
"""
sign = 1 if x >= 0 else -1
x = abs(x)
t = 1.0 / (1.0 + 0.2316419 * x)
d = 0.3989422804014327 # 1/sqrt(2*pi)
poly = t * (0.319381530 + t * (-0.356563782 + t * (1.781477937 + t * (-1.821255978 + t * 1.330274429))))
cdf = 1.0 - d * math.exp(-x * x / 2.0) * poly
return 0.5 + sign * (cdf - 0.5)
def calculate_z_score(p1: float, p2: float, n1: int, n2: int) -> float:
"""Calculate Z-score for two-proportion Z-test."""
p_pool = safe_divide(p1 * n1 + p2 * n2, n1 + n2)
se = math.sqrt(p_pool * (1 - p_pool) * (1.0 / n1 + 1.0 / n2)) if p_pool > 0 and p_pool < 1 else 0
if se == 0:
return 0.0
return (p2 - p1) / se
def analyze_test(data: dict) -> dict:
"""Analyze an A/B test for statistical significance."""
tests = data.get("tests", [data]) if "tests" not in data else data["tests"]
results = []
for test in tests:
control = test.get("control", {})
variant = test.get("variant", {})
c_impressions = control.get("impressions", 0)
c_conversions = control.get("conversions", 0)
v_impressions = variant.get("impressions", 0)
v_conversions = variant.get("conversions", 0)
c_rate = safe_divide(c_conversions, c_impressions) * 100
v_rate = safe_divide(v_conversions, v_impressions) * 100
absolute_lift = v_rate - c_rate
relative_lift = safe_divide(absolute_lift, c_rate) * 100
# Statistical significance
c_prop = safe_divide(c_conversions, c_impressions)
v_prop = safe_divide(v_conversions, v_impressions)
z_score = calculate_z_score(c_prop, v_prop, c_impressions, v_impressions)
p_value = 2 * (1 - normal_cdf(abs(z_score))) # Two-tailed
confidence = (1 - p_value) * 100
# Sample size check
min_sample = 1000 # Minimum per variant for popup tests
sufficient_sample = c_impressions >= min_sample and v_impressions >= min_sample
# Recommendation
if confidence >= 95 and absolute_lift > 0 and sufficient_sample:
recommendation = "Ship Variant"
reason = f"Variant wins with {confidence:.1f}% confidence and +{absolute_lift:.2f}pp lift"
elif confidence >= 95 and absolute_lift < 0 and sufficient_sample:
recommendation = "Keep Control"
reason = f"Control wins with {confidence:.1f}% confidence"
elif not sufficient_sample:
needed = max(min_sample - c_impressions, min_sample - v_impressions, 0)
recommendation = "Continue Testing"
reason = f"Need ~{needed:,} more impressions per variant (minimum {min_sample:,} each)"
elif confidence < 80:
recommendation = "Continue Testing"
reason = f"Only {confidence:.1f}% confidence -- need more data or larger effect size"
else:
recommendation = "Continue Testing"
reason = f"At {confidence:.1f}% confidence -- approaching significance but not yet actionable"
# Check bounce rate impact
bounce_warning = None
c_bounce = control.get("bounce_rate_pct")
v_bounce = variant.get("bounce_rate_pct")
if c_bounce is not None and v_bounce is not None:
bounce_diff = v_bounce - c_bounce
if bounce_diff > 5:
bounce_warning = f"Variant increases bounce rate by {bounce_diff:.1f}pp -- conversion win may be a net negative"
if recommendation == "Ship Variant":
recommendation = "Investigate Further"
reason += f" BUT bounce rate increased {bounce_diff:.1f}pp"
test_result = {
"test_name": test.get("name", "Unnamed Test"),
"control": {
"impressions": c_impressions,
"conversions": c_conversions,
"conversion_rate_pct": round(c_rate, 3),
},
"variant": {
"impressions": v_impressions,
"conversions": v_conversions,
"conversion_rate_pct": round(v_rate, 3),
},
"analysis": {
"absolute_lift_pp": round(absolute_lift, 3),
"relative_lift_pct": round(relative_lift, 2),
"z_score": round(z_score, 4),
"p_value": round(p_value, 6),
"confidence_pct": round(confidence, 2),
"sufficient_sample": sufficient_sample,
},
"recommendation": recommendation,
"reason": reason,
}
if bounce_warning:
test_result["bounce_warning"] = bounce_warning
results.append(test_result)
return {"test_results": results}
def format_text(result: dict) -> str:
"""Format test results as human-readable text."""
lines = []
lines.append("=" * 60)
lines.append("POPUP A/B TEST ANALYSIS")
lines.append("=" * 60)
for tr in result["test_results"]:
lines.append("")
lines.append(f"Test: {tr['test_name']}")
lines.append("-" * 40)
c = tr["control"]
v = tr["variant"]
a = tr["analysis"]
lines.append(f" Control: {c['impressions']:>8,} impressions | {c['conversions']:>6,} conversions | {c['conversion_rate_pct']:.3f}%")
lines.append(f" Variant: {v['impressions']:>8,} impressions | {v['conversions']:>6,} conversions | {v['conversion_rate_pct']:.3f}%")
lines.append("")
lines.append(f" Absolute Lift: {a['absolute_lift_pp']:+.3f}pp")
lines.append(f" Relative Lift: {a['relative_lift_pct']:+.2f}%")
lines.append(f" Confidence: {a['confidence_pct']:.2f}%")
lines.append(f" p-value: {a['p_value']:.6f}")
lines.append(f" Sufficient Sample: {'Yes' if a['sufficient_sample'] else 'No'}")
lines.append("")
lines.append(f" >> RECOMMENDATION: {tr['recommendation']}")
lines.append(f" {tr['reason']}")
if "bounce_warning" in tr:
lines.append(f" >> WARNING: {tr['bounce_warning']}")
lines.append("")
return "\n".join(lines)
def main() -> None:
"""Main entry point."""
parser = argparse.ArgumentParser(
description="Calculate statistical significance for popup A/B tests."
)
parser.add_argument(
"input_file",
help="Path to JSON file with A/B test data",
)
parser.add_argument(
"--format",
choices=["text", "json"],
default="text",
help="Output format (default: text)",
)
args = parser.parse_args()
try:
with open(args.input_file, "r") as f:
data = json.load(f)
except FileNotFoundError:
print(f"Error: File not found: {args.input_file}", file=sys.stderr)
sys.exit(1)
except json.JSONDecodeError as e:
print(f"Error: Invalid JSON: {e}", file=sys.stderr)
sys.exit(1)
result = analyze_test(data)
if args.format == "json":
print(json.dumps(result, indent=2))
else:
print(format_text(result))
if __name__ == "__main__":
main()
#!/usr/bin/env python3
"""Popup ROI Estimator - Estimate revenue impact of popup lead capture.
Models lead volume, conversion pipeline, and customer lifetime value to estimate
popup program ROI. Compares popup-sourced leads against other acquisition channels.
Usage:
python popup_roi_estimator.py roi_data.json
python popup_roi_estimator.py roi_data.json --format json
"""
import argparse
import json
import sys
from typing import Any
def safe_divide(num: float, den: float, default: float = 0.0) -> float:
"""Safely divide two numbers."""
return num / den if den != 0 else default
def estimate_popup_roi(data: dict) -> dict:
"""Estimate ROI from popup lead capture program."""
popup = data.get("popup_performance", {})
business = data.get("business_metrics", {})
channels = data.get("other_channels", [])
# Core popup metrics
monthly_visitors = popup.get("monthly_visitors", 0)
popup_view_rate = popup.get("popup_view_rate_pct", 50) / 100
popup_conversion_rate = popup.get("popup_conversion_rate_pct", 3) / 100
lead_to_customer_rate = popup.get("lead_to_customer_rate_pct", 5) / 100
# Business metrics
avg_ltv = business.get("avg_customer_ltv", 0)
avg_order_value = business.get("avg_order_value", 0)
monthly_popup_cost = business.get("monthly_popup_tool_cost", 0)
# Calculate pipeline
monthly_impressions = int(monthly_visitors * popup_view_rate)
monthly_leads = int(monthly_impressions * popup_conversion_rate)
monthly_customers = int(monthly_leads * lead_to_customer_rate)
# Revenue calculation
if avg_ltv > 0:
monthly_revenue = monthly_customers * avg_ltv
revenue_metric = "LTV-based"
elif avg_order_value > 0:
monthly_revenue = monthly_customers * avg_order_value
revenue_metric = "AOV-based"
else:
monthly_revenue = 0
revenue_metric = "No revenue metric provided"
annual_revenue = monthly_revenue * 12
annual_cost = monthly_popup_cost * 12
# Cost per lead and CAC
cost_per_lead = safe_divide(monthly_popup_cost, monthly_leads)
popup_cac = safe_divide(monthly_popup_cost, monthly_customers)
# ROI
monthly_profit = monthly_revenue - monthly_popup_cost
annual_profit = annual_revenue - annual_cost
roi_pct = safe_divide(annual_profit, annual_cost) * 100
popup_analysis = {
"pipeline": {
"monthly_visitors": monthly_visitors,
"monthly_impressions": monthly_impressions,
"monthly_leads": monthly_leads,
"monthly_customers": monthly_customers,
"popup_view_rate_pct": round(popup_view_rate * 100, 2),
"popup_conversion_rate_pct": round(popup_conversion_rate * 100, 2),
"lead_to_customer_rate_pct": round(lead_to_customer_rate * 100, 2),
},
"revenue": {
"revenue_metric": revenue_metric,
"monthly_revenue": round(monthly_revenue, 2),
"annual_revenue": round(annual_revenue, 2),
"monthly_cost": round(monthly_popup_cost, 2),
"annual_cost": round(annual_cost, 2),
"monthly_profit": round(monthly_profit, 2),
"annual_profit": round(annual_profit, 2),
"roi_pct": round(roi_pct, 1),
},
"unit_economics": {
"cost_per_lead": round(cost_per_lead, 2),
"popup_cac": round(popup_cac, 2),
"ltv_to_cac_ratio": round(safe_divide(avg_ltv, popup_cac), 2) if popup_cac > 0 else "N/A",
},
}
# Channel comparison
channel_comparison = []
for ch in channels:
ch_leads = ch.get("monthly_leads", 0)
ch_cost = ch.get("monthly_cost", 0)
ch_customers = ch.get("monthly_customers", 0)
ch_cpl = safe_divide(ch_cost, ch_leads)
ch_cac = safe_divide(ch_cost, ch_customers)
ch_ltv_cac = safe_divide(avg_ltv, ch_cac) if ch_cac > 0 else 0
channel_comparison.append({
"channel": ch.get("name", "Unknown"),
"monthly_leads": ch_leads,
"monthly_customers": ch_customers,
"cost_per_lead": round(ch_cpl, 2),
"cac": round(ch_cac, 2),
"ltv_to_cac": round(ch_ltv_cac, 2),
})
# Add popup as a channel for comparison
channel_comparison.insert(0, {
"channel": "Popup Lead Capture",
"monthly_leads": monthly_leads,
"monthly_customers": monthly_customers,
"cost_per_lead": round(cost_per_lead, 2),
"cac": round(popup_cac, 2),
"ltv_to_cac": round(safe_divide(avg_ltv, popup_cac), 2) if popup_cac > 0 else 0,
})
# Improvement scenarios
scenarios = []
for improvement_name, new_rate in [
("Optimized copy (+50% conversion)", popup_conversion_rate * 1.5),
("Added countdown timer (+46% per benchmarks)", popup_conversion_rate * 1.46),
("Gamified popup (+3x per benchmarks)", popup_conversion_rate * 3),
]:
new_leads = int(monthly_impressions * new_rate)
new_customers = int(new_leads * lead_to_customer_rate)
new_revenue = new_customers * (avg_ltv if avg_ltv > 0 else avg_order_value)
scenarios.append({
"scenario": improvement_name,
"new_conversion_rate_pct": round(new_rate * 100, 2),
"monthly_leads": new_leads,
"monthly_customers": new_customers,
"monthly_revenue": round(new_revenue, 2),
"incremental_monthly_revenue": round(new_revenue - monthly_revenue, 2),
})
return {
"popup_analysis": popup_analysis,
"channel_comparison": channel_comparison,
"improvement_scenarios": scenarios,
}
def format_text(result: dict) -> str:
"""Format ROI estimate as human-readable text."""
lines = []
pa = result["popup_analysis"]
lines.append("=" * 60)
lines.append("POPUP ROI ESTIMATE")
lines.append("=" * 60)
lines.append("")
p = pa["pipeline"]
lines.append("-" * 40)
lines.append("LEAD PIPELINE")
lines.append("-" * 40)
lines.append(f" Monthly Visitors: {p['monthly_visitors']:>10,}")
lines.append(f" Popup Impressions: {p['monthly_impressions']:>10,} ({p['popup_view_rate_pct']}% view rate)")
lines.append(f" Leads Captured: {p['monthly_leads']:>10,} ({p['popup_conversion_rate_pct']}% conversion)")
lines.append(f" New Customers: {p['monthly_customers']:>10,} ({p['lead_to_customer_rate_pct']}% lead-to-customer)")
lines.append("")
r = pa["revenue"]
lines.append("-" * 40)
lines.append(f"REVENUE ({r['revenue_metric']})")
lines.append("-" * 40)
lines.append(f" Monthly Revenue: ${r['monthly_revenue']:>12,.2f}")
lines.append(f" Annual Revenue: ${r['annual_revenue']:>12,.2f}")
lines.append(f" Monthly Cost: ${r['monthly_cost']:>12,.2f}")
lines.append(f" Annual Profit: ${r['annual_profit']:>12,.2f}")
lines.append(f" ROI: {r['roi_pct']:>12.1f}%")
lines.append("")
u = pa["unit_economics"]
lines.append("-" * 40)
lines.append("UNIT ECONOMICS")
lines.append("-" * 40)
lines.append(f" Cost per Lead: ${u['cost_per_lead']:>12,.2f}")
lines.append(f" Popup CAC: ${u['popup_cac']:>12,.2f}")
lines.append(f" LTV:CAC Ratio: {u['ltv_to_cac_ratio']}:1" if isinstance(u['ltv_to_cac_ratio'], (int, float)) else f" LTV:CAC Ratio: {u['ltv_to_cac_ratio']}")
lines.append("")
if result["channel_comparison"]:
lines.append("-" * 40)
lines.append("CHANNEL COMPARISON")
lines.append("-" * 40)
for ch in result["channel_comparison"]:
lines.append(f" {ch['channel']:<25} Leads: {ch['monthly_leads']:>5,} CAC: ${ch['cac']:>8,.2f} LTV:CAC: {ch['ltv_to_cac']:.1f}x")
lines.append("")
if result["improvement_scenarios"]:
lines.append("-" * 40)
lines.append("IMPROVEMENT SCENARIOS")
lines.append("-" * 40)
for sc in result["improvement_scenarios"]:
lines.append(f"\n {sc['scenario']}")
lines.append(f" Conv Rate: {sc['new_conversion_rate_pct']}% | Leads: {sc['monthly_leads']:,} | Revenue: ${sc['monthly_revenue']:,.2f} | Uplift: ${sc['incremental_monthly_revenue']:+,.2f}/mo")
lines.append("")
return "\n".join(lines)
def main() -> None:
"""Main entry point."""
parser = argparse.ArgumentParser(
description="Estimate revenue impact of popup lead capture program."
)
parser.add_argument(
"input_file",
help="Path to JSON file with popup performance and revenue data",
)
parser.add_argument(
"--format",
choices=["text", "json"],
default="text",
help="Output format (default: text)",
)
args = parser.parse_args()
try:
with open(args.input_file, "r") as f:
data = json.load(f)
except FileNotFoundError:
print(f"Error: File not found: {args.input_file}", file=sys.stderr)
sys.exit(1)
except json.JSONDecodeError as e:
print(f"Error: Invalid JSON: {e}", file=sys.stderr)
sys.exit(1)
result = estimate_popup_roi(data)
if args.format == "json":
print(json.dumps(result, indent=2))
else:
print(format_text(result))
if __name__ == "__main__":
main()
#!/usr/bin/env python3
"""Popup Strategy Auditor - Audit popup configurations for compliance and best practices.
Checks popup inventory for frequency conflicts, targeting gaps, mobile safety,
compliance issues (GDPR, CAN-SPAM), and dark pattern risks.
Usage:
python popup_strategy_auditor.py popups.json
python popup_strategy_auditor.py popups.json --format json
"""
import argparse
import json
import sys
from typing import Any
MOBILE_SAFE_FORMATS = ["bottom_bar", "top_bar", "slide_in", "inline_expansion"]
MOBILE_UNSAFE_FORMATS = ["full_screen_overlay"]
VALID_FORMATS = [
"center_modal", "slide_in", "top_bar", "bottom_bar",
"full_screen_overlay", "inline_expansion", "exit_intent_modal",
]
VALID_TRIGGERS = [
"exit_intent", "time_delay", "scroll_depth", "page_count",
"click_trigger", "inactivity", "entry",
]
COMPLIANCE_REQUIREMENTS = {
"gdpr": ["consent_checkbox", "privacy_link", "data_purpose"],
"ccpa": ["do_not_sell_link", "privacy_policy"],
"can_spam": ["unsubscribe_mechanism", "physical_address"],
}
SHAME_PHRASES = [
"no, i don't want to",
"no thanks, i prefer",
"i don't need",
"i hate saving",
]
def audit_single_popup(popup: dict, index: int) -> list[dict]:
"""Audit a single popup configuration."""
issues = []
popup_id = popup.get("id", f"popup_{index}")
# Format validation
fmt = popup.get("format", "")
if fmt and fmt not in VALID_FORMATS:
issues.append({
"popup_id": popup_id,
"category": "configuration",
"severity": "warning",
"issue": f"Unknown format: {fmt}",
"fix": f"Use one of: {', '.join(VALID_FORMATS)}",
})
# Mobile safety
if fmt in MOBILE_UNSAFE_FORMATS and popup.get("show_on_mobile", True):
issues.append({
"popup_id": popup_id,
"category": "mobile",
"severity": "error",
"issue": f"Format '{fmt}' is not mobile-safe and will trigger Google penalty",
"fix": "Set show_on_mobile to false or switch to a mobile-safe format (slide_in, bottom_bar)",
})
# Trigger validation
trigger = popup.get("trigger", "")
if trigger == "exit_intent" and popup.get("show_on_mobile", True):
issues.append({
"popup_id": popup_id,
"category": "mobile",
"severity": "warning",
"issue": "Exit intent trigger does not work on mobile devices",
"fix": "Add mobile-specific trigger (scroll_up, inactivity) or disable popup on mobile",
})
# Time delay check
if trigger == "time_delay":
delay = popup.get("delay_seconds", 0)
if delay < 10:
issues.append({
"popup_id": popup_id,
"category": "timing",
"severity": "warning",
"issue": f"Time delay of {delay}s is too aggressive (< 10s)",
"fix": "Set delay to 15-30 seconds for optimal balance",
})
# Frequency caps
max_per_session = popup.get("max_per_session")
if max_per_session is not None and max_per_session > 1:
issues.append({
"popup_id": popup_id,
"category": "frequency",
"severity": "warning",
"issue": f"Allows {max_per_session} impressions per session (should be max 1)",
"fix": "Limit to 1 popup impression per session",
})
cooldown = popup.get("cooldown_days", 0)
if cooldown < 3:
issues.append({
"popup_id": popup_id,
"category": "frequency",
"severity": "warning",
"issue": f"Cooldown of {cooldown} days is too short after dismissal",
"fix": "Set cooldown to minimum 7 days after dismissal",
})
# Exclusion rules
if not popup.get("exclude_converted_users", False):
issues.append({
"popup_id": popup_id,
"category": "targeting",
"severity": "error",
"issue": "Not excluding users who already converted",
"fix": "Always suppress popup for users who have already subscribed/purchased/signed up",
})
if not popup.get("exclude_checkout_flow", False):
issues.append({
"popup_id": popup_id,
"category": "targeting",
"severity": "error",
"issue": "Popup may appear during checkout flow",
"fix": "Exclude checkout and payment pages from popup targeting",
})
# Decline text dark pattern check
decline_text = popup.get("decline_text", "").lower()
for phrase in SHAME_PHRASES:
if phrase in decline_text:
issues.append({
"popup_id": popup_id,
"category": "dark_pattern",
"severity": "error",
"issue": f"Shame language in decline text: '{popup.get('decline_text')}'",
"fix": "Use neutral text: 'No thanks', 'Maybe later', 'Not now'",
})
break
# Close button
if popup.get("close_button_visible") is False:
issues.append({
"popup_id": popup_id,
"category": "dark_pattern",
"severity": "error",
"issue": "Close button is hidden",
"fix": "Close button must be clearly visible with minimum 44x44px touch target on mobile",
})
# Compliance checks
if popup.get("captures_email", False):
compliance_regions = popup.get("target_regions", [])
for region, requirements in COMPLIANCE_REQUIREMENTS.items():
if not compliance_regions or region in compliance_regions:
compliance_features = popup.get("compliance", {})
for req in requirements:
if not compliance_features.get(req, False):
issues.append({
"popup_id": popup_id,
"category": "compliance",
"severity": "error",
"issue": f"Missing {region.upper()} requirement: {req.replace('_', ' ')}",
"fix": f"Add {req.replace('_', ' ')} to comply with {region.upper()} regulations",
})
return issues
def check_conflicts(popups: list[dict]) -> list[dict]:
"""Check for conflicts between multiple popups."""
issues = []
# Check for overlapping triggers on same pages
page_popups: dict[str, list] = {}
for popup in popups:
for page in popup.get("target_pages", ["all"]):
page_popups.setdefault(page, []).append(popup)
for page, page_popup_list in page_popups.items():
if len(page_popup_list) > 2:
popup_ids = [p.get("id", "unknown") for p in page_popup_list]
issues.append({
"popup_id": "system",
"category": "conflict",
"severity": "warning",
"issue": f"Page '{page}' has {len(page_popup_list)} popups configured: {', '.join(popup_ids)}",
"fix": "Implement priority queue -- max 1 marketing popup per session after legal popups",
})
# Check for missing cookie consent priority
has_cookie_consent = any(p.get("type") == "cookie_consent" for p in popups)
has_marketing = any(p.get("type") != "cookie_consent" for p in popups)
if has_marketing and not has_cookie_consent:
issues.append({
"popup_id": "system",
"category": "compliance",
"severity": "warning",
"issue": "Marketing popups configured but no cookie consent popup found",
"fix": "Add cookie consent popup with highest priority if targeting EU/UK users",
})
return issues
def audit_popups(data: dict) -> dict:
"""Run full popup audit."""
popups = data.get("popups", [])
all_issues = []
for i, popup in enumerate(popups):
all_issues.extend(audit_single_popup(popup, i))
all_issues.extend(check_conflicts(popups))
error_count = sum(1 for i in all_issues if i["severity"] == "error")
warning_count = sum(1 for i in all_issues if i["severity"] == "warning")
# Category summary
categories = {}
for issue in all_issues:
cat = issue["category"]
categories.setdefault(cat, {"errors": 0, "warnings": 0})
categories[cat]["errors" if issue["severity"] == "error" else "warnings"] += 1
if error_count == 0 and warning_count <= 2:
health = "Healthy"
elif error_count <= 3:
health = "Needs Attention"
else:
health = "Critical"
return {
"summary": {
"total_popups": len(popups),
"total_issues": len(all_issues),
"errors": error_count,
"warnings": warning_count,
"health_rating": health,
},
"category_summary": categories,
"issues": all_issues,
}
def format_text(result: dict) -> str:
"""Format audit results as human-readable text."""
lines = []
summary = result["summary"]
lines.append("=" * 60)
lines.append("POPUP STRATEGY AUDIT REPORT")
lines.append("=" * 60)
lines.append("")
lines.append(f"Health Rating: {summary['health_rating']}")
lines.append(f"Total Popups: {summary['total_popups']}")
lines.append(f"Issues: {summary['total_issues']} ({summary['errors']} errors, {summary['warnings']} warnings)")
lines.append("")
if result["category_summary"]:
lines.append("-" * 40)
lines.append("ISSUES BY CATEGORY")
lines.append("-" * 40)
for cat, counts in sorted(result["category_summary"].items()):
lines.append(f" {cat}: {counts['errors']} errors, {counts['warnings']} warnings")
lines.append("")
if result["issues"]:
lines.append("-" * 40)
lines.append("DETAILED ISSUES")
lines.append("-" * 40)
for issue in result["issues"]:
severity = "[ERROR]" if issue["severity"] == "error" else "[WARN]"
lines.append(f"\n{severity} [{issue['category']}] {issue['popup_id']}")
lines.append(f" Issue: {issue['issue']}")
lines.append(f" Fix: {issue['fix']}")
else:
lines.append("No issues found.")
lines.append("")
return "\n".join(lines)
def main() -> None:
"""Main entry point."""
parser = argparse.ArgumentParser(
description="Audit popup configurations for compliance, frequency, and best practices."
)
parser.add_argument(
"input_file",
help="Path to JSON file with popup inventory",
)
parser.add_argument(
"--format",
choices=["text", "json"],
default="text",
help="Output format (default: text)",
)
args = parser.parse_args()
try:
with open(args.input_file, "r") as f:
data = json.load(f)
except FileNotFoundError:
print(f"Error: File not found: {args.input_file}", file=sys.stderr)
sys.exit(1)
except json.JSONDecodeError as e:
print(f"Error: Invalid JSON: {e}", file=sys.stderr)
sys.exit(1)
result = audit_popups(data)
if args.format == "json":
print(json.dumps(result, indent=2))
else:
print(format_text(result))
if __name__ == "__main__":
main()
Related skills
FAQ
What popup format converts best for lead capture?
Per the format matrix, exit-intent modals on landing pages and scroll-triggered slide-ins or inline expansions on content pages; full-screen overlays are avoided on mobile because Google penalizes them.
When should a time-delay popup fire?
The skill recommends 15-30 seconds: under 10s feels aggressive and over 60s misses visitors.