openpanel service hinzugefügt
This commit is contained in:
@@ -1,6 +1,8 @@
|
|||||||
import { ApplicationConfig, provideBrowserGlobalErrorListeners } from '@angular/core';
|
import { ApplicationConfig, provideBrowserGlobalErrorListeners } from '@angular/core';
|
||||||
import { provideRouter } from '@angular/router';
|
import { provideRouter } from '@angular/router';
|
||||||
import { provideNgIconsConfig } from '@ng-icons/core';
|
import { provideNgIconsConfig } from '@ng-icons/core';
|
||||||
|
import { environment } from '../environments/openpanel';
|
||||||
|
import { provideOpenPanel } from '@core/provider/openpanel.provider';
|
||||||
|
|
||||||
import { routes } from './app.routes';
|
import { routes } from './app.routes';
|
||||||
import { provideClientHydration, withEventReplay } from '@angular/platform-browser';
|
import { provideClientHydration, withEventReplay } from '@angular/platform-browser';
|
||||||
@@ -10,6 +12,12 @@ export const appConfig: ApplicationConfig = {
|
|||||||
provideBrowserGlobalErrorListeners(),
|
provideBrowserGlobalErrorListeners(),
|
||||||
provideRouter(routes),
|
provideRouter(routes),
|
||||||
provideClientHydration(withEventReplay()),
|
provideClientHydration(withEventReplay()),
|
||||||
provideNgIconsConfig({})
|
provideNgIconsConfig({}),
|
||||||
|
provideOpenPanel({
|
||||||
|
clientId: environment.openPanel.clientId,
|
||||||
|
apiUrl: environment.openPanel.apiUrl,
|
||||||
|
trackScreenViews: true,
|
||||||
|
debug: !environment.production,
|
||||||
|
})
|
||||||
]
|
]
|
||||||
};
|
};
|
||||||
|
|||||||
58
src/app/core/directives/openpanel.directive.ts
Normal file
58
src/app/core/directives/openpanel.directive.ts
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
import { Directive, HostListener, Input, inject } from '@angular/core';
|
||||||
|
import { OpenPanelService, TrackProperties } from '@core/services/openpanel.service';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Directive for declarative event tracking directly in templates.
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* <button opTrack="signup_clicked" [opTrackProps]="{ location: 'hero' }">
|
||||||
|
* Sign Up
|
||||||
|
* </button>
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* <a routerLink="/pricing" opTrack="pricing_link_clicked">Pricing</a>
|
||||||
|
*/
|
||||||
|
@Directive({
|
||||||
|
selector: '[opTrack]',
|
||||||
|
standalone: true,
|
||||||
|
})
|
||||||
|
export class OpenPanelTrackDirective {
|
||||||
|
private readonly op = inject(OpenPanelService);
|
||||||
|
|
||||||
|
/** The event name to track on click. */
|
||||||
|
@Input({ required: true }) opTrack!: string;
|
||||||
|
|
||||||
|
/** Optional properties to send with the event. */
|
||||||
|
@Input() opTrackProps?: TrackProperties;
|
||||||
|
|
||||||
|
/** Which DOM event triggers tracking. Default: 'click' */
|
||||||
|
@Input() opTrackOn: 'click' | 'mouseenter' | 'focus' | 'blur' = 'click';
|
||||||
|
|
||||||
|
@HostListener('click')
|
||||||
|
onClick(): void {
|
||||||
|
if (this.opTrackOn === 'click') {
|
||||||
|
this.op.track(this.opTrack, this.opTrackProps);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@HostListener('mouseenter')
|
||||||
|
onMouseEnter(): void {
|
||||||
|
if (this.opTrackOn === 'mouseenter') {
|
||||||
|
this.op.track(this.opTrack, this.opTrackProps);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@HostListener('focus')
|
||||||
|
onFocus(): void {
|
||||||
|
if (this.opTrackOn === 'focus') {
|
||||||
|
this.op.track(this.opTrack, this.opTrackProps);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@HostListener('blur')
|
||||||
|
onBlur(): void {
|
||||||
|
if (this.opTrackOn === 'blur') {
|
||||||
|
this.op.track(this.opTrack, this.opTrackProps);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
35
src/app/core/models/openpanel.model.ts
Normal file
35
src/app/core/models/openpanel.model.ts
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
import { InjectionToken } from '@angular/core';
|
||||||
|
|
||||||
|
export interface OpenPanelConfig {
|
||||||
|
/** Your OpenPanel Client ID (required) */
|
||||||
|
clientId: string;
|
||||||
|
|
||||||
|
/** URL of your OpenPanel API or self-hosted instance.
|
||||||
|
* Defaults to https://api.openpanel.dev */
|
||||||
|
apiUrl?: string;
|
||||||
|
|
||||||
|
/** Automatically track Angular Router navigation events as screen views.
|
||||||
|
* Default: true */
|
||||||
|
trackScreenViews?: boolean;
|
||||||
|
|
||||||
|
/** Track clicks on outgoing links automatically.
|
||||||
|
* Default: false */
|
||||||
|
trackOutgoingLinks?: boolean;
|
||||||
|
|
||||||
|
/** Enable declarative tracking via data-track HTML attributes.
|
||||||
|
* Default: false */
|
||||||
|
trackAttributes?: boolean;
|
||||||
|
|
||||||
|
/** Global properties sent with every event (e.g. app_version, environment). */
|
||||||
|
globalProperties?: Record<string, string | number | boolean>;
|
||||||
|
|
||||||
|
/** Completely disable all tracking (e.g. in test environments).
|
||||||
|
* Default: false */
|
||||||
|
disabled?: boolean;
|
||||||
|
|
||||||
|
/** Enable verbose console logging for debugging.
|
||||||
|
* Default: false */
|
||||||
|
debug?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const OPENPANEL_CONFIG = new InjectionToken<OpenPanelConfig>('OPENPANEL_CONFIG');
|
||||||
31
src/app/core/provider/openpanel.provider.ts
Normal file
31
src/app/core/provider/openpanel.provider.ts
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
import { EnvironmentProviders, makeEnvironmentProviders } from '@angular/core';
|
||||||
|
import { OpenPanelConfig, OPENPANEL_CONFIG } from '@core/models/openpanel.model';
|
||||||
|
import { OpenPanelService } from '@core/services/openpanel.service';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Provides the OpenPanel analytics service for your Angular application.
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* // app.config.ts
|
||||||
|
* import { provideOpenPanel } from './openpanel/openpanel.provider';
|
||||||
|
*
|
||||||
|
* export const appConfig: ApplicationConfig = {
|
||||||
|
* providers: [
|
||||||
|
* provideRouter(routes),
|
||||||
|
* provideOpenPanel({
|
||||||
|
* clientId: 'your-client-id',
|
||||||
|
* trackScreenViews: true,
|
||||||
|
* globalProperties: {
|
||||||
|
* app_version: '1.0.0',
|
||||||
|
* environment: 'production',
|
||||||
|
* },
|
||||||
|
* }),
|
||||||
|
* ],
|
||||||
|
* };
|
||||||
|
*/
|
||||||
|
export function provideOpenPanel(config: OpenPanelConfig): EnvironmentProviders {
|
||||||
|
return makeEnvironmentProviders([
|
||||||
|
{ provide: OPENPANEL_CONFIG, useValue: config },
|
||||||
|
OpenPanelService,
|
||||||
|
]);
|
||||||
|
}
|
||||||
@@ -1,13 +1,13 @@
|
|||||||
import { TestBed } from '@angular/core/testing';
|
import { TestBed } from '@angular/core/testing';
|
||||||
|
|
||||||
import { OpenpanelService } from './openpanel.service';
|
import { OpenPanelService } from './openpanel.service';
|
||||||
|
|
||||||
describe('OpenpanelService', () => {
|
describe('OpenpanelService', () => {
|
||||||
let service: OpenpanelService;
|
let service: OpenPanelService;
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
TestBed.configureTestingModule({});
|
TestBed.configureTestingModule({});
|
||||||
service = TestBed.inject(OpenpanelService);
|
service = TestBed.inject(OpenPanelService);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should be created', () => {
|
it('should be created', () => {
|
||||||
|
|||||||
@@ -1,29 +1,148 @@
|
|||||||
import { Injectable } from '@angular/core';
|
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 { OpenPanel } from '@openpanel/web';
|
||||||
|
import type { IdentifyPayload } from '@openpanel/web';
|
||||||
|
import { OpenPanelConfig, OPENPANEL_CONFIG } from '@core/models/openpanel.model';
|
||||||
|
|
||||||
|
export type TrackProperties = Record<string, string | number | boolean | null | undefined>;
|
||||||
|
|
||||||
@Injectable({
|
@Injectable({
|
||||||
providedIn: 'root'
|
providedIn: 'root',
|
||||||
})
|
})
|
||||||
export class AnalyticsService {
|
export class OpenPanelService implements OnDestroy {
|
||||||
private openpanel: OpenPanel;
|
private readonly config = inject(OPENPANEL_CONFIG);
|
||||||
|
private readonly platformId = inject(PLATFORM_ID)
|
||||||
|
private readonly router = inject(Router);
|
||||||
|
|
||||||
|
private op?: OpenPanel;
|
||||||
|
private routerSubscription?: Subscription;
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
this.openpanel = new OpenPanel({
|
if(isPlatformBrowser(this.platformId)) {
|
||||||
clientId: 'DEINE_CLIENT_ID',
|
this.initialize();
|
||||||
trackScreenViews: true,
|
}
|
||||||
trackOutgoingLinks: true,
|
}
|
||||||
|
|
||||||
|
// ─── 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 {
|
||||||
|
// Nur einmalig subscriben, vorherige Sub zerstören
|
||||||
|
this.routerSubscription?.unsubscribe();
|
||||||
|
|
||||||
|
this.routerSubscription = this.router.events
|
||||||
|
.pipe(
|
||||||
|
filter((event) => event instanceof NavigationEnd),
|
||||||
|
// Ersten initialNavigation-Event überspringen – SSR hat ihn schon getriggert
|
||||||
|
skip(1),
|
||||||
|
)
|
||||||
|
.subscribe((event) => {
|
||||||
|
const navEvent = event as NavigationEnd;
|
||||||
|
this.trackScreenView(navEvent.urlAfterRedirects);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
trackEvent(name: string, properties?: Record<string, any>) {
|
// ─── Public API ────────────────────────────────────────────────────────────
|
||||||
this.openpanel.track(name, properties);
|
|
||||||
|
/**
|
||||||
|
* 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);
|
||||||
}
|
}
|
||||||
|
|
||||||
identifyUser(userId: string, traits?: Record<string, any>) {
|
/**
|
||||||
this.openpanel.identify(userId);
|
* 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);
|
||||||
|
}
|
||||||
|
|
||||||
if (traits) {
|
/**
|
||||||
this.openpanel.setProfile(traits);
|
* 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', 5);
|
||||||
|
*/
|
||||||
|
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();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -2,7 +2,7 @@
|
|||||||
<section class="header">
|
<section class="header">
|
||||||
<div class="logo-container">
|
<div class="logo-container">
|
||||||
<span class="logo-container__logo centered">H</span>
|
<span class="logo-container__logo centered">H</span>
|
||||||
<p class="logo-container__company"><span>Hurler</span> Webdesign</p>
|
<p class="logo-container__company" (click)="onFeaturesClick('lustiges Zeug')"><span>Hurler</span> Webdesign</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="header__nav-section centered">
|
<div class="header__nav-section centered">
|
||||||
<div class="theme-toggle-container">
|
<div class="theme-toggle-container">
|
||||||
|
|||||||
@@ -3,13 +3,20 @@ import { NgIcon } from '@ng-icons/core';
|
|||||||
import { ToogleThemeComponent } from '@shared/utils/toogle-theme/toogle-theme.component';
|
import { ToogleThemeComponent } from '@shared/utils/toogle-theme/toogle-theme.component';
|
||||||
import { NavMenuComponent } from '@shared/ui/nav-menu/nav-menu.component';
|
import { NavMenuComponent } from '@shared/ui/nav-menu/nav-menu.component';
|
||||||
import { NavigationService } from '@core/services/navigation.service';
|
import { NavigationService } from '@core/services/navigation.service';
|
||||||
|
import { OpenPanelService } from '@core/services/openpanel.service';
|
||||||
|
import { OpenPanelTrackDirective } from '@core/directives/openpanel.directive';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-navigation',
|
selector: 'app-navigation',
|
||||||
imports: [NgIcon, ToogleThemeComponent, NavMenuComponent],
|
imports: [NgIcon, ToogleThemeComponent, NavMenuComponent, OpenPanelTrackDirective],
|
||||||
templateUrl: './navigation.component.html',
|
templateUrl: './navigation.component.html',
|
||||||
styleUrl: './navigation.component.scss',
|
styleUrl: './navigation.component.scss',
|
||||||
})
|
})
|
||||||
export class NavigationComponent {
|
export class NavigationComponent {
|
||||||
protected readonly navigationService = inject(NavigationService);
|
protected readonly navigationService = inject(NavigationService);
|
||||||
|
private op = inject(OpenPanelService)
|
||||||
|
|
||||||
|
onFeaturesClick(blindplan: string): void {
|
||||||
|
this.op.track('features_clicked', { blindplan })
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
<li class="navigation__list-item">
|
<li class="navigation__list-item">
|
||||||
<!-- Anchor-Link: Smooth Scroll -->
|
<!-- Anchor-Link: Smooth Scroll -->
|
||||||
@if (isAnchor(item)) {
|
@if (isAnchor(item)) {
|
||||||
<a [href]="'#' + item.target" (click)="onAnchorClick($event, item)">
|
<a [href]="'#' + item.target" (click)="onAnchorClick($event, item)" opTrack="links_nav_menu" [opTrackProps]="{location: `${item.target}`}">
|
||||||
@if (item.icon) {
|
@if (item.icon) {
|
||||||
<ng-icon [name]="item.icon"></ng-icon>
|
<ng-icon [name]="item.icon"></ng-icon>
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,10 +3,11 @@ import { RouterLink } from '@angular/router';
|
|||||||
import { NgIcon } from '@ng-icons/core';
|
import { NgIcon } from '@ng-icons/core';
|
||||||
import { NavigationService } from '@core/services/navigation.service';
|
import { NavigationService } from '@core/services/navigation.service';
|
||||||
import {NavigationItem, isAnchor, isRoute} from '@core/models/navigation.model';
|
import {NavigationItem, isAnchor, isRoute} from '@core/models/navigation.model';
|
||||||
|
import { OpenPanelTrackDirective } from '@core/directives/openpanel.directive';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-nav-menu',
|
selector: 'app-nav-menu',
|
||||||
imports: [RouterLink, NgIcon],
|
imports: [RouterLink, NgIcon, OpenPanelTrackDirective],
|
||||||
templateUrl: './nav-menu.component.html',
|
templateUrl: './nav-menu.component.html',
|
||||||
styleUrl: './nav-menu.component.scss',
|
styleUrl: './nav-menu.component.scss',
|
||||||
})
|
})
|
||||||
|
|||||||
9
src/environments/openpanel.ts
Normal file
9
src/environments/openpanel.ts
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
// environment.ts
|
||||||
|
export const environment = {
|
||||||
|
production: false,
|
||||||
|
secret: "sec_4aa70c091e704023c6df",
|
||||||
|
openPanel: {
|
||||||
|
clientId: '727b9649-26ac-4083-96ea-92c3a60fe7a8',
|
||||||
|
apiUrl: 'https://analytics.hurler-webdesign.de/api', // oder self-hosted URL
|
||||||
|
}
|
||||||
|
};
|
||||||
Reference in New Issue
Block a user