Ghost Memberships E-commerce Tracking with GA4 | OpsBlu Docs

Ghost Memberships E-commerce Tracking with GA4

Track Ghost paid memberships, subscription tiers, and checkout events with GA4 e-commerce tracking

Ghost's native membership and subscription system integrates with Stripe for payments. Track the complete subscription lifecycle with GA4 e-commerce events to measure revenue, conversion rates, and member value.

Ghost Membership Model

Ghost memberships follow an e-commerce pattern:

  • Products - Ghost Tiers (Free, Paid, Custom tiers)
  • Transactions - Member subscriptions (monthly/yearly)
  • Checkout - Ghost Portal payment flow
  • Revenue - Stripe-processed subscription payments
  • Customers - Ghost Members with paid subscriptions

E-commerce Event Mapping

Map Ghost membership actions to GA4 e-commerce events:

Ghost Action GA4 Event Description
View pricing view_item_list Member views available tiers
Select tier select_item Member clicks on tier
Begin checkout begin_checkout Portal checkout opens
Add payment info add_payment_info Stripe payment form shown
Purchase subscription purchase Successful subscription
Upgrade tier purchase Tier upgrade completed
Downgrade tier refund Tier downgrade (partial refund model)
Cancel subscription refund Subscription cancelled

Implementation Methods

Track subscription events using Ghost Portal's custom events:

