First-Party Experience

First Party Centric: Built-In Privacy

This journey began with a meticulous evaluation of the third-party services the platform relied on. Every third-party script that people take for granted uses and shares cookies with other vendors. Examples include embedding YouTube videos, implementing Google Analytics, Tag Managers, advertising scripts, and the massive list that keeps going.

📄 The code in this article is meant to entice ideas and not meant to be a template that can be copy and pasted. It is designed to mentor you to come up with ideas on how you would implement this yourself on the technology you work with.

Similar to the belief and goals with accessibility, privacy is a feature that should be built into your product. It’s not a checkbox that you can check off and say you’re done. It’s a continuous process of making sure your product is built with privacy in mind.

Transitioning from Third Parties

Transitioning to a first-party experience is an essential process for securing user data and maintaining a high level of privacy.

Take a look at these steps I took to shift towards a first-party experience:

  • Conduct a thorough analysis of all cookies and envision the functionality of 3ee Games without their use.
  • Implement a native HTML5 player to replace the YouTube player, while maintaining an external YouTube channel for marketing purposes.
  • Substitute Google Services such as Analytics and Fonts with cookie-free and self-hosted alternatives.

Though third-party services such as Google Analytics may seem appealing due to their ease of implementation and cost-effectiveness, these services often come at the expense of user data privacy. In reality, these services monetize user data, necessitating the use of cookie banners to inform users of data collection and offering them an option to opt out.

To make a change with so many moving parts can be challenging for businesses. An example: moving off the Youtube player: Such a move raises numerous questions, such as video hosting, closed captioning file storage, and creating a caching strategy.

Remember that embracing a first-party experience is a journey rather than a sprint. Hastily rushing through the process may result in unforeseen costs and complications.

Identify All Cookies

🍪 State Cookies:

Start by identifying cookies that store state, such as accepting or declining a cookie banner. State cookies are the first to go, and you should avoid using them whenever possible. If you have to use them, try to find alternative ways to store state.

There are many situations where developers have rushed to stored state in a cookie. The reason is: they have no other place to store it. There are what seem to be infinite reasons why, main one being: a backend service that is not ready to store state.

Another example: guest experiences. You allow a user to anonymously buy products. Try to encourage users to create an account instead. If your sign-up process is complicated, simplify it to encourage more people to sign up. By having users create an account, you can store their data properly instead of relying on state cookies.

🍪 Shared Cookies:

Once you’ve tackled state cookies, move on to researching third parties you want to replace. It’s important to find a self-hosted or homebrew solution that is GDPR compliant and does not use cookies. This may take time, but it’s worth the investment to have more control over your data.

Example: Replace Google Analytics

Ditching Google Analytics might seem like a bold move, but it’s necessary to achieve a first-party experience. To maintain the data analytics my business depends on, I needed a reliable alternative.

3ee Games uses Umami for analytics. Umami is open source, offers visitor privacy, data ownership, and is self hosted. Using a self hosted solution is what we want - I now own the data!

Let’s break down this down:

  • Plan a way to support both platforms. This will give you an advantage to compare the data between the two.
  • Translate custom events from Google Analytics to the new platform.
  • Configure switches to turn on and off each platform.

Example of handling multiple platforms

There are options to choose from when supporting different analytics software. A common solution is creating a wrapper around the analytics platform. This wrapper will handle the differences between the two platforms. Let’s create a wrapper that will handle the differences between the Google Analytics and umami:

let analytics = new Analytics();
analytics.trackEvent('video', 'play', 'video_id');

The wrapper calls trackEvent which in turn calls both Google Analytics and umami:

trackEvent(category, action, label) {
  //Google
  ga('send', 'event', category, action, label);

  //umami
  umami.trackEvent(`${category action label}`, { type: 'signup', moreInfo: 123 });
}

Preserving your events and porting them over to the new platform is a tedious task. Verify the events are working as expected in a networking tool such as the built-in browser networking tools, Fiddler, or Charles.

