Skip to main content

Overview

Subscription tracking helps you monitor your SaaS revenue metrics, understand user subscription behavior, and predict churn. The SDK provides automatic payment provider detection (Stripe, Paddle, Chargebee) and comprehensive subscription lifecycle tracking.

Quick Start

1

Identify Users with Subscription Data

When users sign up or subscribe, identify them with subscription properties:
import { useAnalytics } from "mentiq-sdk";

function SubscriptionSuccess({ subscription }) {
  const { identify } = useAnalytics();

  useEffect(() => {
    identify(user.id, {
      email: user.email,
      subscription: {
        status: "active",
        plan_name: "Pro",
        plan_tier: "pro",
        mrr: 9900, // Monthly recurring revenue in cents
        billing_interval: "month",
        provider: "stripe",
      },
    });
  }, []);

  return <SuccessMessage />;
}
2

Track Subscription Events

Track lifecycle events as they happen:
const { trackSubscriptionStarted } = useAnalytics();

const handleSubscribe = async (plan) => {
  const subscription = await createSubscription(plan);
  
  trackSubscriptionStarted({
    status: "active",
    plan_name: plan.name,
    mrr: plan.price,
    provider: "stripe",
  });
};
3

Monitor Churn Risk

Use built-in churn prediction to identify at-risk users:
const { calculateChurnRisk } = useAnalytics();

const churnMetrics = calculateChurnRisk();

if (churnMetrics.risk_category === "high") {
  // Show retention offer
}

Subscription Properties

interface SubscriptionProperties {
  // Status & Plan
  status: "active" | "trialing" | "past_due" | "canceled" | "paused" | "incomplete" | "incomplete_expired" | "unpaid";
  plan_id?: string;
  plan_name?: string;
  plan_tier?: string; // e.g., "free", "starter", "pro", "enterprise"

  // Pricing (in cents)
  mrr?: number;       // Monthly Recurring Revenue
  arr?: number;       // Annual Recurring Revenue  
  ltv?: number;       // Lifetime Value
  currency?: string;  // e.g., "usd", "eur"

  // Billing Cycle
  billing_interval?: "day" | "week" | "month" | "year";
  current_period_start?: string; // ISO date
  current_period_end?: string;   // ISO date

  // Trial
  trial_start?: string;  // ISO date
  trial_end?: string;    // ISO date
  is_trial?: boolean;

  // Payment (PCI-safe - last 4 digits only)
  payment_method_type?: string;      // e.g., "card", "paypal"
  payment_method_last4?: string;     // Last 4 digits only
  payment_method_brand?: string;     // e.g., "visa", "mastercard"

  // Cancellation
  cancel_at_period_end?: boolean;
  canceled_at?: string;              // ISO date
  cancellation_reason?: string;

  // Provider Info
  provider?: "stripe" | "paddle" | "chargebee" | "manual" | string;
  provider_customer_id?: string;
  provider_subscription_id?: string;
}
PCI Compliance: Never store full credit card numbers. Only use payment_method_last4 (last 4 digits). The SDK automatically validates and truncates card data.

Automatic Provider Detection

The SDK can auto-detect subscription data from Stripe, Paddle, and Chargebee by scanning browser globals and localStorage.

How It Works

From src/subscription-detection.ts:412-431:
export function autoDetectSubscription(): ProviderDetectionResult {
  const results = [
    detectStripeSubscription(),
    detectPaddleSubscription(),
    detectChargebeeSubscription(),
  ];

  // Return highest confidence detection
  const detected = results
    .filter((r) => r.detected)
    .sort((a, b) => b.confidence - a.confidence);

  if (detected.length > 0) {
    return detected[0]; // Highest confidence
  }

  return { detected: false, confidence: 0 };
}

Detection Confidence

Each provider detection returns a confidence score (0-100):
  • 30+: Provider SDK loaded (e.g., window.Stripe)
  • 50+: Found provider-specific localStorage keys
  • 70+: Extracted subscription data from localStorage
  • 100: Full subscription details including pricing

Stripe Detection

From src/subscription-detection.ts:29-166, the SDK checks:
  1. window.Stripe object present (+30 confidence)
  2. LocalStorage keys containing “stripe”, “customer_id”, “subscription_id” (+20)
  3. Subscription object with plan data (+30)
  4. Valid customer ID starting with cus_ (+10)
export function detectStripeSubscription(): ProviderDetectionResult {
  let confidence = 0;
  const subscription: Partial<SubscriptionProperties> = {
    provider: "stripe",
  };

  // Check if Stripe.js is loaded
  if (window.Stripe) {
    confidence += 30;
  }

  // Check localStorage for Stripe data
  const storageKeys = Object.keys(localStorage);
  const stripeKeys = storageKeys.filter(
    (key) => key.includes("stripe") || key.includes("customer_id") || key.includes("subscription_id")
  );

  if (stripeKeys.length > 0) {
    confidence += 20;
    // Extract subscription data...
  }

  if (confidence > 30) {
    return {
      detected: true,
      provider: "stripe",
      confidence: Math.min(confidence, 100),
      subscription: subscription as SubscriptionProperties,
    };
  }

  return { detected: false, confidence };
}

