Skip to content

Commit c9ccde2

Browse files
authored
Implemented service worker to intercept network traces, added tracking simple page navigation (#2743)
* Added sent page load metrics to backend logs * Added service worker, network traces and tracking simple page navigation * Clean sensitive data from traces parameters * Added feature flag for client telemetry * added clean up worker if feature flag is disabled * extended user event logging for buttons clicks * improved logging messages format * set console logger by default and improved user interaction logging
1 parent 9d7d952 commit c9ccde2

File tree

13 files changed

+359
-13
lines changed

13 files changed

+359
-13
lines changed

.github/workflows/mainCI.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,10 +18,10 @@ jobs:
1818
security-events: write
1919

2020
steps:
21-
- name: Use Node.js 16.x
21+
- name: Use Node.js 20.x
2222
uses: actions/checkout@v2
2323
with:
24-
node-version: 16.x
24+
node-version: 20.x
2525

2626
- name: Install
2727
run: npm install

src/apim.runtime.module.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,10 +88,12 @@ import { TagService } from "./services/tagService";
8888
import { TenantService } from "./services/tenantService";
8989
import { UsersService } from "./services/usersService";
9090
import { TraceClick } from "./bindingHandlers/traceClick";
91+
import { ClientLogger } from "./logging/clientLogger";
9192

9293
export class ApimRuntimeModule implements IInjectorModule {
9394
public register(injector: IInjector): void {
9495
injector.bindSingleton("logger", ConsoleLogger);
96+
// injector.bindSingleton("logger", ClientLogger);
9597
injector.bindSingleton("traceClick", TraceClick);
9698
injector.bindToCollection("autostart", UnhandledErrorHandler);
9799
injector.bindToCollection("autostart", BalloonBindingHandler);
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
<button id="signinB2C" class="button" data-bind="click: signIn, css: classNames, traceClick">
1+
<button id="signinB2C" class="button" data-action="B2C Sign in" data-bind="click: signIn, css: classNames, traceClick">
22
<i class="icon-emb icon-svg-aad"></i>
33
<span data-bind="text: label"></span>
44
</button>
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
<button id="signinAad" class="button" data-bind="click: signIn, css: classNames, traceClick">
1+
<button id="signinAad" class="button" data-action="AAD Sign in" data-bind="click: signIn, css: classNames, traceClick">
22
<i class="icon-emb icon-svg-aad"></i>
33
<span data-bind="text: label"></span>
44
</button>

src/components/users/signup/ko/runtime/signup.html

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -34,16 +34,16 @@
3434

3535
<!-- ko if: requireHipCaptcha -->
3636
<hip-captcha params="{ captchaData: captchaData, onInitComplete: onCaptchaCreated }"></hip-captcha>
37-
<!-- /ko -->
38-
37+
<!-- /ko -->
38+
3939
<!-- ko if: termsEnabled && termsOfUse -->
4040
<terms-of-use params="{ isConsentRequired: isConsentRequired, consented: consented, termsOfUse: termsOfUse }"></terms-of-use>
4141
<!-- /ko -->
4242

4343
<div class="form-group">
44-
44+
4545
<!-- ko ifnot: working -->
46-
<button type="button" id="signup" class="button button-primary" data-bind="click: signup">
46+
<button type="button" id="signup" class="button button-primary" data-action="Sign up" data-bind="click: signup">
4747
Sign up
4848
</button>
4949
<!-- /ko -->

src/constants.ts

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -344,4 +344,15 @@ export const smallMobileBreakpoint = 400;
344344
/**
345345
* Key of the default admin user
346346
*/
347-
export const integrationUserId = '/users/integration';
347+
export const integrationUserId = '/users/integration';
348+
349+
/**
350+
* This is used to store the unique user in local storage and identify the user session in client telemetry.
351+
*/
352+
export const USER_SESSION = "userSessionId";
353+
export const USER_ID = "userId";
354+
export const USER_ACTION = "data-action";
355+
356+
// Feature flags
357+
export const FEATURE_FLAGS = "featureFlags";
358+
export const FEATURE_CLIENT_TELEMETRY = "clientTelemetry";

src/logging/clientLogger.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { ISettingsProvider } from "@paperbits/common/configuration";
55
import { ClientEvent } from "../models/logging/clientEvent";
66
import { v4 as uuidv4 } from "uuid";
77
import * as Constants from "../constants";
8+
import { Utils } from "../utils";
89

910
export enum eventTypes {
1011
error = "Error",
@@ -33,6 +34,7 @@ export class ClientLogger implements Logger {
3334

3435
public async trackEvent(eventName: string, properties?: Bag<string>): Promise<void> {
3536
const devPortalEvent = new ClientEvent();
37+
this.addUserDataToEventData(properties);
3638

3739
devPortalEvent.eventType = eventName;
3840
devPortalEvent.message = properties?.message;
@@ -43,6 +45,7 @@ export class ClientLogger implements Logger {
4345

4446
public async trackError(error: Error, properties?: Bag<string>): Promise<void> {
4547
const devPortalEvent = new ClientEvent();
48+
this.addUserDataToEventData(properties);
4649

4750
devPortalEvent.eventType = eventTypes.error;
4851
devPortalEvent.message = error?.message;
@@ -54,6 +57,7 @@ export class ClientLogger implements Logger {
5457

5558
public async trackView(viewName: string, properties?: Bag<string>): Promise<void> {
5659
const devPortalEvent = new ClientEvent();
60+
this.addUserDataToEventData(properties);
5761

5862
devPortalEvent.eventType = viewName;
5963
devPortalEvent.message = properties?.message;
@@ -70,6 +74,13 @@ export class ClientLogger implements Logger {
7074
// Not implemented
7175
}
7276

77+
private addUserDataToEventData(eventData?: Bag<string>) {
78+
const userData = Utils.getUserData();
79+
eventData = eventData || {};
80+
eventData[Constants.USER_ID] = userData.userId;
81+
eventData[Constants.USER_SESSION] = userData.sessionId;
82+
}
83+
7384
private async traceEvent(clientEvent: ClientEvent) {
7485
const datetime = new Date();
7586

src/startup.runtime.ts

Lines changed: 39 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,10 @@ import { staticDataEnvironment } from "./../environmentConstants";
66
import { define } from "mime";
77
import { TraceClick } from "./bindingHandlers/traceClick";
88
import { Logger } from "@paperbits/common/logging";
9+
import { TelemetryConfigurator } from "./telemetry/telemetryConfigurator";
10+
import { Utils } from "./utils";
11+
import { ISettingsProvider } from "@paperbits/common/configuration/ISettingsProvider";
12+
import { FEATURE_CLIENT_TELEMETRY, FEATURE_FLAGS } from "./constants";
913

1014
define({ "application/x-zip-compressed": ["zip"] }, true);
1115

@@ -25,13 +29,46 @@ document.addEventListener("DOMContentLoaded", () => {
2529
traceClick.setupBinding();
2630
});
2731

32+
initFeatures();
33+
2834
window.onbeforeunload = () => {
2935
if (!location.pathname.startsWith("/signin-sso") &&
3036
!location.pathname.startsWith("/signup") &&
3137
!location.pathname.startsWith("/signin")) {
3238
const rest = location.href.split(location.pathname)[1];
3339
const returnUrl = location.pathname + rest;
3440
sessionStorage.setItem("returnUrl", returnUrl);
35-
document.cookie = `returnUrl=${returnUrl}`; // for delegation
41+
Utils.setCookie("returnUrl", returnUrl); // for delegation
42+
}
43+
};
44+
45+
function initFeatures() {
46+
const logger = injector.resolve<Logger>("logger");
47+
const settingsProvider = injector.resolve<ISettingsProvider>("settingsProvider");
48+
checkIsFeatureEnabled(FEATURE_CLIENT_TELEMETRY, settingsProvider, logger)
49+
.then((isEnabled) => {
50+
logger.trackEvent("FeatureFlag", { feature: FEATURE_CLIENT_TELEMETRY, enabled: isEnabled.toString(), message: `Feature flag '${FEATURE_CLIENT_TELEMETRY}' - enabled` });
51+
let telemetryConfigurator = new TelemetryConfigurator(injector);
52+
if (isEnabled) {
53+
telemetryConfigurator.configure();
54+
} else {
55+
telemetryConfigurator.cleanUp();
56+
}
57+
});
58+
}
59+
60+
async function checkIsFeatureEnabled(featureFlagName: string, settingsProvider: ISettingsProvider, logger: Logger): Promise<boolean> {
61+
try {
62+
const settingsObject = await settingsProvider.getSetting(FEATURE_FLAGS);
63+
64+
const featureFlags = new Map(Object.entries(settingsObject ?? {}));
65+
if (!featureFlags || !featureFlags.has(featureFlagName)) {
66+
return false;
67+
}
68+
69+
return featureFlags.get(featureFlagName) == true;
70+
} catch (error) {
71+
logger?.trackEvent("FeatureFlag", { message: "Feature flag check failed", data: error.message });
72+
return false;
3673
}
37-
};
74+
}

src/telemetry/serviceWorker.ts

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
import { Bag } from "@paperbits/common/bag";
2+
declare const clients: any;
3+
4+
const allowedList = ["state", "session_state"];
5+
6+
function sendMessageToClients(message: Bag<string>): void {
7+
clients.matchAll().then((items: any[]) => {
8+
if (items.length > 0) {
9+
items.forEach(client => client.postMessage(message));
10+
}
11+
});
12+
}
13+
14+
addEventListener("fetch", (event: FetchEvent) => {
15+
const request = event.request;
16+
17+
event.respondWith(
18+
(async () => {
19+
const response = await fetch(request);
20+
21+
if (request.url.endsWith("/trace")) {
22+
return response;
23+
}
24+
25+
const cleanedUrl = request.url.indexOf("#code=") > -1 ? cleanUpUrlParams(request) : request.url;
26+
27+
const telemetryData = {
28+
url: cleanedUrl,
29+
method: request.method.toUpperCase(),
30+
status: response.status.toString(),
31+
responseHeaders: ""
32+
};
33+
34+
const headers: { [key: string]: string } = {};
35+
response.headers.forEach((value, key) => {
36+
if (key.toLocaleLowerCase() === "authorization") {
37+
return;
38+
}
39+
headers[key] = value;
40+
});
41+
telemetryData.responseHeaders = JSON.stringify(headers);
42+
43+
sendMessageToClients(telemetryData);
44+
45+
return response;
46+
})()
47+
);
48+
});
49+
50+
console.log("Telemetry worker started.");
51+
52+
function cleanUpUrlParams(request: Request): string {
53+
const url = new URL(request.url);
54+
const hash = url.hash.substring(1); // Remove the leading '#'
55+
const params = new URLSearchParams(hash);
56+
57+
// Remove all parameters except those in the allowedList
58+
for (const key of params.keys()) {
59+
if (!allowedList.includes(key)) {
60+
// Replace the 'code' parameter value
61+
params.set(key, "xxxxxxxxxx");
62+
}
63+
}
64+
65+
url.hash = params.toString();
66+
return url.toString();
67+
}

0 commit comments

Comments
 (0)