📄 To see only the requests made to the analytics platform, filter the requests by collect.

Check networking to see if events are working

Now that can use both Google Analytics and Umami. Let’s configure a switch to turn on and off Google Analytics. In the example below, let’s assume Umami is working correctly and we create a switch for Google Analytics and only fire the events if the switch is on:

//check config
let isGoogleOn = config.get('google_analytics');

//track event
trackEvent(category, action, label) {
  if (isGoogleOn) {
    ga('send', 'event', category, action, label);
  }
  umami.trackEvent(`${category action label}`, { type: 'signup', moreInfo: 123 });
}

This allows you to pivot quickly if you need to restore Google Analytics temporarly. With iteration, you can remove the switch and Google Analytics from your codebase:

trackEvent(category, action, label) {
  umami.trackEvent(`${category action label}`, { type: 'signup', moreInfo: 123 });
}

At this point, we have moved to our new platform. Whether you decided to use umami or another analytics platform. You’re now in control of your data!

Umami Dashboard Umami's dashboard showing the video events

From Cookies to LocalStorage

LocalStorage is a web storage API supported by modern web browsers, providing an efficient alternative to traditional cookies.

Cookies were once a default choice for storing small pieces of data in users’ browsers, but they have several drawbacks, including size limitations, cross-domain security concerns, and GDPR compliance challenges.

LocalStorage offers several advantages:

  • Increased Storage: provides a significantly larger storage capacity compared to cookies, which is especially valuable when dealing with user data.
  • First-Party: Data stored in LocalStorage is first-party data, which aligns with the privacy-centric approach of a first-party website.
  • Simplified Data Management: simplifies data management and retrieval, making it easier to work with user-specific information.
  • Cross-Origin Security: LocalStorage is subject to the same-origin policy, meaning data stored in LocalStorage can only be accessed by the same domain, enhancing user privacy and security.

🍪 Migrating from Cookies

To migrate from cookies to LocalStorage, follow these general steps:

  • Identify Cookies: Begin by identifying the cookies you currently use on your website. Determine the purpose of each cookie and whether it’s feasible to replace it with LocalStorage.
  • Implement LocalStorage: Replace cookie-related functionality with LocalStorage. The process may involve modifying JavaScript code responsible for reading and writing cookies.
  • Data Migration: Migrate existing cookie data to LocalStorage, ensuring a seamless transition for returning users.
  • Testing and Validation: Thoroughly test your website to confirm that LocalStorage is functioning as expected. Pay attention to user-specific data storage and retrieval.

Imagine we have a service that receives a user’s mute preference. We store in it a cookie and use it across the application:

// Set the cookie
fetch('https://example.com/mute')
  .then(response => {
    // Get the mute boolean from the response
    const mute = response.mute;

    // Set the cookie with a 7-day expiration date
    const expirationDate = new Date();
    expirationDate.setDate(expirationDate.getDate() + 7);
    document.cookie = `mute=${mute};expires=${expirationDate.toUTCString()};path=/`;
  })
  .catch(error => {
    console.error(`Error fetching mute boolean: ${error}`);
  });

Now we’ll port the script over to use localstorage instead:

// Set local storage
fetch('https://example.com/mute')
  .then(response => {
    // Get the mute boolean from the response
    const mute = response.mute;

    // Set local storage with the mute boolean
    localStorage.setItem('mute', mute);
  })
  .catch(error => {
    console.error(`Error fetching mute boolean: ${error}`);
  });

// Get mute from storage
const mute = localStorage.getItem('mute');

Now we’ll take it further and store an object in local storage by using JSON.stringify and JSON.parse:

// Set local storage
const videoPlayerSettings = {
  mute: true,
  volume: 50,
  autoplay: false,
  paused: false,
  fullscreen: true,
};

// Serialize the video player settings object to a string and store it in local storage
localStorage.setItem('videoPlayerSettings', JSON.stringify(videoPlayerSettings));

