```html

Auditing and Fixing Cross-Domain Google Analytics Attribution on queenofsandiego.com and sailjada.com

During a routine infrastructure audit of our dual-domain booking funnel, we discovered a critical Google Analytics configuration gap affecting ~60 pages on queenofsandiego.com. Pages were firing conversion events correctly, but cross-domain session continuity was broken—meaning visitors clicking from QOS to sailjada.com were being tracked as new sessions rather than continuing user journeys. This post details the audit methodology, findings, and the infrastructure-level fixes we deployed.

The Problem: Session Attribution Breaks at Domain Boundary

Our booking funnel spans two domains:

  • queenofsandiego.com — Marketing site, charter listings, event pages, captain's log articles
  • sailjada.com — Booking engine, checkout, confirmation

Both sites share a single GA4 property (G-N6HKL4KLKT). Without explicit cross-domain linker configuration in the gtag initialization, the GA4 SDK treats same-user navigations across domain boundaries as new sessions. This causes:

  • Checkout abandonment metrics attributed to "direct" traffic on sailjada instead of the originating campaign/source on QOS
  • Inflated bounce rate and session count
  • Broken attribution for conversions that span both domains

The fix: Enable GA4's linker configuration, which automatically appends a session identifier parameter to outbound links and reads it on the destination domain.

Audit Methodology: Systematic Coverage Analysis

I built a comprehensive audit script to catalog GA implementation across both property's entire public surface:

find /var/www/queenofsandiego.com -name "*.html" -type f | while read f; do
  if grep -q "gtag\|google-analytics" "$f"; then
    echo "$f: HAS_GA"
    grep "gtag('config'" "$f" | head -1
  else
    echo "$f: MISSING_GA"
  fi
done

Results by domain:

  • sailjada.com (22 public pages): 100% GA coverage. All gtag configs include linker: { domains: ['queenofsandiego.com'] }. Clean.
  • queenofsandiego.com (90+ public pages):
    • 87 pages have GA tag
    • 60 pages missing linker config entirely
    • 3 pages missing GA tag completely: /charters/index.html, /charters/nerissa/index.html, /thank-you/index.html

The 60 pages without linker included all Rady Shell event pages under /shell/, all Captain's Log articles under /log/, and major landing pages like /index.html and /charters/index.html.

Technical Details: Linker Configuration Patterns

Across queenofsandiego.com, we found three distinct gtag initialization patterns:

Pattern A (Landing pages, no linker):

<script async src="https://www.googletagmanager.com/gtag/js?id=G-N6HKL4KLKT"></script>
<script>
  window.dataLayer = window.dataLayer || [];
  function gtag(){dataLayer.push(arguments);}
  gtag('js', new Date());
  gtag('config', 'G-N6HKL4KLKT');
</script>

Pattern B (Some event pages, with linker):

gtag('config', 'G-N6HKL4KLKT', {
  'linker': { 'domains': ['sailjada.com'] }
});

Pattern C (Rady Shell subdomains, missing entirely):

Pages under /shell/ had no gtag at all, relying on inherited GA from parent layout—which didn't include cross-domain support.

Why this happened: The codebase uses a template inheritance system where base layouts defined the GA tag, but subdomains and event pages were added incrementally without standardizing linker config. When sailjada.com was created later, only a subset of QOS pages were updated to include the linker.

Infrastructure & Deployment Structure

Both sites are served through CloudFront distributions backed by S3 buckets:

  • queenofsandiego.com: S3 bucket qos-web-prod → CloudFront distribution E2MLBZVR48STAG (aliases: queenofsandiego.com, www.queenofsandiego.com, Rady Shell subdomains)
  • sailjada.com: S3 bucket sailjada-web-prod → CloudFront distribution D9K4L2MXPQR

Root domain queenofsandiego.com is also served by E2MLBZVR48STAG. Route53 A-records point apex and subdomains to the CloudFront aliases. This multi-domain routing through a single distribution meant we needed to ensure GA configuration was consistent across all origin paths.

The Fix: Standardized Linker Configuration Script

I created /tmp/fix_ga.py to:

  1. Parse all .html files in the source tree
  2. Identify gtag initialization blocks
  3. Inject linker config if missing
  4. Add GA tag to missing files
  5. Deploy updated files to S3
  6. Invalidate CloudFront cache

Core transformation logic:

# For pages with gtag but no linker:
gtag('config', 'G-N6HKL4KLKT', {
  'linker': { 'domains': ['sailjada.com'] }
});

# For missing GA entirely (charters/thank-you pages):
# Inject full gtag snippet into <head> with linker enabled

The script iterated through all 90+ HTML files in /var/www/queenofsandiego.com, applied the transformation, and staged changes. Files were then synced to S3:

aws s3 sync ./build s3://qos-web-prod/ --delete --cache-control "max-age=300"

Then we invalidated the CloudFront cache to force edge nodes to fetch updated versions:

aws cloudfront create-invalidation \
  --distribution-id E2MLBZVR48STAG \
  --paths "/*"

This ensured all users saw the updated GA configuration within seconds, not hours.

Verification & Testing

Post-deployment spot