Skip to main content

Feature Usage Tracking

Understanding how users interact with your features is essential for making data-driven decisions. Toggly tracks three distinct types of feature interactions: Checks, Views, and Usage. Each serves a different purpose in your analytics funnel.

The Feature Interaction Funnel​

┌─────────────────────────────────────────────────────────────────┐
│ CHECKS │
│ "How many users are ELIGIBLE for the feature?" │
│ ✅ Automatic - happens when IsEnabledAsync() is called │
├─────────────────────────────────────────────────────────────────┤
│ VIEWS │
│ "How many users VISITED the feature?" │
│ 👁️ Use [FeatureView] on pages that ARE the feature │
├─────────────────────────────────────────────────────────────────┤
│ USAGE │
│ "How many users ENGAGED with the feature?" │
│ 🖱️ Use [FeatureUsage] on actions within the feature │
└─────────────────────────────────────────────────────────────────┘

Understanding Each Metric​

Feature Checks (Evaluations)​

A check occurs every time your application evaluates whether a feature is enabled. This is automatically tracked by Toggly when you use the feature management SDK.

What it measures:

  • How often the feature decision is being made
  • The ratio of enabled vs disabled evaluations
  • Whether the feature code path is being executed

When it happens:

// Every call to IsEnabledAsync triggers a "check"
if (await featureManager.IsEnabledAsync("new-checkout"))
{
// Check recorded: enabled
}
else
{
// Check recorded: disabled
}

Use cases:

  • Detecting stale feature flags (no checks = code removed)
  • Understanding feature reach (how often the decision point is hit)
  • Debugging targeting rules (unexpected enabled/disabled ratios)
Automatic Tracking

Checks are recorded automatically. You don't need to call any additional methods.


Feature Views (Impressions)​

A view represents when a feature's UI component is actually rendered and displayed to the user. This is manually tracked because only you know when your feature is truly "seen."

What it measures:

  • How many users actually saw the feature
  • Impression counts for A/B test analysis
  • Visual reach of the feature

When to use it:

// User navigates to a page that IS the feature
// Landing on this page = viewing the feature
[FeatureView("new-dashboard")]
public IActionResult Dashboard()
{
return View();
}

The difference from checks:

  • A check happens at the decision point (is user eligible?)
  • A view happens when the user arrives at the feature

Consider this scenario:

// Home page - CHECK happens here (user qualifies for the feature)
public async Task<IActionResult> Index()
{
if (await featureManager.IsEnabledAsync("new-dashboard"))
{
// User qualifies, but they haven't SEEN the dashboard yet!
ViewBag.ShowDashboardLink = true;
}
return View();
}

// Dashboard page - VIEW happens when they actually visit
[FeatureView("new-dashboard")]
public IActionResult Dashboard()
{
// NOW we know the user actually visited the feature
return View();
}

Use cases:

  • Measuring how many users actually visited the feature page
  • Calculating "view → usage" conversion rates
  • Understanding feature discovery (how many eligible users find it)
ASP.NET Core Attribute

Use [FeatureView] on controller actions that serve feature pages. It gates AND tracks - checking if the feature is enabled and recording the view when the user lands on the page:

// User landing on this page = viewing the feature
[FeatureView("new-dashboard")]
public IActionResult Dashboard()
{
return View();
}

If the feature is disabled, the action returns 404 (or your custom IDisabledFeaturesHandler behavior).


Feature Usage (Interactions)​

Usage tracks when a user actually interacts with or engages with a feature. This is manually tracked when meaningful user actions occur.

What it measures:

  • Active engagement with the feature
  • Conversion from viewing to using
  • Feature adoption and stickiness

When to use it:

// User takes an action within the feature
// This shows they actually engaged with it, not just visited
[FeatureUsage("new-checkout")]
public async Task<IActionResult> SubmitOrder(OrderModel order)
{
// User completed an action = they USED the feature
await _orderService.ProcessAsync(order);
return RedirectToAction("Confirmation");
}

Examples of usage events:

  • Submitting a form (not just viewing it)
  • Completing a purchase
  • Saving settings
  • Sending a message
  • Any meaningful action that shows engagement

Use cases:

  • Measuring feature adoption
  • Calculating engagement rates
  • Determining if users find value in the feature
  • Making rollout decisions based on actual usage
ASP.NET Core Attribute

