Angular Integration Example
This guide provides a complete, production-ready example of integrating Zenovay analytics into an Angular application using modern Angular patterns, services, and dependency injection.
Quick Start
Add the Zenovay tracking script to your index.html:
<!-- src/index.html -->
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>My Angular App</title>
</head>
<body>
<app-root></app-root>
<script defer data-tracking-code="YOUR_TRACKING_CODE" src="https://api.zenovay.com/z.js"></script>
</body>
</html>
That's it for basic page view tracking. The script automatically tracks page views on load.
Basic Setup
1. Create an Analytics Service
Create a service to wrap the global zenovay object with Angular dependency injection:
// services/analytics.service.ts
import { Injectable } from '@angular/core';
@Injectable({
providedIn: 'root'
})
export class AnalyticsService {
private get zenovay(): any {
return (window as any).zenovay;
}
track(eventName: string, data?: Record<string, any>): void {
if (this.zenovay) {
this.zenovay('track', eventName, data);
}
}
identify(userId: string, traits?: Record<string, any>): void {
if (this.zenovay) {
this.zenovay('identify', userId, traits);
}
}
trackGoal(goalName: string, data?: Record<string, any>): void {
if (this.zenovay) {
this.zenovay('goal', goalName, data);
}
}
trackPurchase(data: { amount: number; currency: string; product: string }): void {
if (this.zenovay) {
this.zenovay('revenue', data.amount, data.currency);
}
}
}
2. Track Page Views (Angular Router)
Create a service to track route changes:
// services/page-tracking.service.ts
import { Injectable } from '@angular/core';
import { Router, NavigationEnd } from '@angular/router';
import { AnalyticsService } from './analytics.service';
import { filter } from 'rxjs/operators';
@Injectable({
providedIn: 'root'
})
export class PageTrackingService {
constructor(
private router: Router,
private analytics: AnalyticsService
) {
this.initPageTracking();
}
private initPageTracking(): void {
this.router.events.pipe(
filter(event => event instanceof NavigationEnd)
).subscribe((event: NavigationEnd) => {
this.analytics.track('pageview', {
path: event.urlAfterRedirects,
referrer: document.referrer,
title: document.title,
});
});
}
}
Initialize in your app component:
// app.component.ts
import { Component, OnInit } from '@angular/core';
import { PageTrackingService } from './services/page-tracking.service';
@Component({
selector: 'app-root',
template: '<router-outlet></router-outlet>',
})
export class AppComponent implements OnInit {
constructor(private pageTracking: PageTrackingService) {}
ngOnInit(): void {
// Service automatically starts tracking on init
}
}
3. Track Custom Events
Use the analytics service in your components:
// components/signup-button/signup-button.component.ts
import { Component } from '@angular/core';
import { AnalyticsService } from '../../services/analytics.service';
@Component({
selector: 'app-signup-button',
template: `
<button (click)="handleSignup()">
Start Free Trial
</button>
`
})
export class SignupButtonComponent {
constructor(private analytics: AnalyticsService) {}
handleSignup(): void {
this.analytics.track('signup_started', {
plan: 'professional',
source: 'pricing_page'
});
// Continue with signup logic
}
}
Complete Example
Here's a full implementation with all common use cases:
Analytics Service with Configuration
// services/zenovay-analytics.service.ts
import { Injectable } from '@angular/core';
import { environment } from '../environments/environment';
@Injectable({
providedIn: 'root'
})
export class ZenovayAnalyticsService {
private get zenovay(): any {
return (window as any).zenovay;
}
// Page tracking
trackPageView(path: string, title?: string): void {
if (this.zenovay) {
this.zenovay('track', 'pageview', {
path,
title: title || document.title,
referrer: document.referrer,
});
}
}
// Custom events
trackEvent(eventName: string, data?: Record<string, any>): void {
if (this.zenovay) {
this.zenovay('track', eventName, {
...data,
timestamp: new Date().toISOString(),
environment: environment.production ? 'production' : 'development',
});
}
}
// User identification
identifyUser(userId: string, traits?: Record<string, any>): void {
if (this.zenovay) {
this.zenovay('identify', userId, traits);
}
}
// Debug mode
enableDebug(): void {
if (!environment.production) {
console.log('[Zenovay] Debug mode enabled');
}
}
}
Trackable Button Directive
// directives/trackable-button.directive.ts
import { Directive, HostListener, Input } from '@angular/core';
import { AnalyticsService } from '../services/analytics.service';
@Directive({
selector: '[appTrackableButton]',
standalone: true
})
export class TrackableButtonDirective {
@Input() eventName!: string;
@Input() eventData?: Record<string, any>;
constructor(private analytics: AnalyticsService) {}
@HostListener('click', ['$event'])
onClick(event: MouseEvent): void {
if (!this.eventName) {
console.warn('[TrackableButton] No event name provided');
return;
}
this.analytics.track(this.eventName, {
...this.eventData,
button_text: (event.target as HTMLElement).textContent,
timestamp: Date.now(),
});
}
}
Usage Example
// pages/pricing/pricing.component.ts
import { Component } from '@angular/core';
import { CommonModule } from '@angular/common';
import { TrackableButtonDirective } from '../../directives/trackable-button.directive';
@Component({
selector: 'app-pricing',
standalone: true,
imports: [CommonModule, TrackableButtonDirective],
template: `
<div class="pricing-page">
<h1>Pricing</h1>
<button
appTrackableButton
[eventName]="'plan_selected'"
[eventData]="{
plan: 'scale',
billing: 'monthly',
price: 90
}"
class="btn-primary">
Choose Scale
</button>
</div>
`
})
export class PricingComponent {}
Advanced Patterns
Conditional Tracking
Track events only when certain conditions are met:
// pages/product/product.component.ts
import { Component, OnInit, Input } from '@angular/core';
import { AnalyticsService } from '../../services/analytics.service';
interface Product {
id: string;
name: string;
price: number;
category: string;
}
@Component({
selector: 'app-product',
template: `<div>{{ product.name }}</div>`
})
export class ProductComponent implements OnInit {
@Input() product!: Product;
constructor(private analytics: AnalyticsService) {}
ngOnInit(): void {
// Only track if product is high-value
if (this.product.price > 1000) {
this.analytics.track('high_value_product_viewed', {
product_id: this.product.id,
product_name: this.product.name,
price: this.product.price,
category: this.product.category,
});
}
}
}
Form Tracking
Track form interactions and submissions:
// components/contact-form/contact-form.component.ts
import { Component } from '@angular/core';
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
import { AnalyticsService } from '../../services/analytics.service';
@Component({
selector: 'app-contact-form',
template: `
<form [formGroup]="contactForm" (ngSubmit)="onSubmit()">
<input
type="text"
formControlName="name"
(focus)="onFieldFocus('name')"
placeholder="Name"
/>
<input
type="email"
formControlName="email"
(focus)="onFieldFocus('email')"
placeholder="Email"
/>
<textarea
formControlName="message"
(focus)="onFieldFocus('message')"
placeholder="Message"
></textarea>
<button type="submit">Send Message</button>
</form>
`
})
export class ContactFormComponent {
contactForm: FormGroup;
constructor(
private fb: FormBuilder,
private analytics: AnalyticsService
) {
this.contactForm = this.fb.group({
name: ['', Validators.required],
email: ['', [Validators.required, Validators.email]],
message: ['', Validators.required],
});
}
onFieldFocus(fieldName: string): void {
this.analytics.track('form_field_focused', {
form_id: 'contact',
field_name: fieldName,
});
}
async onSubmit(): Promise<void> {
if (this.contactForm.invalid) {
return;
}
const formData = this.contactForm.value;
// Track form submission
this.analytics.track('contact_form_submitted', {
form_id: 'contact',
has_message: formData.message.length > 0,
});
// Submit form logic
}
}
E-commerce Tracking
Track product views, add to cart, and purchases:
// services/ecommerce-tracking.service.ts
import { Injectable } from '@angular/core';
import { AnalyticsService } from './analytics.service';
interface Product {
id: string;
name: string;
category: string;
price: number;
}
interface CartItem extends Product {
quantity: number;
}
interface Cart {
items: CartItem[];
total: number;
}
interface Order extends Cart {
id: string;
tax: number;
shipping: number;
}
@Injectable({
providedIn: 'root'
})
export class EcommerceTrackingService {
constructor(private analytics: AnalyticsService) {}
trackProductView(product: Product): void {
this.analytics.track('product_viewed', {
product_id: product.id,
product_name: product.name,
category: product.category,
price: product.price,
currency: 'USD',
});
}
trackAddToCart(product: Product, quantity: number = 1): void {
this.analytics.track('product_added_to_cart', {
product_id: product.id,
product_name: product.name,
quantity,
price: product.price,
total: product.price * quantity,
});
}
trackCheckout(cart: Cart): void {
this.analytics.track('checkout_started', {
cart_total: cart.total,
item_count: cart.items.length,
currency: 'USD',
});
}
trackPurchase(order: Order): void {
this.analytics.trackPurchase({
amount: order.total,
currency: 'USD',
product: `Order #${order.id}`,
});
this.analytics.track('purchase_completed', {
order_id: order.id,
revenue: order.total,
tax: order.tax,
shipping: order.shipping,
currency: 'USD',
items: order.items.map(item => ({
product_id: item.id,
quantity: item.quantity,
price: item.price,
})),
});
}
}
Usage in Component
// pages/product-detail/product-detail.component.ts
import { Component, OnInit } from '@angular/core';
import { ActivatedRoute } from '@angular/router';
import { EcommerceTrackingService } from '../../services/ecommerce-tracking.service';
@Component({
selector: 'app-product-detail',
template: `
<div *ngIf="product">
<h1>{{ product.name }}</h1>
<p>\${{ product.price }}</p>
<button (click)="addToCart()">Add to Cart</button>
</div>
`
})
export class ProductDetailComponent implements OnInit {
product: any;
constructor(
private route: ActivatedRoute,
private ecommerceTracking: EcommerceTrackingService
) {}
ngOnInit(): void {
// Load product and track view
this.route.params.subscribe(params => {
// Fetch product by params.id
this.product = {
id: params['id'],
name: 'Example Product',
price: 99.99,
category: 'Electronics',
};
this.ecommerceTracking.trackProductView(this.product);
});
}
addToCart(): void {
this.ecommerceTracking.trackAddToCart(this.product);
// Add to cart logic
}
}
Error Tracking with ErrorHandler
Track errors automatically:
// services/global-error-handler.service.ts
import { ErrorHandler, Injectable } from '@angular/core';
@Injectable()
export class GlobalErrorHandler implements ErrorHandler {
handleError(error: Error): void {
// Track error via global zenovay object
if ((window as any).zenovay) {
(window as any).zenovay('track', 'error_occurred', {
error_message: error.message,
error_stack: error.stack,
error_name: error.name,
url: window.location.href,
user_agent: navigator.userAgent,
});
}
// Log to console
console.error('Error caught by GlobalErrorHandler:', error);
}
}
Register in app config:
// app.config.ts
import { ApplicationConfig, ErrorHandler } from '@angular/core';
import { GlobalErrorHandler } from './services/global-error-handler.service';
export const appConfig: ApplicationConfig = {
providers: [
{ provide: ErrorHandler, useClass: GlobalErrorHandler },
// other providers
]
};
User Identification
Track identified users:
// services/user-tracking.service.ts
import { Injectable } from '@angular/core';
import { AnalyticsService } from './analytics.service';
import { AuthService } from './auth.service';
@Injectable({
providedIn: 'root'
})
export class UserTrackingService {
constructor(
private analytics: AnalyticsService,
private auth: AuthService
) {
this.initUserTracking();
}
private initUserTracking(): void {
this.auth.user$.subscribe(user => {
if (user) {
this.analytics.identify(user.id, {
email: user.email,
name: user.name,
plan: user.subscription?.plan,
signup_date: user.createdAt,
});
}
});
}
}
Initialize in app component:
// app.component.ts
import { Component } from '@angular/core';
import { UserTrackingService } from './services/user-tracking.service';
@Component({
selector: 'app-root',
template: '<router-outlet></router-outlet>',
})
export class AppComponent {
constructor(private userTracking: UserTrackingService) {
// Service automatically starts tracking on init
}
}
TypeScript Support
Full TypeScript support with typed events:
// types/analytics.types.ts
export interface AnalyticsEvents {
signup_started: {
plan: 'free' | 'pro' | 'scale' | 'enterprise';
source: string;
};
product_viewed: {
product_id: string;
product_name: string;
price: number;
};
purchase_completed: {
order_id: string;
revenue: number;
currency: string;
};
}
// Declare the global zenovay function for TypeScript
declare global {
interface Window {
zenovay: (...args: any[]) => void;
}
}
// Typed tracking service
import { Injectable } from '@angular/core';
import { AnalyticsEvents } from '../types/analytics.types';
@Injectable({
providedIn: 'root'
})
export class TypedAnalyticsService {
private get zenovay(): any {
return (window as any).zenovay;
}
track<K extends keyof AnalyticsEvents>(
eventName: K,
data: AnalyticsEvents[K]
): void {
if (this.zenovay) {
this.zenovay('track', eventName, data);
}
}
}
RxJS Integration
Leverage Angular's reactive patterns:
// services/analytics-rxjs.service.ts
import { Injectable } from '@angular/core';
import { AnalyticsService } from './analytics.service';
import { Observable, of } from 'rxjs';
import { tap, catchError } from 'rxjs/operators';
@Injectable({
providedIn: 'root'
})
export class AnalyticsRxjsService {
constructor(private analytics: AnalyticsService) {}
trackEvent$(eventName: string, data?: Record<string, any>): Observable<void> {
return of(this.analytics.track(eventName, data)).pipe(
tap(() => console.log(`[Analytics] Tracked: ${eventName}`)),
catchError(error => {
console.error('[Analytics] Error:', error);
throw error;
})
);
}
}
Performance Optimization
Debounce Tracking
Prevent excessive tracking:
// components/search-box/search-box.component.ts
import { Component } from '@angular/core';
import { FormControl } from '@angular/forms';
import { debounceTime, distinctUntilChanged } from 'rxjs/operators';
import { AnalyticsService } from '../../services/analytics.service';
@Component({
selector: 'app-search-box',
template: `
<input
type="search"
[formControl]="searchControl"
placeholder="Search..."
/>
`
})
export class SearchBoxComponent {
searchControl = new FormControl('');
constructor(private analytics: AnalyticsService) {
this.searchControl.valueChanges.pipe(
debounceTime(500),
distinctUntilChanged()
).subscribe(query => {
if (query) {
this.analytics.track('search_performed', {
query,
length: query.length,
});
}
});
}
}
Testing
Mock Analytics Service in Tests
// testing/analytics.mock.ts
import { Injectable } from '@angular/core';
@Injectable()
export class MockAnalyticsService {
track = jasmine.createSpy('track');
identify = jasmine.createSpy('identify');
trackGoal = jasmine.createSpy('trackGoal');
trackPurchase = jasmine.createSpy('trackPurchase');
}
Test Component
// components/signup-button/signup-button.component.spec.ts
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { SignupButtonComponent } from './signup-button.component';
import { AnalyticsService } from '../../services/analytics.service';
import { MockAnalyticsService } from '../../testing/analytics.mock';
describe('SignupButtonComponent', () => {
let component: SignupButtonComponent;
let fixture: ComponentFixture<SignupButtonComponent>;
let analyticsService: MockAnalyticsService;
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [SignupButtonComponent],
providers: [
{ provide: AnalyticsService, useClass: MockAnalyticsService }
]
}).compileComponents();
fixture = TestBed.createComponent(SignupButtonComponent);
component = fixture.componentInstance;
analyticsService = TestBed.inject(AnalyticsService) as any;
fixture.detectChanges();
});
it('should track event on click', async () => {
const button = fixture.nativeElement.querySelector('button');
button.click();
await fixture.whenStable();
expect(analyticsService.track).toHaveBeenCalledWith('signup_started', {
plan: 'professional',
source: 'pricing_page'
});
});
});
Standalone Components (Angular 17+)
Complete integration with standalone components:
// app.component.ts
import { Component } from '@angular/core';
import { RouterOutlet } from '@angular/router';
@Component({
selector: 'app-root',
standalone: true,
imports: [RouterOutlet],
template: '<router-outlet></router-outlet>',
})
export class AppComponent {}
No special Angular module configuration is needed. The Zenovay tracking script in index.html handles everything. Your AnalyticsService simply wraps window.zenovay calls and can be injected into any component.
Environment Configuration
// environments/environment.ts
export const environment = {
production: false,
};
// environments/environment.prod.ts
export const environment = {
production: true,
};
The tracking code is configured directly in the index.html script tag. For different environments, you can use Angular's file replacement feature to swap index.html files, or simply use the same tracking code across environments (Zenovay handles environment separation in the dashboard).