Last active 4 days ago

Integration von OpenPanel als Analytics für Angularprojekte

mathias revised this gist 4 days ago. Go to revision

No changes

mathias revised this gist 6 days ago. Go to revision

4 files changed, 250 insertions

environment.ts(file created)

@@ -0,0 +1,7 @@
1 + export 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(file created)

@@ -0,0 +1,58 @@
1 + import { Directive, HostListener, Input, inject } from '@angular/core';
2 + import { 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 + })
19 + export 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(file created)

@@ -0,0 +1,35 @@
1 + import { InjectionToken } from '@angular/core';
2 +
3 + export 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 +
35 + export const OPENPANEL_CONFIG = new InjectionToken<OpenPanelConfig>('OPENPANEL_CONFIG');

openpanel.service.ts(file created)

@@ -0,0 +1,150 @@
1 + import { Injectable, OnDestroy, inject } from '@angular/core';
2 + import { Router, NavigationEnd } from '@angular/router';
3 + import { isPlatformBrowser } from '@angular/common';
4 + import { PLATFORM_ID } from '@angular/core';
5 + import { filter, Subscription, skip } from 'rxjs';
6 + import { OpenPanel } from '@openpanel/web';
7 + import type { IdentifyPayload } from '@openpanel/web';
8 + import { OpenPanelConfig, OPENPANEL_CONFIG } from '@core/models/openpanel.model';
9 +
10 + export type TrackProperties = Record<string, string | number | boolean | null | undefined>;
11 +
12 + @Injectable({
13 + providedIn: 'root',
14 + })
15 + export 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 + }
Newer Older