[CommerceTracking] skips the action outside view mode, so CMS edit and preview render a blank page

Any action decorated with [CommerceTracking] returns an empty HTTP 200 when it is opened in CMS edit or preview mode. The editor sees a blank preview iframe, the public URL renders normally, and nothing is logged. Our start pages and every product and category page were blank for editors until we replaced the attribute.

Seen on EPiServer.Tracking.Commerce 4.2.1 (net8.0). 4.1.46 has the same code. 4.1.31 and earlier are not affected, so the change landed somewhere between 4.1.31 and 4.1.46.

Why it happens

  • CommerceTrackingAttribute.OnActionExecutionAsync has three early returns that skip await next(). They run when IContentRouteHelper.Content is null, when IContextModeResolver.CurrentMode is not ContextMode.Default (so in edit and preview), and when TrackingRequestFactory.CreateTrackingData returns null.
  • In an async action filter, returning without calling next() and without setting context.Result short-circuits the pipeline. The action, its view and every later filter never run, and MVC writes an empty 200.
  • Up to 4.1.31 the attribute overrode the synchronous OnActionExecuting, where an early return was harmless because MVC runs the action afterwards anyway. The early exits were kept as they were when the method became async, so they now cancel the request instead of just skipping tracking.

We also hit the null-data case outside the CMS, on cart pages. There the factory looks the cart up by Cart.DefaultName, and our carts use a different name, so the public cart page rendered blank too.

To reproduce

Put [CommerceTracking(TrackingType.Home)] on a page controller's Index action and open that page in CMS edit mode or preview. The iframe is blank, and the network tab shows a 200 response with content-length: 0. Remove the attribute and the page renders.

Suggested fix

Call await next() on every path, so that the conditions only decide whether to track:

public override async Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next)
{
    var content = _routeHelper.Service.Content;
    if (content != null && PageIsInViewMode())
    {
        var data = CreateTrackingContent(content, context);
        if (data != null)
        {
            if (SkipRecommendations)
                data.SkipRecommendations();
            ProcessTrackingResult(await _trackingService.Service.TrackAsync(data, context.HttpContext, content, Scope), context);
        }
    }

    await next();
}

Workaround

We wrap the stock attribute and call next() ourselves whenever it didn't:

 
using EPiServer.Tracking.Commerce;
using Microsoft.AspNetCore.Mvc.Filters;

/// <summary>
/// [CommerceTracking] that always runs the action. Whether to track is still left to the stock filter.
/// </summary>
[AttributeUsage(AttributeTargets.Method)]
public sealed class SafeCommerceTrackingAttribute : ActionFilterAttribute
{
    private readonly CommerceTrackingAttribute _inner;

    public SafeCommerceTrackingAttribute(TrackingType trackingType)
    {
        _inner = new CommerceTrackingAttribute(trackingType);
    }

    public override async Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next)
    {
        var nextCalled = false;
        await _inner.OnActionExecutionAsync(context, () =>
        {
            nextCalled = true;
            return next();
        });

        if (!nextCalled)
            await next();
    }
}

Then replace [CommerceTracking(TrackingType.X)] with [SafeCommerceTracking(TrackingType.X)]. The stock filter still decides whether to track, so nothing is tracked in edit or preview. The flag makes sure the action runs exactly once: through the stock filter when it tracks, and through the wrapper when it doesn't. If you use Scope or SkipRecommendations, add pass-through properties that set them on _inner.

With the wrapper, edit and preview render again. Pages that hit the null-data case now render too, but they still track nothing, so we gave our cart, checkout and wishlist pages their own tracking. We'll remove the wrapper once the stock filter calls next() on every path.

#343759
Nov 05, 2026 22:11

Thanks for the report, I will create a bug for Commerce team who owns this. 

#343770
Nov 06, 2026 13:01
Quan Mai - Nov 06, 2026 13:02
The bug is COM-22643
* You are NOT allowed to include any hyperlinks in the post because your account hasn't associated to your company. User profile should be updated.