// Get video player settings from storage
const storedVideoPlayerSettings = JSON.parse(localStorage.getItem('videoPlayerSettings'));

// Access the properties from the stored video player settings object
const mute = storedVideoPlayerSettings.mute;
const autoplay = storedVideoPlayerSettings.autoplay;
const paused = storedVideoPlayerSettings.paused;

Updating Privacy Policies

Moving to a first-party experience involves more than just technical changes; it also requires transparency and compliance. When updating your policies, keep these points in mind:

  • Use clear and simple language for easy comprehension.
  • Outline the data collected and its intended purposes.
  • Describe data usage, whether for enhancing the user experience, analytics, legitimate purposes.
  • Disclose the use of cookies and local storage for data tracking.

Streamlining with Containers

Containerization simplifies managing applications by packaging them in digital containers. Think of it as neatly storing each application in a perfectly sized container, much like LEGO bricks fitting into designated spaces.

These containers bundle everything an application needs to run smoothly, offering a standardized way to transport and deploy them. Whether you’re testing locally, deploying in the cloud, or scaling up, containerization provides consistency and reliability.

Additionally, containerization ensures that your applications behave consistently across different environments, from development to testing to production. It’s like knowing your LEGO bricks will always fit together seamlessly.

📄 Imagine your application experiencing increased demand. Containerization lets you add containers effortlessly to handle the higher load, ensuring efficient performance and resource use. This streamlined approach is cost-effective and keeps your infrastructure running smoothly.

Troubleshooting with Docker

In our real-world example, Umami ships with a Docker Compose configuation that contains the application and a PostgreSQL database. Now we can configure, troubleshoot, monitor, and update Umami easily.

In this example, imagine you received an alert that the Umami frontend is not working. Time to troubleshoot and inspect the containers:

#show both running and stopped containers
docker ps -a
CONTAINER ID IMAGE PORTS CREATED STATUS
abc123456789 nginx 0.0.0.0:80->80/tcp, 0.0.0.0:443->443/tcp 2023-11-07 08:00:00 Up 2 hours
def987654321 umani_frontend 0.0.0.0:3000->3000/tcp 2023-11-07 09:00:00 Down 1 hour
xyz567890123 postgres_alpine 0.0.0.0:5432->5432/tcp 2023-11-07 10:00:00 Up 30 minutes

Docker containers allows you to assess if any of them are currently not running or down. In the Umani application, the infrastructure is comprised of three distinct components that make up the Umami application:

  1. Nginx: The Nginx container is hosting web services configured to listen on ports 80 and 443.

  2. Umani Frontend: Umani is the analytics frontend, serving as the user interface. It listens on port 3000.

  3. Postgres Alpine: This container hosts a PostgreSQL database. It’s configured to listen on port 5432.

Each of these containers serves a unique role within the infrastructure, with Nginx handling web traffic, Umani frontend providing the user interface, and Postgres managing the database, forming a typical setup for a web application.

In this example, Umani frontend is down and needs a restart:

#start up Umani frontend:
docker start umani_frontend
CONTAINER ID IMAGE PORTS CREATED STATUS
abc123456789 nginx 0.0.0.0:80->80/tcp, 0.0.0.0:443->443/tcp 2023-11-07 08:00:00 Up 2 hours
def987654321 umani_frontend 0.0.0.0:3000->3000/tcp 2023-11-07 09:00:00 Up 1 second
xyz567890123 postgres_alpine 0.0.0.0:5432->5432/tcp 2023-11-07 10:00:00 Up 30 minutes

Conclusion

Third-party to a first-party experience is a significant undertaking, but it’s one that can lead to compliance with evolving privacy regulations. By gradually replacing third-party dependencies with privacy-centric solutions, you can create a user-focused environment while retaining the data you need for analysis.

Remember that this journey is continuous, as privacy regulations evolve, and user expectations change. Stay committed to transparency, user education, and data protection, and you’ll be prepared for the privacy-centric future of the web.