environment.ts
· 214 B · TypeScript
Raw
export const environment = {
production: false,
openPanel: {
clientId: 'OPENPANEL_CLIENT_ID',
apiUrl: 'https://worker.domain.de', // mapping zu Worker Container, apiURL nur bei selfhost notwendig
}
};
| 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
· 1.5 KiB · TypeScript
Raw
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);
}
}
}
| 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
· 1.1 KiB · TypeScript
Raw
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');
| 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
· 4.6 KiB · TypeScript
Raw
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<string, string | number | boolean | null | undefined>;
@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();
}
}
| 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 | } |