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); }
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.
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.OnActionExecutionAsynchas three earlyreturns that skipawait next(). They run whenIContentRouteHelper.Contentis null, whenIContextModeResolver.CurrentModeis notContextMode.Default(so in edit and preview), and whenTrackingRequestFactory.CreateTrackingDatareturns null.next()and without settingcontext.Resultshort-circuits the pipeline. The action, its view and every later filter never run, and MVC writes an empty 200.OnActionExecuting, where an earlyreturnwas 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'sIndexaction and open that page in CMS edit mode or preview. The iframe is blank, and the network tab shows a 200 response withcontent-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:Workaround
We wrap the stock attribute and call
next()ourselves whenever it didn't: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 useScopeorSkipRecommendations, 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.