Last active 4 days ago

Integration von OpenPanel als Analytics für Angularprojekte

environment.ts Raw
1export const environment = {
2 production: false,
3 openPanel: {
4 clientId: 'OPENPANEL_CLIENT_ID',
5 apiUrl: 'https://worker.domain.de', // mapping zu Worker Container, apiURL nur bei selfhost notwendig
6 }
7};
openpanel.directive.ts Raw
1import { Directive, HostListener, Input, inject } from '@angular/core';
2import { OpenPanelService, TrackProperties } from '@core/services/openpanel.service';
3
4/**
5 * Directive for declarative event tracking directly in templates.
6 *
7 * @example
8 * <button opTrack="signup_clicked" [opTrackProps]="{ location: 'hero' }">
9 * Sign Up
10 * </button>
11 *
12 * @example
13 * <a routerLink="/pricing" opTrack="pricing_link_clicked">Pricing</a>
14 */
15@Directive({
16 selector: '[opTrack]',
17 standalone: true,
18})
19export class OpenPanelTrackDirective {
20 private readonly op = inject(OpenPanelService);
21
22 /** The event name to track on click. */
23 @Input({ required: true }) opTrack!: string;
24
25 /** Optional properties to send with the event. */
26 @Input() opTrackProps?: TrackProperties;
27
28 /** Which DOM event triggers tracking. Default: 'click' */
29 @Input() opTrackOn: 'click' | 'mouseenter' | 'focus' | 'blur' = 'click';
30
31 @HostListener('click')
32 onClick(): void {
33 if (this.opTrackOn === 'click') {
34 this.op.track(this.opTrack, this.opTrackProps);
35 }
36 }
37
38 @HostListener('mouseenter')
39 onMouseEnter(): void {
40 if (this.opTrackOn === 'mouseenter') {
41 this.op.track(this.opTrack, this.opTrackProps);
42 }
43 }
44
45 @HostListener('focus')
46 onFocus(): void {
47 if (this.opTrackOn === 'focus') {
48 this.op.track(this.opTrack, this.opTrackProps);
49 }
50 }
51
52 @HostListener('blur')
53 onBlur(): void {
54 if (this.opTrackOn === 'blur') {
55 this.op.track(this.opTrack, this.opTrackProps);
56 }
57 }
58}
openpanel.model.ts Raw
1import { InjectionToken } from '@angular/core';
2
3export interface OpenPanelConfig {
4 /** Your OpenPanel Client ID (required) */
5 clientId: string;
6
7 /** URL of your OpenPanel API or self-hosted instance.
8 * Defaults to https://api.openpanel.dev */
9 apiUrl?: string;
10
11 /** Automatically track Angular Router navigation events as screen views.
12 * Default: true */
13 trackScreenViews?: boolean;
14
15 /** Track clicks on outgoing links automatically.
16 * Default: false */
17 trackOutgoingLinks?: boolean;
18
19 /** Enable declarative tracking via data-track HTML attributes.
20 * Default: false */
21 trackAttributes?: boolean;
22
23 /** Global properties sent with every event (e.g. app_version, environment). */
24 globalProperties?: Record<string, string | number | boolean>;
25
26 /** Completely disable all tracking (e.g. in test environments).
27 * Default: false */
28 disabled?: boolean;
29
30 /** Enable verbose console logging for debugging.
31 * Default: false */
32 debug?: boolean;
33}
34
35export const OPENPANEL_CONFIG = new InjectionToken<OpenPanelConfig>('OPENPANEL_CONFIG');
openpanel.service.ts Raw
1import { Injectable, OnDestroy, inject } from '@angular/core';
2import { Router, NavigationEnd } from '@angular/router';
3import { isPlatformBrowser } from '@angular/common';
4import { PLATFORM_ID } from '@angular/core';
5import { filter, Subscription, skip } from 'rxjs';
6import { OpenPanel } from '@openpanel/web';
7import type { IdentifyPayload } from '@openpanel/web';
8import { OpenPanelConfig, OPENPANEL_CONFIG } from '@core/models/openpanel.model';
9
10export type TrackProperties = Record<string, string | number | boolean | null | undefined>;
11
12@Injectable({
13 providedIn: 'root',
14})
15export class OpenPanelService implements OnDestroy {
16 private readonly config = inject(OPENPANEL_CONFIG);
17 private readonly platformId = inject(PLATFORM_ID)
18 private readonly router = inject(Router);
19
20 private op?: OpenPanel;
21 private routerSubscription?: Subscription;
22
23 constructor() {
24 if(isPlatformBrowser(this.platformId)) {
25 this.initialize();
26 }
27 }
28
29 // ─── Initialization ────────────────────────────────────────────────────────
30
31 private initialize(): void {
32 this.op = new OpenPanel({
33 clientId: this.config.clientId,
34 apiUrl: this.config.apiUrl,
35 trackScreenViews: false, // We handle this manually via Router
36 trackOutgoingLinks: this.config.trackOutgoingLinks ?? false,
37 trackAttributes: this.config.trackAttributes ?? false,
38 disabled: this.config.disabled ?? false,
39 });
40
41 if (this.config.globalProperties) {
42 this.op.setGlobalProperties(this.config.globalProperties);
43 }
44
45 if (this.config.trackScreenViews !== false) {
46 this.setupRouteTracking();
47 }
48 }
49
50 private setupRouteTracking(): void {
51 this.routerSubscription?.unsubscribe();
52
53 this.routerSubscription = this.router.events.pipe(
54 filter((event) => event instanceof NavigationEnd),
55 ).subscribe(() => {
56 const route = this.getActiveRoute();
57 const trackName = route.snapshot.data['trackName'] ?? this.router.url;
58 this.trackScreenView(trackName);
59 });
60 }
61
62 private getActiveRoute() {
63 let route = this.router.routerState.root;
64 while (route.firstChild) route = route.firstChild;
65 return route;
66 }
67
68 // ─── Public API ────────────────────────────────────────────────────────────
69
70 /**
71 * Tracks a custom event with optional properties.
72 * @example opService.track('button_clicked', { button_name: 'signup' });
73 */
74 track(eventName: string, properties?: TrackProperties): void {
75 if (!this.op) return;
76 if (this.config.debug) {
77 console.debug('[OpenPanel] track:', eventName, properties);
78 }
79 this.op.track(eventName, properties);
80 }
81
82 /**
83 * Identifies the current user. Call this after login.
84 * @example opService.identify({ profileId: 'user-123', email: 'user@example.com' });
85 */
86 identify(payload: IdentifyPayload): void {
87 if (!this.op) return;
88 if (this.config.debug) {
89 console.debug('[OpenPanel] identify:', payload.profileId);
90 }
91 this.op.identify(payload);
92 }
93
94 /**
95 * Clears the current user identity. Call this on logout.
96 */
97 clearUser(): void {
98 if (!this.op) return;
99 if (this.config.debug) {
100 console.debug('[OpenPanel] clearUser');
101 }
102 this.op.clear();
103 }
104
105 /**
106 * Sets properties that will be sent with every subsequent event.
107 */
108 setGlobalProperties(properties: TrackProperties): void {
109 if (!this.op) return;
110 this.op.setGlobalProperties(properties);
111 }
112
113 /**
114 * Increments a numeric property on the user profile.
115 * @example opService.increment('login_count');
116 */
117 increment(property: string): void {
118 if (!this.op) return;
119 this.op.increment(property);
120 }
121
122
123
124 /**
125 * Decrements a numeric property on the user profile.
126 * @example opService.decrement('credits');
127 */
128 decrement(property: string): void {
129 if (!this.op) return;
130 this.op.decrement(property);
131 }
132
133 /**
134 * Manually tracks a screen/page view.
135 */
136 trackScreenView(path?: string): void {
137 if (!this.op) return;
138 const currentPath = path ?? this.router.url;
139 if (this.config.debug) {
140 console.debug('[OpenPanel] screenView:', currentPath);
141 }
142 this.op.track('screen_view', { path: currentPath });
143 }
144
145 // ─── Cleanup ───────────────────────────────────────────────────────────────
146
147 ngOnDestroy(): void {
148 this.routerSubscription?.unsubscribe();
149 }
150}