Use [FeatureUsage] on controller actions where users take meaningful actions. It gates AND tracks - checking if the feature is enabled and recording usage when the action executes:

// User submitting the form = using the feature
[FeatureUsage("new-checkout")]
public async Task<IActionResult> SubmitOrder(OrderModel order)
{
// Process order...
}

If the feature is disabled, the action returns 404 (or your custom IDisabledFeaturesHandler behavior).


Putting It All Together​

Here's a real-world example showing all three metrics. The key distinction:

  • View = User lands on a page that IS the feature
  • Usage = User takes an action within that feature
public class CheckoutController : Controller
{
private readonly IFeatureManager _featureManager;

// Step 1: CHECK - Happens automatically when routing decisions are made
// or when you call IsEnabledAsync() to decide what to show
public async Task<IActionResult> Index()
{
// Automatic check happens here - determines which checkout to show
if (await _featureManager.IsEnabledAsync("new-checkout"))
{
return RedirectToAction("NewCheckout");
}
return View("LegacyCheckout");
}

// Step 2: VIEW - User lands on the new checkout page
// Just arriving at this page = they're viewing the feature
[FeatureView("new-checkout")]
public IActionResult NewCheckout()
{
// User is now VIEWING the new checkout experience
return View();
}

// Step 3: USAGE - User submits their order (takes action)
// This is actual engagement with the feature
[FeatureUsage("new-checkout")]
public async Task<IActionResult> SubmitOrder(OrderModel order)
{
// User is now USING the new checkout - they completed an action
await _orderService.ProcessAsync(order);
return RedirectToAction("Confirmation");
}
}

The mental model:

  • Check: "Is this user eligible for the feature?" (automatic)
  • View: "Did the user see/visit the feature?" (landing on a feature page)
  • Usage: "Did the user engage with the feature?" (taking an action)

Analyzing the Funnel​

With all three metrics, you can calculate:

MetricFormulaWhat it tells you
Eligibility RateEnabled Checks / Total Checks% of users who qualify for the feature
Discovery RateViews / Enabled Checks% of eligible users who found/visited the feature
Engagement RateUsage / Views% of visitors who took action
Overall AdoptionUsage / Enabled Checks% of eligible users who engaged

SDK Reference​

Recording Views​

// Basic view recording
await usageStats.RecordViewAsync("feature-key");

// With context (for unique user tracking)
await usageStats.RecordViewAsync("feature-key", userContext);

Recording Usage​

// Basic usage recording
await usageStats.RecordUsageAsync("feature-key");

// With context (for unique user tracking)
await usageStats.RecordUsageAsync("feature-key", userContext);

ASP.NET Core Attributes​

Both [FeatureView] and [FeatureUsage] attributes gate AND track - they check if features are enabled before allowing the action to execute:

// Gates on feature-key, records view if enabled, returns 404 if disabled
[FeatureView("feature-key")]
public IActionResult MyAction() { }

// Gates on feature-key, records usage if enabled, returns 404 if disabled
[FeatureUsage("feature-key")]
public IActionResult MyAction() { }

// Multiple features (all must be enabled by default)
[FeatureView("feature-a", "feature-b")]
public IActionResult MyAction() { }

// RequirementType.Any - at least one feature must be enabled
[FeatureView("feature-a", "feature-b", RequirementType = RequirementType.Any)]
public IActionResult MyAction() { }
No FeatureGate Needed

You don't need to combine [FeatureGate] with [FeatureView] or [FeatureUsage] - the tracking attributes already include the gating behavior.


Best Practices​

1. Don't Over-Track​

Not every feature needs all three metrics. Choose based on your analysis needs:

Feature TypeCheckViewUsage
Backend optimization✅❌❌
UI component✅✅Optional
Interactive feature✅✅✅

2. Be Consistent​

Define what "view" and "usage" mean for your organization:

  • View: The moment the feature is rendered/displayed
  • Usage: A meaningful interaction (not just hovering)

3. Use Context for Unique Users​

Pass user context to get accurate unique user counts:

var context = new { UserId = currentUser.Id };
await usageStats.RecordViewAsync("feature", context);
await usageStats.RecordUsageAsync("feature", context);

4. Record at the Right Time​

  • Views: Record when the UI is actually rendered, not just when data is loaded
  • Usage: Record on meaningful actions, not on every click

Next Steps​