diff --git a/src/app/app.config.ts b/src/app/app.config.ts
index 67fb0b7..7152bfb 100644
--- a/src/app/app.config.ts
+++ b/src/app/app.config.ts
@@ -1,6 +1,8 @@
import { ApplicationConfig, provideBrowserGlobalErrorListeners } from '@angular/core';
import { provideRouter } from '@angular/router';
import { provideNgIconsConfig } from '@ng-icons/core';
+import { environment } from '../environments/openpanel';
+import { provideOpenPanel } from '@core/provider/openpanel.provider';
import { routes } from './app.routes';
import { provideClientHydration, withEventReplay } from '@angular/platform-browser';
@@ -10,6 +12,12 @@ export const appConfig: ApplicationConfig = {
provideBrowserGlobalErrorListeners(),
provideRouter(routes),
provideClientHydration(withEventReplay()),
- provideNgIconsConfig({})
+ provideNgIconsConfig({}),
+ provideOpenPanel({
+ clientId: environment.openPanel.clientId,
+ apiUrl: environment.openPanel.apiUrl,
+ trackScreenViews: true,
+ debug: !environment.production,
+ })
]
};
diff --git a/src/app/core/directives/openpanel.directive.ts b/src/app/core/directives/openpanel.directive.ts
new file mode 100644
index 0000000..149893b
--- /dev/null
+++ b/src/app/core/directives/openpanel.directive.ts
@@ -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
+ *
+ *
+ * @example
+ * Pricing
+ */
+@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);
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/app/core/models/openpanel.model.ts b/src/app/core/models/openpanel.model.ts
new file mode 100644
index 0000000..216d402
--- /dev/null
+++ b/src/app/core/models/openpanel.model.ts
@@ -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;
+
+ /** 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('OPENPANEL_CONFIG');
\ No newline at end of file
diff --git a/src/app/core/provider/openpanel.provider.ts b/src/app/core/provider/openpanel.provider.ts
new file mode 100644
index 0000000..d5b4db5
--- /dev/null
+++ b/src/app/core/provider/openpanel.provider.ts
@@ -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,
+ ]);
+}
\ No newline at end of file
diff --git a/src/app/core/services/openpanel.service.spec.ts b/src/app/core/services/openpanel.service.spec.ts
index f15e083..eb3e954 100644
--- a/src/app/core/services/openpanel.service.spec.ts
+++ b/src/app/core/services/openpanel.service.spec.ts
@@ -1,13 +1,13 @@
import { TestBed } from '@angular/core/testing';
-import { OpenpanelService } from './openpanel.service';
+import { OpenPanelService } from './openpanel.service';
describe('OpenpanelService', () => {
- let service: OpenpanelService;
+ let service: OpenPanelService;
beforeEach(() => {
TestBed.configureTestingModule({});
- service = TestBed.inject(OpenpanelService);
+ service = TestBed.inject(OpenPanelService);
});
it('should be created', () => {
diff --git a/src/app/core/services/openpanel.service.ts b/src/app/core/services/openpanel.service.ts
index a6b1518..9b7ffe1 100644
--- a/src/app/core/services/openpanel.service.ts
+++ b/src/app/core/services/openpanel.service.ts
@@ -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 type { IdentifyPayload } from '@openpanel/web';
+import { OpenPanelConfig, OPENPANEL_CONFIG } from '@core/models/openpanel.model';
+
+export type TrackProperties = Record;
@Injectable({
- providedIn: 'root'
+ providedIn: 'root',
})
-export class AnalyticsService {
- private openpanel: OpenPanel;
+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() {
- this.openpanel = new OpenPanel({
- clientId: 'DEINE_CLIENT_ID',
- trackScreenViews: true,
- trackOutgoingLinks: true,
- });
- }
-
- trackEvent(name: string, properties?: Record) {
- this.openpanel.track(name, properties);
- }
-
- identifyUser(userId: string, traits?: Record) {
- this.openpanel.identify(userId);
-
- if (traits) {
- this.openpanel.setProfile(traits);
+ 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 {
+ // 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);
+ });
+ }
+
+ // ─── 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', 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();
+ }
}
\ No newline at end of file
diff --git a/src/app/features/landing/components/navigation/navigation.component.html b/src/app/features/landing/components/navigation/navigation.component.html
index decf71b..2106f31 100644
--- a/src/app/features/landing/components/navigation/navigation.component.html
+++ b/src/app/features/landing/components/navigation/navigation.component.html
@@ -2,7 +2,7 @@