Simple pixel conversion tracking implementation in ASP.NET Core WebApi

Conversion tracking implementation using pixel method in ASP.NET Core

Pixel tracking, although it is the oldest method for tracking conversions in marketing, it is still used widely and some big companies like Facebook are still overing it as one of the methods for tracking the conversions of on your web pages.

Tracking conversion via pixel method is still widely used because of it's simplicity. It does not require any complex client side implementation ant thee fore it ensures that it will execute on pretty much all the browsers that can load images. It consists of a simple img tag on the page which has src attribute pointing to a tracking endpoint. The endpoint receives parameters from the GET request initiated by the image tag on HTML page rendering and sends parameters to backend. In return backend sends image content, usually 1x1 pixel transparent PNG or GIF content.

Typical sample of the pixel would be something like this

<img src="//mytrackdomain.com/pixel/?page=4309&region=us" alt="" width="1" height="1" />
    

Pixeltracking

Since it is a simple GET request, not an AJAX call, you do not need to enable CORS or anything else on the backend tracking side, so pretty simple and convenient to use. The logic of the tracking is on the server side, so we'll now focus of how to acquire most of the data from the request and respond with 1x1 pixel image.

Responding with an image content

Since the common thing for all the requests is to respond with an image content, we are going to do that first. What we need first is an image content, but if we keep loading an image from the disk on every request, this can hurt our response speed and therefore potentially slow down page loading in client's browser.

For this reason I decided to keep image content in appsettings.json as serialized base64 image content. This content is loaded to a singleton byte array and then same byte array instance is served on every request to controller action. So let's first store our 1x1pixel transparent image in a config.

{
  "Response": {
    "PixelContentBase64": "R0lGODlhAQABAPcAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACH5BAEAAP8ALAAAAAABAAEAAAgEAP8FBAA7",
    "PixelContentType": "image/gif"
  },
  "Logging": {
    "LogLevel": {
      "Default": "Warning"
    }
  },
  "AllowedHosts": "*"
}

    

Next thing is to create a byte array of image content which we read from the configuration. This is done in Startup.cs in ConfigureServices method

using System;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;

namespace DotnetCorePixelSample
{
    public class Startup
    {
        public Startup(IConfiguration configuration)
        {
            Configuration = configuration;
        }

        public IConfiguration Configuration { get; }

        // This method gets called by the runtime. Use this method to add services to the container.
        public void ConfigureServices(IServiceCollection services)
        {
            services.AddSingleton<FileContentResult>(new FileContentResult(
                    Convert.FromBase64String(this.Configuration.GetValue<String>("Response:PixelContentBase64")), 
                    this.Configuration.GetValue<String>("Response:PixelContentType")
                ));

            services.AddMvc();
        }

        // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
        public void Configure(IApplicationBuilder app, IHostingEnvironment env)
        {
            if (env.IsDevelopment())
            {
                app.UseDeveloperExceptionPage();
            }

            app.UseMvc();
        }
    }
}

    

Upon loading byte array content of our image, we store it in the FileContentResult class instance which will be used for a response returned from the pixel tracking WebAPI action. Since it is a single tone, same instance will be used over and over for each and every request without adding any additional processing on the request.

Collecting the data

What is left now, since we have the content injected from the DI configured in Startup is to collect the data. This is done in the controller action by accessing QueryString parameters and Header values which are part of every request.

Since we do not need to wait for the data storing to finish, we can wrap it in the Task and immediately return image response. This is one more performance improvement which, may cause whole page to perform bad on the client side.

using Microsoft.AspNetCore.Mvc;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.Extensions.Primitives;

namespace DotnetCorePixelSample.Controllers
{
    [Route("[controller]")]
    [ApiController]
    public class TrackController : ControllerBase
    {
        readonly FileContentResult pixelResponse;

        public TrackController(FileContentResult pixelResponse)
        {
            this.pixelResponse = pixelResponse;
        }

        public IActionResult get()
        {
            //get request parameters
            var parameters = Request.Query.Keys.ToDictionary(k => k, k => Request.Query[k]);

            //get request headers
            var headers = Request.Headers.Keys.ToDictionary(k => k, k => Request.Query[k]);

            Task.Factory.StartNew((data) =>
            {
                var dataDictionary = data as IDictionary<string, StringValues>;

                //values storing logic here

            }, parameters.Union(headers).ToDictionary(k=>k.Key, v=>v.Value)).ConfigureAwait(false);

            //return pixel
            return pixelResponse;
        }
    }
}
    

We are not going to wait for the save task to finish and dictionary data from the current context might not be available in the task while executing, so we are passing it to the Task as parameters and headers join of these two dictionaries and we cast it since it is boxed and passed to the Task method.

Storage of the data is irrelevant to this logic and you can use any storage to save the collected data.

Disclaimer

Purpose of the code contained in snippets or available for download in this article is solely for learning and demo purposes. Author will not be held responsible for any failure or damages caused due to any other usage.


About the author

DEJAN STOJANOVIC

Dejan is a passionate Software Architect/Developer. He is highly experienced in .NET programming platform including ASP.NET MVC and WebApi. He likes working on new technologies and exciting challenging projects

CONNECT WITH DEJAN  Loginlinkedin Logintwitter Logingoogleplus Logingoogleplus

JavaScript

read more

SQL/T-SQL

read more

Umbraco CMS

read more

PowerShell

read more

Comments for this article