Skip to content

Commit 1d5b09f

Browse files
Merge pull request #478 from linuxfoundation/issue-#4790-segement-script
Implement Segment analytics integration with LFX Analytics service
2 parents 88b7c8d + 1632967 commit 1d5b09f

File tree

12 files changed

+267
-6
lines changed

12 files changed

+267
-6
lines changed

edge/security-headers.js

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,10 @@ function generateCSP(env, isDevServer) {
4848
'https://www.google-analytics.com', // Google Analytics beacons
4949
'https://analytics.google.com', // Google Analytics 4
5050
'https://www.googletagmanager.com', // GTM fetch requests
51-
'https://stats.g.doubleclick.net' // DoubleClick stats
51+
'https://stats.g.doubleclick.net', // DoubleClick stats
52+
'https://lfx-segment.dev.platform.linuxfoundation.org', // LFX Segments Analytics (dev)
53+
'https://lfx-segment.platform.linuxfoundation.org', // LFX Segments Analytics (prod)
54+
'https://api.segment.io' // Segment Analytics API
5255
];
5356
let scriptSources = [SELF, UNSAFE_EVAL, UNSAFE_INLINE,
5457
'https://cdn.dev.platform.linuxfoundation.org/lfx-header-v2.js',
@@ -60,7 +63,9 @@ function generateCSP(env, isDevServer) {
6063
'https://cdn.staging.platform.linuxfoundation.org/lfx-footer-no-zone.js',
6164
'https://cdn.platform.linuxfoundation.org/lfx-footer-no-zone.js',
6265
'https://cmp.osano.com', // Cookie consent
63-
'https://www.googletagmanager.com' // Google Tag Manager for Osano
66+
'https://www.googletagmanager.com', // Google Tag Manager for Osano
67+
'https://lfx-segment.dev.platform.linuxfoundation.org', // LFX Segments Analytics (dev)
68+
'https://lfx-segment.platform.linuxfoundation.org' // LFX Segments Analytics (prod)
6469
];
6570

6671
const styleSources = [SELF, UNSAFE_INLINE, 'https://use.fontawesome.com/', 'https://communitybridge.org/'];

src/app/app.module.ts

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
// SPDX-License-Identifier: MIT
33

44
import { BrowserModule } from '@angular/platform-browser';
5-
import { NgModule } from '@angular/core';
5+
import { NgModule, APP_INITIALIZER } from '@angular/core';
66

77
import { AppRoutingModule } from './app-routing.module';
88
import { AppComponent } from './app.component';
@@ -17,6 +17,21 @@ import { IndividualContributorModule } from './modules/individual-contributor/in
1717
import { CorporateContributorModule } from './modules/corporate-contributor/corporate-contributor.module';
1818
import { FormsModule } from '@angular/forms';
1919
import { InterceptorService } from './shared/services/interceptor.service';
20+
import { LfxAnalyticsService } from './shared/services/lfx-analytics.service';
21+
22+
// Add initialization factory
23+
export function initializeAnalytics(analyticsService: LfxAnalyticsService) {
24+
return () => {
25+
// Initialize analytics and track app start
26+
analyticsService.trackEvent('Application Initialized', {
27+
timestamp: new Date().toISOString(),
28+
userAgent: navigator.userAgent,
29+
app: 'easycla-contributor-console'
30+
}).catch(error => {
31+
console.error('Failed to track application initialization:', error);
32+
});
33+
};
34+
}
2035

2136
@NgModule({
2237
declarations: [
@@ -45,6 +60,12 @@ import { InterceptorService } from './shared/services/interceptor.service';
4560
useClass: InterceptorService,
4661
multi: true
4762
},
63+
{
64+
provide: APP_INITIALIZER,
65+
useFactory: initializeAnalytics,
66+
deps: [LfxAnalyticsService],
67+
multi: true,
68+
},
4869
AlertService
4970
],
5071
bootstrap: [AppComponent]

src/app/shared/directives/index.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
// Copyright The Linux Foundation and each contributor to CommunityBridge.
2+
// SPDX-License-Identifier: MIT
3+
4+
export * from './track-event.directive';
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
// Copyright The Linux Foundation and each contributor to CommunityBridge.
2+
// SPDX-License-Identifier: MIT
3+
4+
import { Directive, Input, HostListener } from '@angular/core';
5+
import { LfxAnalyticsService } from '../services/lfx-analytics.service';
6+
7+
@Directive({
8+
selector: '[lfxTrackEvent]'
9+
})
10+
export class TrackEventDirective {
11+
@Input() lfxTrackEvent = '';
12+
@Input() lfxTrackEventProperties: Record<string, any> = {};
13+
14+
constructor(private analyticsService: LfxAnalyticsService) {}
15+
16+
@HostListener('click', ['$event'])
17+
onClick(event: Event): void {
18+
if (this.lfxTrackEvent) {
19+
const properties = {
20+
element: (event.target as HTMLElement)?.tagName?.toLowerCase() || 'unknown',
21+
elementId: (event.target as HTMLElement)?.id || undefined,
22+
elementClass: (event.target as HTMLElement)?.className || undefined,
23+
...this.lfxTrackEventProperties
24+
};
25+
26+
this.analyticsService.trackEvent(this.lfxTrackEvent, properties).catch(error => {
27+
console.error('Failed to track event:', error);
28+
});
29+
}
30+
}
31+
}

src/app/shared/services/auth.service.ts

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import Url from 'url-parse';
1414
import { EnvConfig } from '../../config/cla-env-utils';
1515
import { AppSettings } from 'src/app/config/app-settings';
1616
import { StorageService } from './storage.service';
17+
import { LfxAnalyticsService } from './lfx-analytics.service';
1718

1819
@Injectable({
1920
providedIn: 'root',
@@ -72,7 +73,8 @@ export class AuthService {
7273

7374
constructor(
7475
private router: Router,
75-
private storageService: StorageService
76+
private storageService: StorageService,
77+
private analyticsService: LfxAnalyticsService
7678
) {
7779

7880
this.initializeApplication();
@@ -105,6 +107,12 @@ export class AuthService {
105107
this.setSession(user);
106108
this.setUserInHeader(user);
107109
this.userProfileSubject$.next(user);
110+
// Identify user in analytics
111+
if (user) {
112+
this.analyticsService.identifyAuth0User(user).catch(error => {
113+
console.error('Failed to identify user in analytics:', error);
114+
});
115+
}
108116
})
109117
);
110118
}
@@ -129,6 +137,11 @@ export class AuthService {
129137
}
130138

131139
logout() {
140+
// Reset analytics state before logout
141+
this.analyticsService.reset().catch(error => {
142+
console.error('Failed to reset analytics state:', error);
143+
});
144+
132145
const { query, fragmentIdentifier } = querystring.parseUrl(
133146
window.location.href,
134147
{ parseFragmentIdentifier: true }
Lines changed: 179 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,179 @@
1+
// Copyright The Linux Foundation and each contributor to CommunityBridge.
2+
// SPDX-License-Identifier: MIT
3+
4+
import { Injectable } from '@angular/core';
5+
import { Router, NavigationEnd } from '@angular/router';
6+
import { filter } from 'rxjs/operators';
7+
import { environment } from '../../../environments/environment';
8+
9+
declare global {
10+
interface Window {
11+
LfxAnalytics?: {
12+
LfxSegmentsAnalytics?: any;
13+
};
14+
}
15+
}
16+
17+
@Injectable({
18+
providedIn: 'root'
19+
})
20+
export class LfxAnalyticsService {
21+
private analytics: any;
22+
private isInitialized = false;
23+
private isScriptLoaded = false;
24+
private initializationPromise: Promise<void> | null = null;
25+
26+
constructor(private router: Router) {
27+
this.initializeService();
28+
}
29+
30+
async trackPageView(pageName: string, properties?: Record<string, any>): Promise<void> {
31+
if (!this.isInitialized || !this.analytics) {
32+
await this.initializeService();
33+
}
34+
35+
if (this.isInitialized && this.analytics) {
36+
try {
37+
const pageProperties = {
38+
path: pageName,
39+
url: window.location.href,
40+
title: document.title,
41+
referrer: document.referrer,
42+
...properties
43+
};
44+
45+
await this.analytics.page(pageName, pageProperties);
46+
} catch (error) {
47+
console.error('Failed to track page view:', error);
48+
}
49+
}
50+
}
51+
52+
async trackEvent(eventName: string, properties?: Record<string, any>): Promise<void> {
53+
if (!this.isInitialized || !this.analytics) {
54+
await this.initializeService();
55+
}
56+
57+
if (this.isInitialized && this.analytics) {
58+
try {
59+
const eventProperties = {
60+
url: window.location.href,
61+
path: window.location.pathname,
62+
...properties
63+
};
64+
65+
await this.analytics.track(eventName, eventProperties);
66+
} catch (error) {
67+
console.error('Failed to track event:', error);
68+
}
69+
}
70+
}
71+
72+
async identifyAuth0User(auth0User: any): Promise<void> {
73+
if (!this.isInitialized || !this.analytics) {
74+
await this.initializeService();
75+
}
76+
77+
if (this.isInitialized && this.analytics && auth0User) {
78+
try {
79+
await this.analytics.identifyAuth0User(auth0User);
80+
} catch (error) {
81+
console.error('Failed to identify Auth0 user:', error);
82+
}
83+
}
84+
}
85+
86+
async reset(): Promise<void> {
87+
if (this.isInitialized && this.analytics) {
88+
try {
89+
await this.analytics.reset();
90+
} catch (error) {
91+
console.error('Failed to reset analytics:', error);
92+
}
93+
}
94+
}
95+
96+
isAnalyticsInitialized(): boolean {
97+
return this.isInitialized;
98+
}
99+
100+
private async initializeService(): Promise<void> {
101+
if (this.initializationPromise) {
102+
return this.initializationPromise;
103+
}
104+
105+
this.initializationPromise = this.performInitialization();
106+
return this.initializationPromise;
107+
}
108+
109+
private async performInitialization(): Promise<void> {
110+
try {
111+
if (!this.isScriptLoaded) {
112+
await this.loadAnalyticsScript();
113+
this.isScriptLoaded = true;
114+
}
115+
116+
await this.waitForAnalytics();
117+
118+
this.analytics = window.LfxAnalytics.LfxSegmentsAnalytics.getInstance();
119+
await this.analytics.init();
120+
this.isInitialized = true;
121+
122+
this.setupRouteTracking();
123+
124+
} catch (error) {
125+
console.error('Failed to initialize LFX Segments Analytics:', error);
126+
}
127+
}
128+
129+
private loadAnalyticsScript(): Promise<void> {
130+
return new Promise((resolve, reject) => {
131+
if (window.LfxAnalytics?.LfxSegmentsAnalytics) {
132+
resolve();
133+
return;
134+
}
135+
136+
const script = document.createElement('script');
137+
script.src = environment.lfxSegmentAnalyticsUrl;
138+
script.async = true;
139+
script.defer = true;
140+
141+
script.onload = () => {
142+
resolve();
143+
};
144+
145+
script.onerror = (error) => {
146+
console.error('Failed to load LFX Segments Analytics script:', error);
147+
reject(new Error('Failed to load analytics script'));
148+
};
149+
150+
document.head.appendChild(script);
151+
});
152+
}
153+
154+
private async waitForAnalytics(): Promise<void> {
155+
const maxAttempts = 50; // 5 seconds max wait time
156+
let attempts = 0;
157+
158+
while (!window.LfxAnalytics?.LfxSegmentsAnalytics && attempts < maxAttempts) {
159+
await new Promise(resolve => setTimeout(resolve, 100));
160+
attempts++;
161+
}
162+
163+
if (!window.LfxAnalytics?.LfxSegmentsAnalytics) {
164+
throw new Error('LFX Segments Analytics library not available after timeout');
165+
}
166+
}
167+
168+
private setupRouteTracking(): void {
169+
this.router.events
170+
.pipe(
171+
filter(event => event instanceof NavigationEnd)
172+
)
173+
.subscribe((event: NavigationEnd) => {
174+
this.trackPageView(event.urlAfterRedirects || event.url);
175+
});
176+
177+
this.trackPageView(this.router.url);
178+
}
179+
}

src/app/shared/shared.module.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import { InterceptorService } from './services/interceptor.service';
1818
import { FooterComponent } from './components/footer/footer.component';
1919
import { ConsentComponent } from './components/consent/consent.component';
2020
import { CommonModule } from '@angular/common';
21+
import { TrackEventDirective } from './directives/track-event.directive';
2122

2223
@NgModule({
2324
declarations: [
@@ -31,7 +32,8 @@ import { CommonModule } from '@angular/common';
3132
TrimCharactersPipe,
3233
CheckboxComponent,
3334
FooterComponent,
34-
ConsentComponent
35+
ConsentComponent,
36+
TrackEventDirective
3537
],
3638
imports: [
3739
CommonModule
@@ -47,7 +49,8 @@ import { CommonModule } from '@angular/common';
4749
TrimCharactersPipe,
4850
CheckboxComponent,
4951
FooterComponent,
50-
ConsentComponent
52+
ConsentComponent,
53+
TrackEventDirective
5154
],
5255
providers: [StorageService, AuthService, LfxHeaderService, InterceptorService]
5356
})

src/environments/environment.dev.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
export const environment = {
55
production: true,
66
lfxHeader: 'https://cdn.dev.platform.linuxfoundation.org',
7+
lfxSegmentAnalyticsUrl: 'https://lfx-segment.dev.platform.linuxfoundation.org/latest/lfx-segment-analytics.min.js',
78
ACCEPTABLE_USER_POLICY: 'https://communitybridge.dev.platform.linuxfoundation.org/acceptable-use/',
89
SERVICE_SPECIFIC_TERM: 'https://communitybridge.dev.platform.linuxfoundation.org/service-terms/',
910
PLATFORM_USER_AGREEMENT: 'https://communitybridge.dev.platform.linuxfoundation.org/platform-use-agreement/',

src/environments/environment.local.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
export const environment = {
55
production: false,
66
lfxHeader: 'https://cdn.dev.platform.linuxfoundation.org',
7+
lfxSegmentAnalyticsUrl: 'https://lfx-segment.dev.platform.linuxfoundation.org/latest/lfx-segment-analytics.min.js',
78
ACCEPTABLE_USER_POLICY:
89
'https://communitybridge.dev.platform.linuxfoundation.org/acceptable-use/',
910
SERVICE_SPECIFIC_TERM:

src/environments/environment.prod.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
export const environment = {
55
production: true,
66
lfxHeader: 'https://cdn.platform.linuxfoundation.org',
7+
lfxSegmentAnalyticsUrl: 'https://lfx-segment.platform.linuxfoundation.org/latest/lfx-segment-analytics.min.js',
78
ACCEPTABLE_USER_POLICY: 'https://communitybridge.platform.linuxfoundation.org/acceptable-use/',
89
SERVICE_SPECIFIC_TERM: 'https://communitybridge.platform.linuxfoundation.org/service-terms/',
910
PLATFORM_USER_AGREEMENT: 'https://communitybridge.platform.linuxfoundation.org/platform-use-agreement/',

0 commit comments

Comments
 (0)