Safari's aggressive request cancellation during page navigation has long been a challenge for Sitecore developers implementing goal tracking. When users click links, Safari immediately cancels pending API requests, causing analytics data loss and incomplete conversion tracking. This article presents a production-tested solution using the Beacon API that ensures reliable goal tracking across all browsers, with specific optimizations for Safari's strict handling of async operations.
The Safari Navigation Challenge
Safari's approach to handling pending requests during navigation differs significantly from other browsers, creating unique challenges for Sitecore analytics implementations:
- Immediate Request Cancellation: Safari cancels all pending XHR/Fetch requests when navigation begins
- Race Condition Issues: Traditional async tracking calls lose the race against page unload
- Incomplete Analytics: Goal tracking failures result in missing conversion data
- Browser-Specific Behavior: Code that works in Chrome/Firefox fails silently in Safari
- Business Impact: Lost analytics data affects decision-making and ROI tracking
These issues manifest as canceled or blocked requests in Safari's developer console, with analytics events failing to reach Sitecore's xConnect. The Beacon API provides a browser-standard solution specifically designed for this use case.
Understanding the Beacon API Advantage
The Beacon API is a W3C standard specifically created to handle analytics and diagnostic data during page unload. Unlike traditional HTTP requests, Beacon requests are guaranteed to complete even after the page navigates away:
Key Benefits for Sitecore Tracking
// Traditional approach - fails in Safari on navigation
fetch('/api/sitecore/trackgoal', {
method: 'POST',
body: JSON.stringify({ goalId: 'purchase-complete' }),
}) // ❌ Canceled when user navigates
// Beacon API approach - survives navigation
const formData = new FormData()
formData.append('goalId', 'purchase-complete')
navigator.sendBeacon('/api/sitecore/trackgoal', formData)
// ✅ Completes even after page unloadThe Beacon API offers several advantages over traditional tracking methods:
- Guaranteed Delivery: Requests complete regardless of page state
- Browser Optimization: Queued and sent at optimal network times
- Non-Blocking: Doesn't delay page navigation or user interactions
- Automatic Retry: Built-in retry logic for failed requests
- Low Overhead: Minimal impact on page performance
Browser Support Matrix
The Beacon API has excellent browser support, making it production-ready for enterprise Sitecore implementations:
// Browser support check
function hasBeaconSupport() {
return 'sendBeacon' in navigator
}
// Support matrix (all modern browsers supported):
// ✅ Chrome 39+ (2014)
// ✅ Safari 11.1+ (2018) - Critical for our use case
// ✅ Firefox 31+ (2014)
// ✅ Edge (all versions)
// ❌ Internet Explorer (not supported)Client-Side JavaScript Implementation
Let's implement a robust JavaScript solution that handles Sitecore goal tracking on the client-side:
Core Tracking Function
/**
* Sitecore Goal Tracking with Beacon API
* Ensures reliable goal tracking even during page navigation
*/
function trackSitecoreGoal(goalId, goalValue, additionalData) {
// Feature detection
if (!navigator.sendBeacon) {
console.error('Beacon API not supported in this browser')
return false
}
// Prepare data for Beacon API
const formData = new FormData()
formData.append('goalId', goalId)
formData.append('goalValue', goalValue || '0')
formData.append('timestamp', new Date().toISOString())
// Add any additional tracking data
if (additionalData) {
Object.keys(additionalData).forEach((key) => {
formData.append(key, additionalData[key])
})
}
// Include Sitecore session info if available
if (typeof Sitecore !== 'undefined' && Sitecore.Analytics) {
formData.append('contactId', Sitecore.Analytics.contactId || '')
formData.append('sessionId', Sitecore.Analytics.sessionId || '')
}
// Send beacon - returns true if queued successfully
const queued = navigator.sendBeacon(
'/api/sitecore/analytics/triggergoal',
formData,
)
if (!queued) {
console.error('Beacon queue full - request may not be sent')
}
return queued
}Automatic Link Tracking
Automatically track goals for links with data attributes, without interfering with natural navigation:
// Automatic link tracking setup
document.addEventListener('DOMContentLoaded', function () {
// Find all links with goal tracking attributes
const trackedLinks = document.querySelectorAll(
'a[data-goal-id], a[data-sc-goal]',
)
trackedLinks.forEach((link) => {
link.addEventListener('click', function (e) {
const goalId = this.dataset.goalId || this.dataset.scGoal
const goalValue = this.dataset.goalValue || this.dataset.scGoalValue
if (goalId) {
// Track goal - let beacon API handle it
trackSitecoreGoal(goalId, goalValue, {
linkUrl: this.href || '',
linkText: (this.textContent || '').substring(0, 200), // Limit length
pageUrl: window.location.href,
})
// Important: Do NOT preventDefault()
// Let the navigation happen naturally
}
})
})
})
// HTML usage example:
// <a href="/products"
// data-goal-id="{110D559F-DEA5-42EA-9C1C-8A5DF7E70EF9}"
// data-goal-value="10">
// View Products
// </a>Advanced Tracking with Confirmation
For critical goals where you need confirmation before navigation, implement a Promise-based approach:
// Enhanced version with Promise support for confirmation needs
function trackSitecoreGoalWithConfirmation(goalId, goalValue) {
return new Promise((resolve, reject) => {
// Ensure Beacon API is available
if (!navigator.sendBeacon) {
reject(new Error('Beacon API not supported'))
return
}
// With Beacon API, we can't get confirmation
// But we can verify it was queued
const formData = new FormData()
formData.append('goalId', goalId)
formData.append('goalValue', goalValue || '0')
const queued = navigator.sendBeacon(
'/api/sitecore/analytics/triggergoal',
formData,
)
if (queued) {
resolve(true)
} else {
reject(new Error('Failed to queue beacon'))
}
})
}
// Usage for critical conversions
function handlePurchaseComplete(event) {
event.preventDefault()
const purchaseValue = calculateOrderTotal()
trackSitecoreGoalWithConfirmation('purchase-complete', purchaseValue)
.then(() => {
// Navigate to thank you page
window.location.href = '/thank-you'
})
.catch((error) => {
console.error('Tracking failed:', error)
// Navigate anyway - don't break UX
window.location.href = '/thank-you'
})
}Server-Side C# Implementation
The server-side implementation must handle FormData from the Beacon API while ensuring proper Sitecore Analytics integration. Remember to register your controller route in your MVC application:
Analytics Controller
using System;
using System.Linq;
using System.Web.Mvc;
using Sitecore.Analytics;
using Sitecore.Analytics.Data;
using Sitecore.Analytics.Tracking;
using Sitecore.Configuration;
namespace YourProject.Controllers
{
public class AnalyticsController : Controller
{
/// <summary>
/// Endpoint optimized for Beacon API goal tracking.
/// Handles FormData from navigator.sendBeacon().
/// SECURITY: this is a public endpoint that writes to the visitor's
/// own xDB session. Add a same-origin / Referer check to stop other
/// sites from forging beacons via fetch (sendBeacon doesn't honor
/// CORS preflights, so the browser will not block cross-origin
/// posts on its own).
/// </summary>
[HttpPost]
[Route("api/sitecore/analytics/triggergoal")]
public ActionResult TriggerGoal()
{
// Reject cross-origin beacon posts. Same-origin allow-list is
// typically enough; you don't need anti-forgery tokens because
// the endpoint never reads cookies for authentication.
if (!IsSameOriginRequest(Request))
return new HttpStatusCodeResult(403);
try
{
// Extract data from FormData
string goalId = Request.Form["goalId"];
string goalValue = Request.Form["goalValue"] ?? "0";
string timestamp = Request.Form["timestamp"];
string linkUrl = Request.Form["linkUrl"];
// Validate input
if (string.IsNullOrEmpty(goalId) || goalId.Length > 100)
{
return new HttpStatusCodeResult(400, "Invalid goal ID");
}
// Validate goal ID format (GUID or alphanumeric name)
if (!IsValidGoalId(goalId))
{
Sitecore.Diagnostics.Log.Warn(
"Invalid goal ID format attempted", this);
return new HttpStatusCodeResult(400);
}
// Ensure analytics is enabled
if (!Tracker.IsActive || !Tracker.Enabled)
{
// Try to initialize tracker
Tracker.StartTracking();
}
// Verify tracker is now active
if (Tracker.Current == null || !Tracker.Current.IsActive)
{
// Log for debugging but return success to not break navigation
Sitecore.Diagnostics.Log.Warn(
"Analytics tracker not active", this);
return new HttpStatusCodeResult(204); // No Content
}
// Register the goal
bool success = RegisterGoal(goalId, goalValue, linkUrl);
if (success)
{
// Force save to xConnect immediately
// Important for beacon requests that may not have follow-up requests
Tracker.Current.Interaction.AcceptModifications();
// Consider flushing session for critical goals
if (IsCriticalGoal(goalId))
{
Tracker.Current.Session.Identify(
Tracker.Current.Contact.ContactId.ToString());
}
}
// Return 204 No Content - optimal for Beacon API
// No response body needed and saves bandwidth
return new HttpStatusCodeResult(204);
}
catch (Exception ex)
{
Sitecore.Diagnostics.Log.Error(
"Error triggering goal via Beacon API", ex, this);
// Still return success to not break user navigation
return new HttpStatusCodeResult(204);
}
}
private bool IsValidGoalId(string goalId)
{
// Allow GUIDs
Guid guidOutput;
if (Guid.TryParse(goalId, out guidOutput))
return true;
// Allow only safe characters for goal names
return System.Text.RegularExpressions.Regex.IsMatch(
goalId, @"^[a-zA-Z0-9\-\s]+$");
}
private static bool IsSameOriginRequest(HttpRequestBase request)
{
var origin = request.Headers["Origin"];
var referer = request.UrlReferrer?.GetLeftPart(UriPartial.Authority);
var expected = request.Url.GetLeftPart(UriPartial.Authority);
// Beacon requests omit Origin in some browsers — fall back to Referer.
if (!string.IsNullOrEmpty(origin))
return string.Equals(origin, expected, StringComparison.OrdinalIgnoreCase);
if (!string.IsNullOrEmpty(referer))
return string.Equals(referer, expected, StringComparison.OrdinalIgnoreCase);
// No Origin and no Referer — reject. Legitimate beacons from your
// pages always have at least one of these set.
return false;
}Goal Registration
/// <summary>
/// Register goal in Sitecore Analytics
/// </summary>
private bool RegisterGoal(string goalId, string goalValue, string linkUrl)
{
try
{
Guid goalGuid;
// Get the context database - typically 'web' on CD servers
var database = Sitecore.Context.Database ?? Factory.GetDatabase("web");
// Handle both GUID and path formats
if (Guid.TryParse(goalId, out goalGuid))
{
// Goal ID is a GUID
var goalItem = database.GetItem(new Sitecore.Data.ID(goalGuid));
if (goalItem == null) return false;
var goal = new Sitecore.Analytics.Data.Items.PageEventItem(goalItem);
var pageEventData = new PageEventData(goal.Name, goalGuid)
{
Value = ParseGoalValue(goalValue),
Data = linkUrl,
Text = $"Goal triggered via Beacon API at {DateTime.UtcNow}"
};
Tracker.Current.CurrentPage.Register(pageEventData);
return true;
}
else
{
// Try to find goal by path or ID
var goalItem = database.GetItem(goalId);
if (goalItem == null)
{
// Try to find in marketing definitions using safe query
var goalsRoot = database.GetItem("/sitecore/system/Marketing Control Panel/Goals");
if (goalsRoot != null)
{
// Use Axes API for safe traversal
goalItem = goalsRoot.Axes.GetDescendants()
.FirstOrDefault(item => item.Name.Equals(goalId, StringComparison.OrdinalIgnoreCase)
|| item.DisplayName.Equals(goalId, StringComparison.OrdinalIgnoreCase));
}
}
if (goalItem != null)
{
var goal = new Sitecore.Analytics.Data.Items.PageEventItem(goalItem);
var pageEventData = new PageEventData(goal.Name, goal.ID.ToGuid())
{
Value = ParseGoalValue(goalValue),
Data = linkUrl
};
Tracker.Current.CurrentPage.Register(pageEventData);
return true;
}
}
return false;
}
catch (Exception ex)
{
Sitecore.Diagnostics.Log.Error($"Failed to register goal: {goalId}", ex, this);
return false;
}
}
private int ParseGoalValue(string value)
{
int result;
return int.TryParse(value, out result) ? result : 0;
}
private bool IsCriticalGoal(string goalId)
{
// Define your critical goals that need immediate persistence
var criticalGoals = Settings.GetSetting("Analytics.CriticalGoals", "").Split('|');
return Array.Exists(criticalGoals,
g => g.Equals(goalId, StringComparison.OrdinalIgnoreCase));
}Configuration and Setup
Proper configuration ensures your Beacon API implementation works seamlessly with Sitecore's analytics infrastructure:
Web.config Settings
Add these key settings to ensure the Beacon API works with Sitecore's analytics:
<configuration>
<sitecore>
<settings>
<!-- Ensure analytics tracking is enabled -->
<setting name="Analytics.Enabled" value="true" />
<setting name="Xdb.Enabled" value="true" />
<setting name="Xdb.Tracking.Enabled" value="true" />
<!-- Custom setting for your critical goals (create this yourself) -->
<setting name="Analytics.CriticalGoals" value="purchase|signup|download" />
</settings>
</sitecore>
</configuration>
Testing and Debugging
Comprehensive testing ensures your implementation works across all browsers and scenarios:
Browser Console Testing
// Test 1: Check Beacon API availability
console.log('Beacon API supported:', 'sendBeacon' in navigator)
// Test 2: Send test beacon
function testBeacon() {
const testData = new FormData()
testData.append('goalId', 'test-goal-id')
testData.append('goalValue', '5')
testData.append('test', 'true')
const sent = navigator.sendBeacon(
'/api/sitecore/analytics/triggergoal',
testData,
)
console.log('Test beacon queued:', sent)
if (!sent) {
console.error('Beacon failed - check queue or endpoint')
}
}
// Test 3: Monitor all goal tracking
;(function () {
const originalBeacon = navigator.sendBeacon
navigator.sendBeacon = function (url, data) {
console.log('Beacon sent to:', url, data)
return originalBeacon.call(this, url, data)
}
})()
// Test 4: Simulate link click with goal
function testLinkGoalTracking() {
const testLink = document.createElement('a')
testLink.href = 'https://example.com'
testLink.dataset.goalId = '{110D559F-DEA5-42EA-9C1C-8A5DF7E70EF9}'
testLink.dataset.goalValue = '10'
testLink.textContent = 'Test Link'
document.body.appendChild(testLink)
// Trigger click
testLink.click()
}Sitecore Analytics Verification
For development environments only, you can add a test endpoint:
// Test endpoint - ONLY for development!
[HttpGet]
[Route("api/sitecore/analytics/test")]
public ActionResult TestAnalytics()
{
// SECURITY: Block in production
#if !DEBUG
return new HttpStatusCodeResult(404);
#endif
var response = new
{
TrackerActive = Tracker.IsActive,
TrackerEnabled = Tracker.Enabled,
HasSession = Tracker.Current?.Session != null,
HasContact = Tracker.Current?.Contact != null,
InteractionCount = Tracker.Current?.Interaction?.Pages?.Count() ?? 0
};
return Json(response, JsonRequestBehavior.AllowGet);
}Troubleshooting Common Issues
Common issues and their solutions:
- Beacon returns false: Check endpoint URL availability and beacon queue status
- Goals not in xDB: Ensure session flush for critical goals
- Safari still blocking: Verify HTTPS and SameSite cookie settings
- Missing session data: Call Tracker.StartTracking() in controller
- Robot detection blocking: Disable AutoDetectBots or whitelist Beacon requests
Security Considerations
Implement security measures to protect your analytics endpoint from abuse:
Implementation Security Notes
// Security Checklist for Production:
//
// 1. Origin pinning (shown above): same-origin check on every request — the
// Beacon API does not preflight, so without this any site could spam goals
// into your visitors' sessions via fetch.
//
// 2. Authentication (only if the endpoint logs identified users):
// - Sitecore session is established via cookies; goals attach to whoever
// the cookie says they are. No extra auth needed for anonymous tracking.
// - For authenticated-only goals, add [Authorize] and check User.Identity.
//
// 3. Rate Limiting (recommended):
private bool CheckRateLimit()
{
var ipAddress = Request.UserHostAddress;
var cacheKey = $"GoalRateLimit_{ipAddress}";
var count = HttpContext.Cache[cacheKey] as int? ?? 0;
if (count > 100) // Max 100 per minute per IP
{
Sitecore.Diagnostics.Log.Warn(
$"Rate limit exceeded for IP: {ipAddress}", this);
return false;
}
HttpContext.Cache.Insert(cacheKey, count + 1,
null, DateTime.Now.AddMinutes(1), TimeSpan.Zero);
return true;
}
//
// 3. Input Validation (already shown in code above)
// 4. Use HTTPS in production (enforce in IIS/web.config)
// 5. Log suspicious activity for monitoring
// 6. Consider implementing CSRF tokens if needed
// 7. Sanitize all user input before storageCORS Configuration
Restrict CORS to your domains only for production environments:
<!-- Production CORS configuration -->
<!-- IMPORTANT: Never use wildcard (*) in production -->
<add name="Access-Control-Allow-Origin" value="https://yourdomain.com" />
<add name="Access-Control-Allow-Credentials" value="true" />
<add name="Access-Control-Max-Age" value="86400" />
Conclusion and Migration Strategy
The Beacon API provides a robust, browser-standard solution to Safari's aggressive request cancellation during navigation, ensuring reliable Sitecore goal tracking across all modern browsers. This implementation has been successfully deployed in production environments, resolving long-standing analytics data loss issues.
Key benefits of this implementation:
- ✅ Reliable tracking across all modern browsers, especially Safari
- ✅ Zero data loss during page navigation
- ✅ Minimal performance impact with asynchronous processing
- ✅ Seamless integration with Sitecore 10.3 analytics infrastructure
- ✅ Production-tested with measurable improvements in data capture
Migration Recommendations
For existing Sitecore implementations, follow this phased migration approach:
- Phase 1: Implement alongside existing tracking without removing current code
- Phase 2: Enable for Safari users first, monitor analytics data quality
- Phase 3: Expand to all browsers after validation period
- Phase 4: Maintain synchronous fallback for legacy browser support
- Phase 5: Remove old implementation after comprehensive testing
This solution transforms unreliable goal tracking into a robust analytics foundation, enabling accurate conversion tracking and data-driven decision making across your Sitecore implementation.
Additional Resources
For further reading and technical documentation:
- MDN Web Docs: Beacon API - Complete API reference and browser compatibility
- Navigator.sendBeacon() Method - Detailed method documentation
- W3C Beacon Specification - Official W3C specification
- Can I Use: Beacon API - Current browser support matrix
