5 minutes
h1

Single Page Applications change how analytics works. This article outlines a scalable, data-layer-first approach to implementing Adobe Experience Platform Web SDK in SPAs, including page-view detection, route handling, deduplication, and state-based modeling.

Introduction

Single Page Applications (SPAs) fundamentally change how digital analytics operates. In these applications, traditional full-page reloads are replaced by client-side routing mechanisms, where the router dynamically updates the browser’s URL and renders new content without refreshing the entire page. As a result, navigation is managed internally by JavaScript-based routers, and analytics tools no longer receive the automatic page-load signals that typically accompany server-driven navigation.

Without a structured pattern, tracking quickly becomes fragmented across components, leading to duplicate hits, inconsistent page views, and unreliable reporting. A data layer-first approach solves this. By separating application logic from analytics logic, teams gain scalability, clarity, and governance.

SPA page view detection using a data layer

In a traditional multi-page application, a page view equals a full page reload. In an SPA, navigation happens through JavaScript route changes. That means you must intentionally define what constitutes a page view.

For an SPA, the router-based trigger should push structured events into the data layer.

Why this works:

How to implement route-based page views data layer calls

  1. Hook into your SPA routers "after navigation" event.

  2. Build your data layer page context dynamically (URL, title, template, etc.).

  3. Trigger the data layer push

Example conceptual pattern:

Default alt

History events vs. router lifecycle

One of the most common SPA tracking mistakes is relying solely on the browser History API (pushState, popstate).

Why history alone is risky

Why router hooks are best practice

The router should be the authoritative publisher of navigation events into the data layer. History listeners may be used only as fallback mechanisms.

Avoiding duplicate hits

Duplicate hits are a common complaint in SPA analytics. They typically happen for the following reasons:

The solution to this is centralized ownership and deduplication.

Duplicate events caused by re-renders or repeated pushes can be prevented by generating a stable key from the route and relevant state, with deduplication enforced in XDM through a consistent _id field.

AEP processes events on an at-least-once basis; this can result in retries of network replays occasionally producing duplicate events, even when the client fires only once. To prevent downstream duplication, events should include a stable ‘_id’ field at the XDM level. When present, AEP uses ‘_id’ for deduplication during ingestion. In SPAs, a deterministic ‘_id’ can be derived from:

Using the ‘_id’ field not only complements client-side suppression logic but also provides platform level protection against retries and duplicate ingestion.

State-based and route-based event modeling

SPAs allow analytics to move beyond simple URL-based tracking. You can model events based on application states.

Meaningful application state can include some of the following:

Identity handling in SPAs

SPAs introduce an important identity consideration. Unlike multi-page apps, the Adobe Experience Platform Web SDK manages the ECID in memory and cookies, and it persists across route changes.

This means that there is no need to re-fetch identity on every route change. The same ECID will automatically be included with subsequent events.

Repeated identity calls during SPA navigation come with many issues:

To avoid this, the recommended approach is to allow the Web SDK to manage ECID persistence automatically across SPA navigation.

Retrieving ECID when explicit identity population is required

On occasion, there will be a need to explicitly populate the ‘identityMap’, on these occasions ‘alloy("getIdentity")’ can be used to retrieve the ECID. Although this should be used only when explicitly needed and not on every route change.

Example of ‘getIdentity’ call:

Default alt

Separate page views from interactions

Page views should represent meaningful content transitions, whilst interactions should represent user-driven UI changes. This approach ensures clean segmentation and reliable reporting, resulting in stronger activation downstream.

Example of Interaction Push:

Default alt

Using ‘identityMap’ across SPA route changes

SPAs introduce an important identity consideration. While the Web SDK automatically persists the ECID across route changes, any additional identities (such as authenticated user IDs) are not automatically maintained unless explicitly included in subsequent events.

When a user authenticates, the application should update its state with the new identity and ensure that this identity is consistently passed in the identityMap for all relevant downstream events. This avoids inconsistencies and ensures proper stitching between anonymous and authenticated activity, without relying on repeated identity calls.

As a user authenticates, the application should push a single user_authenticated event containing the user identifier.

The Web SDK listener should then enrich this event by constructing the appropriate identityMap, combining the existing ECID with the authenticated ID. This ensures a clear separation of concerns, where the application signals state changes and the Web SDK layer handles identity stitching.

Default alt

Then (conceptually in your listener):

Default alt

Key takeaways

A data layer-first approach transforms SPA tracking from a fragile implementation detail into a scalable architecture pattern.