When Detection Runs

From src/analytics.ts:1377-1434, auto-detection runs:
  • 1 second after SDK initialization (allows provider SDKs to load)
  • Only in browser environments (window exists)
  • Results merged with manually-set subscription data (manual takes precedence)
private setupSubscriptionAutoDetection(): void {
  setTimeout(() => {
    const detection = autoDetectSubscription();

    if (detection.detected && detection.subscription) {
      console.log(
        `Auto-detected ${detection.provider} subscription (confidence: ${detection.confidence}%)`
      );

      // Merge with existing manual data (manual takes precedence)
      const existing = localStorage.getItem("mentiq_user_subscription");
      const mergedSubscription = existing 
        ? { ...detection.subscription, ...JSON.parse(existing) }
        : detection.subscription;

      localStorage.setItem("mentiq_user_subscription", JSON.stringify(mergedSubscription));
    }
  }, 1000);
}

Subscription Lifecycle Events

Track Subscription Started

import { useAnalytics } from "mentiq-sdk";

function CheckoutSuccess({ subscription }) {
  const { trackSubscriptionStarted } = useAnalytics();

  useEffect(() => {
    trackSubscriptionStarted({
      status: "active",
      plan_name: "Pro Plan",
      plan_tier: "pro",
      mrr: 9900, // $99.00 in cents
      currency: "usd",
      billing_interval: "month",
      provider: "stripe",
      provider_subscription_id: subscription.id,
    });
  }, []);

  return <ThankYouPage />;
}

Track Trial Started

const { trackTrialStarted } = useAnalytics();

trackTrialStarted({
  status: "trialing",
  plan_name: "Pro Plan",
  is_trial: true,
  trial_start: new Date().toISOString(),
  trial_end: new Date(Date.now() + 14 * 24 * 60 * 60 * 1000).toISOString(), // 14 days
  provider: "stripe",
});

Track Upgrades

const { trackSubscriptionUpgraded } = useAnalytics();

trackSubscriptionUpgraded({
  status: "active",
  plan_name: "Enterprise",
  plan_tier: "enterprise",
  previous_plan: "Pro",
  mrr: 29900, // $299.00
  provider: "stripe",
});

Track Downgrades

const { trackSubscriptionDowngraded } = useAnalytics();

trackSubscriptionDowngraded({
  status: "active",
  plan_name: "Starter",
  plan_tier: "starter",
  previous_plan: "Pro",
  mrr: 2900, // $29.00
  provider: "stripe",
});

Track Cancellations

const { trackSubscriptionCanceled } = useAnalytics();

trackSubscriptionCanceled({
  status: "canceled",
  plan_name: "Pro",
  cancellation_reason: "too_expensive",
  canceled_at: new Date().toISOString(),
  cancel_at_period_end: true,
  provider: "stripe",
});

Track Payment Events

const { trackPaymentFailed, trackPaymentSucceeded } = useAnalytics();

// Payment succeeded
trackPaymentSucceeded({
  amount: 9900,
  currency: "usd",
  payment_status: "succeeded",
  invoice_id: "inv_123",
});

// Payment failed
trackPaymentFailed({
  amount: 9900,
  currency: "usd",
  payment_status: "failed",
  failure_reason: "card_declined",
  invoice_id: "inv_124",
});

Churn Risk Prediction

The SDK calculates churn risk based on user behavior:
import { useAnalytics } from "mentiq-sdk";

function ChurnRiskBanner() {
  const { calculateChurnRisk } = useAnalytics();
  const risk = calculateChurnRisk();

  if (risk.risk_category === "low") return null;

  return (
    <div className={`alert alert-${risk.risk_category}`}>
      <p>
        Churn Risk: {risk.risk_category.toUpperCase()} ({risk.risk_score}/100)
      </p>
      {risk.intervention_recommended && (
        <button>Get Help</button>
      )}
    </div>
  );
}

Churn Risk Factors

From src/analytics.ts:630-720, risk is calculated using:
interface ChurnRiskMetrics {
  risk_score: number;  // 0-100
  risk_category: "low" | "medium" | "high" | "critical";
  factors: {
    engagement_score?: number;         // Low engagement = higher risk
    days_since_last_active?: number;   // Inactivity = higher risk
    feature_adoption_rate?: number;    // Low adoption = higher risk
    support_tickets?: number;          // Many tickets = issues
    negative_feedback_count?: number;  // Feedback = dissatisfaction
    payment_failures?: number;         // Failed payments = critical
  };
  predicted_churn_date?: string;       // ISO date
  intervention_recommended?: boolean;  // Should you reach out?
}

