Analytics Architecture on SAP Commerce Cloud
SAP Commerce Cloud (formerly Hybris) uses a Java-based backend with a decoupled Angular storefront called Spartacus (now SAP Composable Storefront). The backend exposes data through the OCC (Omni Commerce Connect) REST API, and Spartacus renders the storefront as a single-page application.
The rendering pipeline:
Browser → Spartacus (Angular SPA) → OCC API → SAP Commerce Backend → Database
↓
SSR (optional via Express/Node.js)
In the legacy JSP-based storefront (Accelerator), analytics scripts are injected via JSP templates and CMS components. In the modern Spartacus architecture, all analytics implementation happens in Angular modules and services.
SmartEdit is the WYSIWYG editor overlay for content managers. It runs in an iframe around the storefront and can interfere with analytics script execution.
Installing Tracking Scripts
Method 1: Spartacus Angular Module
Create a dedicated analytics module in the Spartacus project:
// src/app/analytics/analytics.module.ts
import { NgModule, APP_INITIALIZER } from '@angular/core';
import { AnalyticsService } from './analytics.service';
export function initAnalytics(analyticsService: AnalyticsService) {
return () => analyticsService.initialize();
}
@NgModule({
providers: [
AnalyticsService,
{
provide: APP_INITIALIZER,
useFactory: initAnalytics,
deps: [AnalyticsService],
multi: true
}
]
})
export class AnalyticsModule {}
// src/app/analytics/analytics.service.ts
import { Injectable, Inject, PLATFORM_ID } from '@angular/core';
import { isPlatformBrowser } from '@angular/common';
import { Router, NavigationEnd } from '@angular/router';
import { filter } from 'rxjs/operators';
@Injectable({ providedIn: 'root' })
export class AnalyticsService {
constructor(
private router: Router,
@Inject(PLATFORM_ID) private platformId: Object
) {}
initialize(): void {
if (!isPlatformBrowser(this.platformId)) return;
// Load GTM
const script = document.createElement('script');
script.async = true;
script.src = 'https://www.googletagmanager.com/gtm.js?id=GTM-XXXXX';
document.head.appendChild(script);
window['dataLayer'] = window['dataLayer'] || [];
window['dataLayer'].push({
'gtm.start': new Date().getTime(),
event: 'gtm.js'
});
// Track SPA navigation
this.router.events.pipe(
filter(event => event instanceof NavigationEnd)
).subscribe((event: NavigationEnd) => {
window['dataLayer'].push({
event: 'virtual_pageview',
pagePath: event.urlAfterRedirects,
pageTitle: document.title
});
});
}
}
Method 2: CMS Component (Backend-Driven)
Create a CMS component in SAP Commerce that editors can place via SmartEdit:
// AnalyticsScriptComponent.java (backend)
public class AnalyticsScriptComponentModel extends SimpleCMSComponentModel {
private String trackingId;
private String scriptUrl;
private boolean enabled;
// getters and setters
}
Register a Spartacus CMS mapping:
// analytics-cms.component.ts
import { Component, ChangeDetectionStrategy } from '@angular/core';
import { CmsComponentData } from '@spartacus/storefront';
@Component({
selector: 'app-analytics-script',
template: '',
changeDetection: ChangeDetectionStrategy.OnPush
})
export class AnalyticsCmsComponent {
constructor(private component: CmsComponentData<any>) {
this.component.data$.subscribe(data => {
if (data.enabled && data.trackingId) {
this.loadScript(data.scriptUrl, data.trackingId);
}
});
}
private loadScript(url: string, id: string): void {
const script = document.createElement('script');
script.async = true;
script.src = `${url}?id=${id}`;
document.head.appendChild(script);
}
}
Register in module:
provideDefaultConfig({
cmsComponents: {
AnalyticsScriptComponent: {
component: AnalyticsCmsComponent
}
}
});
Method 3: Legacy Accelerator (JSP)
For the older JSP-based storefront:
<%-- /WEB-INF/tags/responsive/template/master.tag --%>
<head>
<script>
window.dataLayer = window.dataLayer || [];
</script>
<script async src="https://www.googletagmanager.com/gtm.js?id=${gtmId}"></script>
</head>
The gtmId can be set via a site-level configuration property in the SAP Commerce backoffice.
Data Layer Implementation
Spartacus Data Layer Service
Build a service that collects commerce data from the OCC API and populates the data layer:
// src/app/analytics/data-layer.service.ts
import { Injectable } from '@angular/core';
import { ActiveCartFacade } from '@spartacus/cart/base/root';
import { CurrentProductService } from '@spartacus/storefront';
import { RoutingService } from '@spartacus/core';
@Injectable({ providedIn: 'root' })
export class DataLayerService {
constructor(
private cart: ActiveCartFacade,
private currentProduct: CurrentProductService,
private routing: RoutingService
) {}
pushPageData(): void {
this.routing.getRouterState().subscribe(state => {
const ctx = state.state.context;
window['dataLayer'] = window['dataLayer'] || [];
window['dataLayer'].push({
event: 'page_data',
platform: 'sap_commerce_cloud',
pageType: ctx?.type || 'unknown',
pageId: ctx?.id || ''
});
});
}
pushProductData(): void {
this.currentProduct.getProduct().subscribe(product => {
if (!product) return;
window['dataLayer'].push({
event: 'product_view',
product: {
id: product.code,
name: product.name,
price: product.price?.value,
currency: product.price?.currencyIso,
category: product.categories?.[0]?.name || '',
brand: product.manufacturer || '',
inStock: product.stock?.stockLevelStatus === 'inStock'
}
});
});
}
pushCartData(): void {
this.cart.getActive().subscribe(cart => {
if (!cart?.code) return;
const items = (cart.entries || []).map(entry => ({
id: entry.product?.code,
name: entry.product?.name,
price: entry.basePrice?.value,
quantity: entry.quantity
}));
window['dataLayer'].push({
event: 'cart_update',
cart: {
id: cart.code,
total: cart.totalPrice?.value,
currency: cart.totalPrice?.currencyIso,
itemCount: cart.totalItems,
items
}
});
});
}
}
OCC API Direct Data Layer
For server-side rendering (SSR) or when you need data before Angular bootstraps, fetch from the OCC API:
// In SSR context or app initializer
async function buildDataLayer(occBaseUrl: string, productCode: string) {
const response = await fetch(
`${occBaseUrl}/occ/v2/mysite/products/${productCode}?fields=FULL`
);
const product = await response.json();
return {
platform: 'sap_commerce_cloud',
product: {
id: product.code,
name: product.name,
price: product.price?.value,
currency: product.price?.currencyIso,
categories: product.categories?.map(c => c.name)
}
};
}
E-commerce Tracking
Purchase Event
Hook into the order confirmation flow in Spartacus:
// order-confirmation-analytics.component.ts
import { Component, OnInit } from '@angular/core';
import { OrderFacade } from '@spartacus/order/root';
@Component({
selector: 'app-order-analytics',
template: ''
})
export class OrderConfirmationAnalyticsComponent implements OnInit {
constructor(private orderFacade: OrderFacade) {}
ngOnInit(): void {
this.orderFacade.getOrderDetails().subscribe(order => {
if (!order?.code) return;
window['dataLayer'].push({
event: 'purchase',
transaction: {
id: order.code,
revenue: order.totalPrice?.value,
tax: order.totalTax?.value,
shipping: order.deliveryCost?.value,
currency: order.totalPrice?.currencyIso
},
items: (order.entries || []).map(entry => ({
id: entry.product?.code,
name: entry.product?.name,
price: entry.basePrice?.value,
quantity: entry.quantity,
category: entry.product?.categories?.[0]?.name || ''
}))
});
});
}
}
Add to Cart
Intercept the add-to-cart action:
// Extend the ActiveCartFacade or use an event listener
this.eventService.get(CartAddEntrySuccessEvent).subscribe(event => {
window['dataLayer'].push({
event: 'add_to_cart',
product: {
id: event.productCode,
quantity: event.quantity
}
});
});
Common Issues
SmartEdit Iframe Breaking Analytics
SmartEdit loads the storefront in an iframe with additional query parameters (cmsTicketId, smartedit-preview). Analytics scripts fire inside the iframe, sending false pageviews.
Detect and suppress:
initialize(): void {
if (!isPlatformBrowser(this.platformId)) return;
// Skip analytics in SmartEdit
const params = new URLSearchParams(window.location.search);
if (params.has('cmsTicketId') || window.location.href.includes('smartedit')) {
return;
}
// ... load analytics
}
SPA Navigation Not Tracked
Spartacus is a single-page application. The initial page load fires a standard pageview, but subsequent navigations do not trigger new page loads. You must listen to Angular Router events (see the NavigationEnd subscription above) and push virtual pageview events.
SSR Hydration Duplicating Events
If using server-side rendering with Angular Universal, the analytics initialization runs on the server (where document does not exist) and again on the client during hydration. The isPlatformBrowser check prevents server-side execution, but ensure you do not double-push events during client hydration:
private initialized = false;
initialize(): void {
if (this.initialized || !isPlatformBrowser(this.platformId)) return;
this.initialized = true;
// ... load analytics
}
OCC API CORS Issues for Client-Side Fetches
If you fetch OCC API endpoints directly from the browser for data layer enrichment, you may hit CORS restrictions. Configure the SAP Commerce backend to allow the storefront origin:
# local.properties or project.properties
corsfilter.commercewebservices.allowedOrigins=https://storefront.example.com
corsfilter.commercewebservices.allowedMethods=GET
Platform-Specific Considerations
Composable Storefront (Spartacus) vs. Accelerator -- The modern Spartacus storefront is an Angular SPA. All analytics logic lives in TypeScript services and modules. The legacy Accelerator uses server-rendered JSP pages where you inject scripts via tag files. Do not mix patterns -- choose one approach based on your storefront.
CMS-driven script injection -- SAP Commerce's CMS system allows content managers to create and place components via SmartEdit. While this provides flexibility, it also means analytics scripts can be removed or misconfigured by non-technical users. For critical tracking (GTM container, data layer initialization), use the Angular module approach rather than CMS components.
Multi-site and multi-country -- SAP Commerce supports multiple base stores with different catalogs, currencies, and languages. Each base store can have its own tracking configuration. Use site configuration properties to store per-store GTM IDs and ensure the data layer includes the current base store identifier for segmentation.
Tag management via SAP Commerce -- SAP Commerce has a built-in tag management framework (de.hybris.platform.addonsupport with tag libraries). While functional, it is less flexible than a dedicated TMS. Most implementations use GTM or Adobe Launch instead, injected via the methods described above.