{{!-- In default.hbs, before </body> --}}
<script>
  // Track tier selection in Portal
  window.addEventListener('portal-open', function(event) {
    if (typeof gtag !== 'undefined') {
      // User opened portal - track tier view
      gtag('event', 'view_item_list', {
        item_list_id: 'ghost_tiers',
        item_list_name: 'Membership Tiers',
        items: [
          {{#foreach @site.members_tiers}}
          {
            item_id: '{{id}}',
            item_name: '{{name}}',
            item_category: 'Ghost Membership',
            price: {{monthly_price}},
            currency: '{{currency}}',
            {{#if description}}
            item_variant: '{{description}}',
            {{/if}}
          }{{#unless @last}},{{/unless}}
          {{/foreach}}
        ]
      });
    }
  });

  // Track checkout initiation
  window.addEventListener('portal-checkout-start', function(event) {
    if (typeof gtag !== 'undefined') {
      gtag('event', 'begin_checkout', {
        event_category: 'ecommerce',
        event_label: 'ghost_subscription_checkout',
        currency: 'USD', // Update with your currency
        value: 0, // Will be updated when tier is selected
        items: []
      });
    }
  });

  // Track successful subscription
  window.addEventListener('portal-subscription-created', function(event) {
    if (typeof gtag !== 'undefined') {
      {{#if @member.subscriptions}}
      var subscription = {{json @member.subscriptions.[0]}};
      var tier = subscription.tier;

      gtag('event', 'purchase', {
        transaction_id: subscription.id,
        value: tier.monthly_price || tier.yearly_price,
        currency: tier.currency || 'USD',
        items: [{
          item_id: tier.id,
          item_name: tier.name,
          item_category: 'Ghost Membership',
          item_variant: subscription.cadence, // monthly or yearly
          price: subscription.cadence === 'month' ? tier.monthly_price : tier.yearly_price,
          quantity: 1
        }]
      });
      {{/if}}
    }
  });

  // Track subscription cancellation
  window.addEventListener('portal-subscription-cancelled', function(event) {
    if (typeof gtag !== 'undefined') {
      gtag('event', 'refund', {
        event_category: 'ecommerce',
        event_label: 'subscription_cancelled',
        currency: 'USD',
        value: 0, // Can't track refund amount from portal event
        transaction_id: event.detail?.subscription_id || 'unknown'
      });
    }
  });
</script>

Note: Ghost Portal events have limited data access. For complete e-commerce tracking, use Method 2.

Method 2: Stripe Webhook Integration (Server-Side)

For accurate revenue tracking, use Stripe webhooks with Ghost Admin API:

Step 1: Create Webhook Handler

Create a server-side webhook endpoint (Node.js example):

// webhook-handler.js
const express = require('express');
const stripe = require('stripe')(process.env.STRIPE_SECRET_KEY);
const axios = require('axios');

const app = express();
app.post('/webhook/stripe', express.raw({type: 'application/json'}), async (req, res) => {
  const sig = req.headers['stripe-signature'];
  let event;

  try {
    event = stripe.webhooks.constructEvent(req.body, sig, process.env.STRIPE_WEBHOOK_SECRET);
  } catch (err) {
    return res.status(400).send(`Webhook Error: ${err.message}`);
  }

  // Handle subscription events
  switch (event.type) {
    case 'customer.subscription.created':
      await trackSubscriptionPurchase(event.data.object);
      break;
    case 'customer.subscription.updated':
      await trackSubscriptionUpdate(event.data.object);
      break;
    case 'customer.subscription.deleted':
      await trackSubscriptionCancellation(event.data.object);
      break;
    case 'invoice.payment_succeeded':
      await trackPayment(event.data.object);
      break;
  }

  res.json({received: true});
});

async function trackSubscriptionPurchase(subscription) {
  // Send to GA4 Measurement Protocol
  const measurementId = 'G-XXXXXXXXXX';
  const apiSecret = 'your-measurement-protocol-secret';

  await axios.post(
    `https://www.google-analytics.com/mp/collect?measurement_id=${measurementId}&api_secret=${apiSecret}`,
    {
      client_id: subscription.metadata.ghost_member_uuid || subscription.customer,
      events: [{
        name: 'purchase',
        params: {
          transaction_id: subscription.id,
          value: (subscription.items.data[0].price.unit_amount / 100),
          currency: subscription.currency.toUpperCase(),
          items: [{
            item_id: subscription.items.data[0].price.product,
            item_name: subscription.metadata.tier_name || 'Paid Membership',
            item_category: 'Ghost Membership',
            item_variant: subscription.items.data[0].price.recurring.interval,
            price: (subscription.items.data[0].price.unit_amount / 100),
            quantity: 1
          }]
        }
      }]
    }
  );
}

async function trackPayment(invoice) {
  // Track recurring payments
  if (invoice.billing_reason === 'subscription_cycle') {
    const measurementId = 'G-XXXXXXXXXX';
    const apiSecret = 'your-measurement-protocol-secret';

    await axios.post(
      `https://www.google-analytics.com/mp/collect?measurement_id=${measurementId}&api_secret=${apiSecret}`,
      {
        client_id: invoice.customer,
        events: [{
          name: 'recurring_payment',
          params: {
            transaction_id: invoice.id,
            value: (invoice.amount_paid / 100),
            currency: invoice.currency.toUpperCase(),
            subscription_id: invoice.subscription,
            payment_type: 'recurring'
          }
        }]
      }
    );
  }
}

async function trackSubscriptionCancellation(subscription) {
  const measurementId = 'G-XXXXXXXXXX';
  const apiSecret = 'your-measurement-protocol-secret';

  await axios.post(
    `https://www.google-analytics.com/mp/collect?measurement_id=${measurementId}&api_secret=${apiSecret}`,
    {
      client_id: subscription.metadata.ghost_member_uuid || subscription.customer,
      events: [{
        name: 'refund',
        params: {
          transaction_id: subscription.id,
          event_category: 'subscription',
          event_label: 'cancelled',
          cancellation_reason: subscription.cancellation_details?.reason || 'unknown'
        }
      }]
    }
  );
}

app.listen(3000);

Step 2: Configure Stripe Webhook

  1. Log in to Stripe Dashboard
  2. Navigate to Developers → Webhooks
  3. Click Add endpoint
  4. Enter your webhook URL: https://yourdomain.com/webhook/stripe
  5. Select events:
    • customer.subscription.created
    • customer.subscription.updated
    • customer.subscription.deleted
    • invoice.payment_succeeded

Step 3: Store Member UUID in Stripe

Modify Ghost theme to pass member UUID to Stripe:

<script>
  window.addEventListener('portal-checkout-start', function() {
    {{#if @member}}
    // Add member UUID to Stripe metadata
    if (window.Stripe) {
      // This requires custom Ghost Portal modification
      console.log('Member UUID:', '{{@member.uuid}}');
    }
    {{/if}}
  });
</script>

Method 3: Client-Side Checkout Tracking

Track checkout steps in Ghost theme:

{{!-- In page.hbs or post.hbs where pricing is shown --}}
<script>
  {{#if @site.members_enabled}}
  // Track tier view
  if (typeof gtag !== 'undefined') {
    gtag('event', 'view_item_list', {
      item_list_id: 'membership_tiers',
      item_list_name: 'Ghost Membership Plans',
      items: [
        {{#foreach @site.members_tiers}}
        {
          item_id: '{{id}}',
          item_name: '{{name}}',
          item_category: 'Membership',
          {{#if monthly_price}}
          price: {{monthly_price}},
          {{/if}}
          {{#if currency}}
          currency: '{{currency}}',
          {{/if}}
          item_variant: 'monthly'
        }{{#unless @last}},{{/unless}}
        {{/foreach}}
      ]
    });
  }

  // Track tier selection (when user clicks pricing button)
  document.querySelectorAll('.tier-select-button').forEach(function(button) {
    button.addEventListener('click', function() {
      var tierId = this.dataset.tierId;
      var tierName = this.dataset.tierName;
      var tierPrice = parseFloat(this.dataset.tierPrice);

      if (typeof gtag !== 'undefined') {
        gtag('event', 'select_item', {
          item_list_id: 'membership_tiers',
          item_list_name: 'Ghost Membership Plans',
          items: [{
            item_id: tierId,
            item_name: tierName,
            item_category: 'Membership',
            price: tierPrice
          }]
        });

        // Track begin_checkout
        gtag('event', 'begin_checkout', {
          currency: 'USD',
          value: tierPrice,
          items: [{
            item_id: tierId,
            item_name: tierName,
            item_category: 'Membership',
            price: tierPrice,
            quantity: 1
          }]
        });
      }
    });
  });
  {{/if}}
</script>

Enhanced E-commerce Tracking

Track Free Tier Signups

Free memberships are still conversions:

<script>
  window.addEventListener('portal-signup', function(event) {
    {{#if @member}}
    {{#unless @member.paid}}
    // Free member signup
    if (typeof gtag !== 'undefined') {
      gtag('event', 'purchase', {
        transaction_id: '{{@member.uuid}}',
        value: 0,
        currency: 'USD',
        items: [{
          item_id: 'free-tier',
          item_name: 'Free Membership',
          item_category: 'Membership',
          price: 0,
          quantity: 1
        }]
      });
    }
    {{/unless}}
    {{/if}}
  });
</script>

Track Tier Upgrades

Monitor when free members upgrade to paid:

<script>
  window.addEventListener('portal-subscription-created', function(event) {
    {{#if @member}}
    var wasFree = !{{@member.paid}};

    if (wasFree && typeof gtag !== 'undefined') {
      // This is an upgrade from free to paid
      gtag('event', 'tier_upgrade', {
        event_category: 'membership',
        event_label: 'free_to_paid',
        {{#if @member.subscriptions}}
        new_tier: '{{@member.subscriptions.[0].tier.name}}',
        subscription_value: {{@member.subscriptions.[0].tier.monthly_price}}
        {{/if}}
      });
    }
    {{/if}}
  });
</script>

Track Subscription Lifetime Value

Calculate and send LTV metrics:

<script>
  {{#if @member.subscriptions}}
  var subscription = {{json @member.subscriptions.[0]}};
  var monthlyPrice = subscription.tier.monthly_price || 0;
  var yearlyPrice = subscription.tier.yearly_price || 0;
  var createdDate = new Date('{{@member.created_at}}');
  var now = new Date();
  var monthsSubscribed = (now - createdDate) / (1000 * 60 * 60 * 24 * 30);

  var lifetimeValue = monthlyPrice * monthsSubscribed;

  if (typeof gtag !== 'undefined') {
    gtag('set', 'user_properties', {
      'member_ltv': lifetimeValue.toFixed(2),
      'subscription_age_months': Math.floor(monthsSubscribed),
      'subscription_tier': subscription.tier.name
    });
  }
  {{/if}}
</script>

GA4 E-commerce Configuration

Enable E-commerce Reporting

  1. Navigate to GA4 → Admin → Data Streams
  2. Select your web data stream
  3. Click Configure tag settings → Show more
  4. Enable Enhanced measurement
  5. Turn on E-commerce events

Create Custom E-commerce Reports

Admin → Custom Definitions → Custom Metrics:

Metric Name Parameter Scope
Subscription Value value Event
Monthly Price price Event
Subscription Age subscription_age_months User
Member LTV member_ltv User

Admin → Custom Definitions → Custom Dimensions:

Dimension Name Parameter Scope
Subscription Tier item_name Event
Billing Cadence item_variant Event
Member Status member_status User
Cancellation Reason cancellation_reason Event

Create E-commerce Funnel

Explore → Funnel Exploration:

  1. Tier View - view_item_list
  2. Tier Selected - select_item
  3. Checkout Started - begin_checkout
  4. Payment Info Added - add_payment_info
  5. Purchase Completed - purchase

Performance Tracking

Key Metrics to Monitor

  • Conversion Rate - Portal views → Subscriptions
  • Average Order Value (AOV) - Average subscription value
  • Subscriber Lifetime Value (LTV) - Total revenue per subscriber
  • Churn Rate - Subscription cancellations / total subscriptions
  • Upgrade Rate - Free → Paid conversions
  • Revenue by Tier - Revenue breakdown by membership tier

Create LTV Cohort Analysis

<script>
  {{#if @member}}
  var joinedDate = new Date('{{@member.created_at}}');
  var cohortMonth = joinedDate.getFullYear() + '-' + String(joinedDate.getMonth() + 1).padStart(2, '0');

  if (typeof gtag !== 'undefined') {
    gtag('set', 'user_properties', {
      'cohort_month': cohortMonth,
      'days_since_join': Math.floor((new Date() - joinedDate) / (1000 * 60 * 60 * 24))
    });
  }
  {{/if}}
</script>

Common Issues

Revenue Mismatch with Stripe

  • Currency Conversion - GA4 uses single currency; convert Stripe amounts
  • Refunds Not Tracked - Implement webhook-based refund tracking
  • Recurring Payments - Track each renewal, not just initial purchase
  • Failed Payments - Don't track failed Stripe charges

Duplicate Purchase Events

  • Portal + Webhook - Use only one tracking method (webhook preferred)
  • Client-Side Issues - Portal events can fire multiple times
  • Use Transaction IDs - GA4 deduplicates by transaction_id

Missing Member Context

  • Cached Pages - Member data may be stale on cached pages
  • Use Server-Side - Webhook tracking more accurate than client-side
  • UUID Issues - Ensure member UUID passed to all events

Attribution Problems

  • Multi-Touch - Members may sign up via multiple channels
  • Cookie Consent - Ensure tracking runs before checkout
  • Cross-Device - Track member UUID for cross-device attribution

Testing E-commerce Tracking

Test Purchase Flow

  1. Enable GA4 DebugView (Admin → DebugView)
  2. Open Ghost site in incognito
  3. Click membership tier button
  4. Complete checkout flow (use Stripe test mode)
  5. Verify events in DebugView:
    • view_item_list
    • select_item
    • begin_checkout
    • purchase

Validate Revenue

Compare GA4 revenue with Stripe:

// In GA4 Explore → Free Form
// Metric: Total Revenue
// Dimension: Date
// Compare with Stripe Dashboard → Payments

Next Steps