Risk Calculation

  • Engagement Score < 20: +30 risk points
  • Inactive > 30 days: +40 risk points
  • Feature Adoption < 20%: +15 risk points
  • Support Tickets > 5: +10 risk points
  • Negative Feedback > 3: +20 risk points
  • Payment Failures > 2: +30 risk points
Risk Categories:
  • Low: 0-24 (healthy user)
  • Medium: 25-49 (monitor)
  • High: 50-74 (intervention needed)
  • Critical: 75-100 (immediate action)

Revenue Metrics

Track MRR/ARR

The SDK automatically calculates ARR from MRR and vice versa:
identify(user.id, {
  subscription: {
    mrr: 9900,              // $99/month
    billing_interval: "month",
    // ARR automatically calculated: 9900 * 12 = 118,800
  },
});

Get Current Subscription

const { getSubscriptionData } = useAnalytics();

const subscription = getSubscriptionData();

if (subscription?.status === "active") {
  console.log(`MRR: $${subscription.mrr / 100}`);
  console.log(`ARR: $${subscription.arr / 100}`);
}

Auto-Enrichment

All events are automatically enriched with subscription context: From src/analytics.ts:1141-1166:
private enqueueEvent(event: AnalyticsEvent): void {
  // Auto-enrich with subscription data from localStorage
  const subscriptionData = localStorage.getItem("mentiq_user_subscription");
  
  if (subscriptionData) {
    const subscription = JSON.parse(subscriptionData);
    
    event.properties = {
      ...event.properties,
      subscription_status: subscription.status,
      subscription_plan: subscription.plan_name || subscription.plan_tier,
      subscription_mrr: subscription.mrr,
      subscription_provider: subscription.provider,
      is_paid_user: subscription.status === "active" || subscription.status === "trialing",
    };
  }

  this.eventQueue.push(event);
}
This means every event automatically includes:
  • subscription_status
  • subscription_plan
  • subscription_mrr
  • subscription_provider
  • is_paid_user

Use Cases

Stripe Integration

import { useAnalytics } from "mentiq-sdk";
import { useStripe } from "@stripe/react-stripe-js";

function StripeCheckout() {
  const stripe = useStripe();
  const { trackSubscriptionStarted, identify } = useAnalytics();

  const handleSubscribe = async (priceId: string) => {
    const { subscription } = await stripe.subscriptions.create({
      customer: customerId,
      items: [{ price: priceId }],
    });

    // Track subscription
    trackSubscriptionStarted({
      status: subscription.status,
      plan_id: subscription.items.data[0].price.id,
      plan_name: subscription.items.data[0].price.nickname,
      mrr: subscription.items.data[0].price.unit_amount,
      billing_interval: subscription.items.data[0].price.recurring.interval,
      provider: "stripe",
      provider_subscription_id: subscription.id,
      provider_customer_id: customerId,
    });

    // Also update user identity
    identify(userId, {
      email: user.email,
      subscription: { /* same data */ },
    });
  };

  return <CheckoutForm onSubmit={handleSubscribe} />;
}

Retention Campaigns

function RetentionBanner() {
  const { calculateChurnRisk } = useAnalytics();
  const risk = calculateChurnRisk();

  if (risk.risk_category !== "high" && risk.risk_category !== "critical") {
    return null;
  }

  return (
    <div className="retention-banner">
      <h3>We noticed you haven't been as active lately</h3>
      <p>Can we help you get more value from {productName}?</p>
      <button>Schedule a call</button>
      <button>Get 50% off next month</button>
    </div>
  );
}

Best Practices

Subscription Tracking Tips:
  • Always use cents for monetary amounts (9900 = $99.00)
  • Track all lifecycle events (start, upgrade, downgrade, cancel)
  • Include cancellation reasons for analysis
  • Monitor churn risk and intervene early
  • Segment users by plan tier for targeted campaigns
  • Never log full credit card numbers (PCI compliance)
  • Use auto-detection for faster integration

Troubleshooting

Auto-Detection Not Working

  1. Check payment provider SDK is loaded (e.g., window.Stripe)
  2. Verify subscription data is in localStorage
  3. Enable debug mode: config.debug = true
  4. Wait 1 second after page load for detection to run

Subscription Data Not Persisting

Subscription data is stored in localStorage. If not persisting:
  1. Check browser allows localStorage
  2. Verify you’re calling identify() with subscription data
  3. Check for localStorage quota errors

Revenue Metrics Incorrect

Ensure you’re using cents, not dollars:
// ❌ Wrong
mrr: 99  // $0.99

// ✅ Correct  
mrr: 9900  // $99.00

Build docs developers (and LLMs) love