import { Injectable, OnDestroy, inject } from '@angular/core'; import { Router, NavigationEnd } from '@angular/router'; import { isPlatformBrowser } from '@angular/common'; import { PLATFORM_ID } from '@angular/core'; import { filter, Subscription, skip } from 'rxjs'; import { OpenPanel } from '@openpanel/web'; import type { IdentifyPayload } from '@openpanel/web'; import { OpenPanelConfig, OPENPANEL_CONFIG } from '@core/models/openpanel.model'; export type TrackProperties = Record; @Injectable({ providedIn: 'root', }) export class OpenPanelService implements OnDestroy { private readonly config = inject(OPENPANEL_CONFIG); private readonly platformId = inject(PLATFORM_ID) private readonly router = inject(Router); private op?: OpenPanel; private routerSubscription?: Subscription; constructor() { if(isPlatformBrowser(this.platformId)) { this.initialize(); } } // ─── Initialization ──────────────────────────────────────────────────────── private initialize(): void { this.op = new OpenPanel({ clientId: this.config.clientId, apiUrl: this.config.apiUrl, trackScreenViews: false, // We handle this manually via Router trackOutgoingLinks: this.config.trackOutgoingLinks ?? false, trackAttributes: this.config.trackAttributes ?? false, disabled: this.config.disabled ?? false, }); if (this.config.globalProperties) { this.op.setGlobalProperties(this.config.globalProperties); } if (this.config.trackScreenViews !== false) { this.setupRouteTracking(); } } private setupRouteTracking(): void { this.routerSubscription?.unsubscribe(); this.routerSubscription = this.router.events.pipe( filter((event) => event instanceof NavigationEnd), ).subscribe(() => { const route = this.getActiveRoute(); const trackName = route.snapshot.data['trackName'] ?? this.router.url; this.trackScreenView(trackName); }); } private getActiveRoute() { let route = this.router.routerState.root; while (route.firstChild) route = route.firstChild; return route; } // ─── Public API ──────────────────────────────────────────────────────────── /** * Tracks a custom event with optional properties. * @example opService.track('button_clicked', { button_name: 'signup' }); */ track(eventName: string, properties?: TrackProperties): void { if (!this.op) return; if (this.config.debug) { console.debug('[OpenPanel] track:', eventName, properties); } this.op.track(eventName, properties); } /** * Identifies the current user. Call this after login. * @example opService.identify({ profileId: 'user-123', email: 'user@example.com' }); */ identify(payload: IdentifyPayload): void { if (!this.op) return; if (this.config.debug) { console.debug('[OpenPanel] identify:', payload.profileId); } this.op.identify(payload); } /** * Clears the current user identity. Call this on logout. */ clearUser(): void { if (!this.op) return; if (this.config.debug) { console.debug('[OpenPanel] clearUser'); } this.op.clear(); } /** * Sets properties that will be sent with every subsequent event. */ setGlobalProperties(properties: TrackProperties): void { if (!this.op) return; this.op.setGlobalProperties(properties); } /** * Increments a numeric property on the user profile. * @example opService.increment('login_count'); */ increment(property: string): void { if (!this.op) return; this.op.increment(property); } /** * Decrements a numeric property on the user profile. * @example opService.decrement('credits'); */ decrement(property: string): void { if (!this.op) return; this.op.decrement(property); } /** * Manually tracks a screen/page view. */ trackScreenView(path?: string): void { if (!this.op) return; const currentPath = path ?? this.router.url; if (this.config.debug) { console.debug('[OpenPanel] screenView:', currentPath); } this.op.track('screen_view', { path: currentPath }); } // ─── Cleanup ─────────────────────────────────────────────────────────────── ngOnDestroy(): void { this.routerSubscription?.unsubscribe(); } }