Add authentication and assets
This commit is contained in:
66
src/services/Authentication.service.ts
Normal file
66
src/services/Authentication.service.ts
Normal file
@@ -0,0 +1,66 @@
|
||||
import LoggerFactory from './logger/LoggerFactory';
|
||||
import ILogger from './logger/LoggerInterface';
|
||||
import PlatformDetectionService from './PlatformDetection.service';
|
||||
import {AuthenticationResponse, PhenixWebSocket} from './net/websockets/PhenixWebSocket';
|
||||
import {PhenixWebSocketMessage} from './net/websockets/PhenixWebSocketMessage';
|
||||
|
||||
//TEMPORARY
|
||||
import config from '../config';
|
||||
|
||||
class AuthenticationService {
|
||||
private static readonly _instance = new AuthenticationService();
|
||||
private readonly _logger: ILogger = LoggerFactory.getLogger('AuthenticationService');
|
||||
private _phenixWebSocket: PhenixWebSocket;
|
||||
|
||||
public static getInstance(): AuthenticationService {
|
||||
return AuthenticationService._instance;
|
||||
}
|
||||
|
||||
constructor() {
|
||||
const backenUrl = 'wss://pcast-stg.phenixrts.com/ws';
|
||||
this._phenixWebSocket = new PhenixWebSocket(backenUrl);
|
||||
}
|
||||
|
||||
get sessionId(): string | null {
|
||||
return this._phenixWebSocket.sessionId;
|
||||
}
|
||||
|
||||
async authenticate(applicationId: string, secret: string): Promise<AuthenticationResponse> {
|
||||
const authenticate = {
|
||||
// @ts-expect-error TODO(AZ): phenix-web-proto does not have Typescript types defined definition
|
||||
apiVersion: this._phenixWebSocket.getApiVersion(), // TODO(AZ): add types for phenix-proto-web
|
||||
clientVersion: config.controlCenterVersion,
|
||||
deviceId: '',
|
||||
platform: PlatformDetectionService.platform,
|
||||
platformVersion: PlatformDetectionService.platformVersion,
|
||||
browser: PlatformDetectionService.browser,
|
||||
browserVersion: PlatformDetectionService.version,
|
||||
applicationId,
|
||||
authenticationToken: secret,
|
||||
sessionId: this.sessionId
|
||||
};
|
||||
|
||||
try {
|
||||
const authenticationResponse = await this._phenixWebSocket.sendMessage(PhenixWebSocketMessage.Authenticate, authenticate);
|
||||
|
||||
if (authenticationResponse.status === 'ok') {
|
||||
this._phenixWebSocket.sessionId = authenticationResponse.sessionId;
|
||||
}
|
||||
|
||||
return authenticationResponse;
|
||||
} catch (error) {
|
||||
this._logger.error('Authentication failed [%s]', error);
|
||||
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
public async signout(): Promise<void> {
|
||||
await this._phenixWebSocket.sendMessage(PhenixWebSocketMessage.Bye, {
|
||||
sessionId: this.sessionId,
|
||||
reason: 'signout'
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export default AuthenticationService.getInstance();
|
||||
165
src/services/PlatformDetection.service.ts
Normal file
165
src/services/PlatformDetection.service.ts
Normal file
@@ -0,0 +1,165 @@
|
||||
export default class PlatformDetectionService {
|
||||
private static _userAgent: string = navigator.userAgent;
|
||||
private static _areClientHintsSupported: boolean = 'userAgentData' in navigator;
|
||||
private static _platform: string = '?';
|
||||
private static _platformVersion: string = '?';
|
||||
private static _browser: string = 'Unknown';
|
||||
private static _version: string | number = '?';
|
||||
private static _isWebview: boolean = false;
|
||||
private static _initialized: boolean = false;
|
||||
|
||||
private constructor() {
|
||||
throw new Error('PlatformDetectionService is a static class that may not be instantiated');
|
||||
}
|
||||
|
||||
static get platform(): string {
|
||||
this.initializeIfNeeded();
|
||||
return this._platform;
|
||||
}
|
||||
|
||||
static get platformVersion(): string {
|
||||
this.initializeIfNeeded();
|
||||
return this._platformVersion;
|
||||
}
|
||||
|
||||
static get userAgent(): string {
|
||||
return this._userAgent;
|
||||
}
|
||||
|
||||
static get browser(): string {
|
||||
this.initializeIfNeeded();
|
||||
return this._browser;
|
||||
}
|
||||
|
||||
static get version(): string | number {
|
||||
this.initializeIfNeeded();
|
||||
return this._version;
|
||||
}
|
||||
|
||||
static get isWebview(): boolean {
|
||||
this.initializeIfNeeded();
|
||||
return this._isWebview;
|
||||
}
|
||||
|
||||
static get areClientHintsSupported(): boolean {
|
||||
return this._areClientHintsSupported;
|
||||
}
|
||||
|
||||
private static initializeIfNeeded(): void {
|
||||
if (this._initialized) return;
|
||||
this.initialize();
|
||||
}
|
||||
|
||||
private static initialize(): void {
|
||||
try {
|
||||
const browserVersionMatch = this._userAgent.match(/(Chrome|Chromium|Firefox|Opera|Safari|Edge|OPR)\/([0-9]+)/);
|
||||
|
||||
if (browserVersionMatch) {
|
||||
const [, browser, version] = browserVersionMatch;
|
||||
PlatformDetectionService._browser = browser === 'OPR' ? 'Opera' : browser;
|
||||
PlatformDetectionService._version = parseInt(version, 10)?.toString() || '?';
|
||||
} else if (this._userAgent.match(/^\(?Mozilla/)) {
|
||||
PlatformDetectionService._browser = 'Mozilla';
|
||||
|
||||
// Check for IE/Edge
|
||||
if (this._userAgent.match(/MSIE/) || this._userAgent.match(/; Trident\/.*rv:[0-9]+/)) {
|
||||
PlatformDetectionService._browser = 'IE';
|
||||
const ieVersionMatch = this._userAgent.match(/rv:([0-9]+)/);
|
||||
|
||||
if (ieVersionMatch) {
|
||||
PlatformDetectionService._version = parseInt(ieVersionMatch[1], 10)?.toString() || '?';
|
||||
}
|
||||
} else if (this._userAgent.match(/Edge\//)) {
|
||||
PlatformDetectionService._browser = 'Edge';
|
||||
const edgeVersionMatch = this._userAgent.match(/Edge\/([0-9]+)/);
|
||||
|
||||
if (edgeVersionMatch) {
|
||||
PlatformDetectionService._version = parseInt(edgeVersionMatch[1], 10)?.toString() || '?';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Handle Opera masquerading as other browsers
|
||||
if (this._userAgent.match(/OPR\//)) {
|
||||
PlatformDetectionService._browser = 'Opera';
|
||||
const operaVersionMatch = this._userAgent.match(/OPR\/([0-9]+)/);
|
||||
if (operaVersionMatch) {
|
||||
PlatformDetectionService._version = parseInt(operaVersionMatch[1], 10)?.toString() || '?';
|
||||
}
|
||||
}
|
||||
|
||||
// Safari and iOS webviews
|
||||
if (this._userAgent.match(/AppleWebKit/i)) {
|
||||
if (this._userAgent.match(/iphone|ipod|ipad/i)) {
|
||||
PlatformDetectionService._browser = 'Safari';
|
||||
PlatformDetectionService._isWebview = true;
|
||||
const iosVersionMatch = this._userAgent.match(/OS\s([0-9]+)/);
|
||||
if (iosVersionMatch) {
|
||||
PlatformDetectionService._version = parseInt(iosVersionMatch[1], 10)?.toString() || '?';
|
||||
}
|
||||
} else if (this._userAgent.match(/Safari\//) && !this._userAgent.match(/Chrome/)) {
|
||||
PlatformDetectionService._browser = 'Safari';
|
||||
const safariVersionMatch = this._userAgent.match(/Version\/([0-9]+)/);
|
||||
if (safariVersionMatch) {
|
||||
PlatformDetectionService._version = parseInt(safariVersionMatch[1], 10)?.toString() || '?';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Android webviews
|
||||
if (this._userAgent.match(/; wv/) || (this._userAgent.match(/Android/) && this._userAgent.match(/Version\/[0-9].[0-9]/))) {
|
||||
PlatformDetectionService._isWebview = true;
|
||||
}
|
||||
|
||||
// React Native
|
||||
if (globalThis.navigator.product === 'ReactNative') {
|
||||
PlatformDetectionService._browser = 'ReactNative';
|
||||
PlatformDetectionService._version = navigator.productSub || '?';
|
||||
}
|
||||
|
||||
// platform information
|
||||
if (this._userAgent.match(/Windows/)) {
|
||||
PlatformDetectionService._platform = 'Windows';
|
||||
const windowsVersionMatch = this._userAgent.match(/Windows NT ([0-9.]+)/);
|
||||
|
||||
if (windowsVersionMatch) {
|
||||
PlatformDetectionService._platformVersion = windowsVersionMatch[1];
|
||||
}
|
||||
} else if (this._userAgent.match(/Mac OS X/)) {
|
||||
PlatformDetectionService._platform = 'macOS';
|
||||
const macVersionMatch = this._userAgent.match(/Mac OS X ([0-9._]+)/);
|
||||
|
||||
if (macVersionMatch) {
|
||||
PlatformDetectionService._platformVersion = macVersionMatch[1].replace(/_/g, '.');
|
||||
}
|
||||
} else if (this._userAgent.match(/Linux/)) {
|
||||
PlatformDetectionService._platform = 'Linux';
|
||||
} else if (this._userAgent.match(/Android/)) {
|
||||
PlatformDetectionService._platform = 'Android';
|
||||
const androidVersionMatch = this._userAgent.match(/Android ([0-9.]+)/);
|
||||
|
||||
if (androidVersionMatch) {
|
||||
PlatformDetectionService._platformVersion = androidVersionMatch[1];
|
||||
}
|
||||
} else if (this._userAgent.match(/iPhone|iPad|iPod/)) {
|
||||
PlatformDetectionService._platform = 'iOS';
|
||||
const iosVersionMatch = this._userAgent.match(/OS ([0-9_]+)/);
|
||||
|
||||
if (iosVersionMatch) {
|
||||
PlatformDetectionService._platformVersion = iosVersionMatch[1].replace(/_/g, '.');
|
||||
}
|
||||
}
|
||||
|
||||
this._initialized = true;
|
||||
} catch (error) {
|
||||
console.warn('Failed to initialize PlatformDetectionService:', error);
|
||||
// fallback values
|
||||
this._browser = 'Unknown';
|
||||
this._version = '?';
|
||||
this._platform = '?';
|
||||
this._platformVersion = '?';
|
||||
this._isWebview = false;
|
||||
this._initialized = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
26
src/services/logger/Appenders.ts
Normal file
26
src/services/logger/Appenders.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
/**
|
||||
* Copyright 2024 Phenix Real Time Solutions, Inc. Confidential and Proprietary. All Rights Reserved.
|
||||
*/
|
||||
import {IAppender} from './IAppender';
|
||||
|
||||
export default class Appenders {
|
||||
private _appenders: Array<IAppender> = [];
|
||||
|
||||
get value(): Array<IAppender> {
|
||||
return this._appenders;
|
||||
}
|
||||
|
||||
add(appender: IAppender): void {
|
||||
this._appenders.push(appender);
|
||||
}
|
||||
|
||||
remove(appender): void {
|
||||
this._appenders = this._appenders.reduce((items, item) => {
|
||||
if (!(item === appender)) {
|
||||
items.push(item);
|
||||
}
|
||||
|
||||
return items;
|
||||
}, [] as Array<IAppender>);
|
||||
}
|
||||
}
|
||||
29
src/services/logger/ConsoleAppender.ts
Normal file
29
src/services/logger/ConsoleAppender.ts
Normal file
@@ -0,0 +1,29 @@
|
||||
/**
|
||||
* Copyright 2024 Phenix Real Time Solutions, Inc. Confidential and Proprietary. All Rights Reserved.
|
||||
*/
|
||||
import {IAppender} from './IAppender';
|
||||
import {LoggingLevel} from './Logger';
|
||||
|
||||
export default class ConsoleAppender implements IAppender {
|
||||
private readonly _threshold: LoggingLevel;
|
||||
|
||||
log(logLevel: LoggingLevel, message: string, category: string, date: Date): void {
|
||||
if (logLevel < this._threshold) {
|
||||
return;
|
||||
}
|
||||
|
||||
const fullMessage = `${date.toISOString()} [${category}] [${LoggingLevel[logLevel]}] ${message}`;
|
||||
|
||||
if (logLevel < LoggingLevel.Warn) {
|
||||
console.log(fullMessage);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
console.error(fullMessage);
|
||||
}
|
||||
|
||||
constructor(threshold: LoggingLevel) {
|
||||
this._threshold = threshold;
|
||||
}
|
||||
}
|
||||
8
src/services/logger/IAppender.ts
Normal file
8
src/services/logger/IAppender.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
/**
|
||||
* Copyright 2024 Phenix Real Time Solutions, Inc. Confidential and Proprietary. All Rights Reserved.
|
||||
*/
|
||||
import {LoggingLevel} from './Logger';
|
||||
|
||||
export interface IAppender {
|
||||
log: (logLevel: LoggingLevel, message: string, category: string, date: Date) => void;
|
||||
}
|
||||
195
src/services/logger/Logger.ts
Normal file
195
src/services/logger/Logger.ts
Normal file
@@ -0,0 +1,195 @@
|
||||
/**
|
||||
* Copyright 2024 Phenix Real Time Solutions, Inc. Confidential and Proprietary. All Rights Reserved.
|
||||
*/
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
import {IAppender} from './IAppender';
|
||||
import Appenders from './Appenders';
|
||||
import LoggingThreshold from './LoggingThreshold';
|
||||
|
||||
export enum LoggingLevel {
|
||||
All = -1,
|
||||
Trace = 10,
|
||||
Debug = 20,
|
||||
Info = 30,
|
||||
Warn = 40,
|
||||
Error = 50,
|
||||
Fatal = 60,
|
||||
Off = 100
|
||||
}
|
||||
|
||||
export type LoggingLevelType = 'Off' | 'Trace' | 'Debug' | 'Info' | 'Warn' | 'Error' | 'Fatal' | 'All';
|
||||
|
||||
export default class Logger {
|
||||
private readonly _category: string;
|
||||
private readonly _appenders: Appenders;
|
||||
private readonly _threshold: LoggingThreshold;
|
||||
|
||||
get category(): string {
|
||||
return this._category;
|
||||
}
|
||||
|
||||
get appenders(): Appenders {
|
||||
return this._appenders;
|
||||
}
|
||||
|
||||
get threshold(): LoggingThreshold {
|
||||
return this._threshold;
|
||||
}
|
||||
|
||||
trace(...args: any): void {
|
||||
if (!this._threshold.value || this._threshold.value > LoggingLevel.Trace) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.log(LoggingLevel.Trace, args);
|
||||
}
|
||||
|
||||
debug(...args: any): void {
|
||||
if (!this._threshold.value || this._threshold.value > LoggingLevel.Debug) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.log(LoggingLevel.Debug, args);
|
||||
}
|
||||
|
||||
info(...args: any): void {
|
||||
if (!this._threshold.value || this._threshold.value > LoggingLevel.Info) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.log(LoggingLevel.Info, args);
|
||||
}
|
||||
|
||||
warn(...args: any): void {
|
||||
if (!this._threshold.value || this._threshold.value > LoggingLevel.Warn) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.log(LoggingLevel.Warn, args);
|
||||
}
|
||||
|
||||
error(...args: any): void {
|
||||
if (!this._threshold.value || this._threshold.value > LoggingLevel.Error) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.log(LoggingLevel.Error, args);
|
||||
}
|
||||
|
||||
fatal(...args: any): void {
|
||||
if (!this._threshold.value || this._threshold.value > LoggingLevel.Fatal) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.log(LoggingLevel.Fatal, args);
|
||||
}
|
||||
|
||||
private log(level: number, args: any): void {
|
||||
const date = new Date();
|
||||
const message = this.replacePlaceholders(args);
|
||||
|
||||
this._appenders.value.forEach((appender: IAppender) => {
|
||||
appender.log(level, message, this.category, date);
|
||||
});
|
||||
}
|
||||
|
||||
private replacePlaceholders(args: any): string {
|
||||
let replacePlaceholdersString = args[0];
|
||||
let index = 0;
|
||||
|
||||
while (replacePlaceholdersString.indexOf && args.length > 1 && index >= 0) {
|
||||
index = replacePlaceholdersString.indexOf('%', index);
|
||||
|
||||
if (index > 0) {
|
||||
const type = replacePlaceholdersString.substring(index + 1, index + 2);
|
||||
|
||||
switch (type) {
|
||||
case '%':
|
||||
// Escaped '%%' turns into '%'
|
||||
replacePlaceholdersString = replacePlaceholdersString.substring(0, index) + replacePlaceholdersString.substring(index + 1);
|
||||
index++;
|
||||
|
||||
break;
|
||||
case 's':
|
||||
case 'd':
|
||||
// Replace '%d' or '%s' with the argument
|
||||
args[0] = replacePlaceholdersString = this.replaceArgument(this.toString(args[1]), replacePlaceholdersString, index);
|
||||
args.splice(1, 1);
|
||||
|
||||
break;
|
||||
case 'j':
|
||||
// Replace %j' with the argument
|
||||
args[0] = replacePlaceholdersString = this.replaceArgument(this.stringify(args[1]), replacePlaceholdersString, index);
|
||||
|
||||
args.splice(1, 1);
|
||||
|
||||
break;
|
||||
default:
|
||||
return args.toString();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (args.length > 1) {
|
||||
args = args.reduce((accumulator, currentValue, index, array) => {
|
||||
if (index + 1 === array.length && currentValue instanceof Error) {
|
||||
return accumulator + '\n' + this.toString(currentValue.stack);
|
||||
}
|
||||
|
||||
return accumulator + `[${this.toString(currentValue)}]`;
|
||||
});
|
||||
}
|
||||
|
||||
return args.toString();
|
||||
}
|
||||
|
||||
private stringify(arg: any): string {
|
||||
try {
|
||||
return JSON.stringify(arg instanceof Error ? this.toString(arg) : arg, null, 2);
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
} catch (e) {
|
||||
return '[object invalid JSON.stringify]';
|
||||
}
|
||||
}
|
||||
|
||||
private replaceArgument(argument: any, replacePlaceholdersString: string, index: number): string {
|
||||
return replacePlaceholdersString.substring(0, index) + this.toString(argument) + replacePlaceholdersString.substring(index + 2);
|
||||
}
|
||||
|
||||
private toString(data: any): string {
|
||||
if (typeof data === 'string') {
|
||||
return data;
|
||||
}
|
||||
|
||||
if (typeof data === 'boolean') {
|
||||
return data ? 'true' : 'false';
|
||||
}
|
||||
|
||||
if (typeof data === 'number') {
|
||||
return data.toString();
|
||||
}
|
||||
|
||||
let toStringStr = '';
|
||||
|
||||
if (data) {
|
||||
if (typeof data === 'function') {
|
||||
toStringStr = data.toString();
|
||||
} else if (data instanceof Object) {
|
||||
try {
|
||||
toStringStr = data.toString();
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
} catch (e) {
|
||||
toStringStr = '[object invalid toString()]';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return toStringStr;
|
||||
}
|
||||
|
||||
constructor(category: string, appenders: Appenders, threshold: LoggingThreshold) {
|
||||
this._category = category;
|
||||
this._appenders = appenders;
|
||||
this._threshold = threshold;
|
||||
}
|
||||
}
|
||||
51
src/services/logger/LoggerDefaults.ts
Normal file
51
src/services/logger/LoggerDefaults.ts
Normal file
@@ -0,0 +1,51 @@
|
||||
/**
|
||||
* Copyright 2024 Phenix Real Time Solutions, Inc. Confidential and Proprietary. All Rights Reserved.
|
||||
*/
|
||||
import {LoggingLevel, LoggingLevelType} from '../logger/Logger';
|
||||
|
||||
declare const __FEATURES__: {
|
||||
sendLogs: LoggingLevelType;
|
||||
logToConsole: LoggingLevelType;
|
||||
};
|
||||
|
||||
export class BuildFeatures {
|
||||
private static _sendLogs: LoggingLevelType;
|
||||
private static _logToConsole: LoggingLevelType;
|
||||
|
||||
static get sendLogs(): LoggingLevelType {
|
||||
return this._sendLogs;
|
||||
}
|
||||
|
||||
static get logToConsole(): LoggingLevelType {
|
||||
return this._logToConsole;
|
||||
}
|
||||
|
||||
static applyFeatures(): void {
|
||||
try {
|
||||
const features = __FEATURES__;
|
||||
|
||||
this._sendLogs = 'sendLogs' in features ? features.sendLogs : 'All';
|
||||
this._logToConsole = 'logToConsole' in features ? features.logToConsole : 'All';
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
} catch (e) {
|
||||
this._sendLogs = 'All';
|
||||
this._logToConsole = 'All';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
BuildFeatures.applyFeatures();
|
||||
|
||||
export default class LoggerDefaults {
|
||||
static get defaultLoggingLevel(): LoggingLevel {
|
||||
return LoggingLevel[BuildFeatures.sendLogs];
|
||||
}
|
||||
|
||||
static get defaultConsoleLoggingLevel(): LoggingLevel {
|
||||
return LoggingLevel[BuildFeatures.logToConsole];
|
||||
}
|
||||
|
||||
static get defaultTelemetryLoggingLevel(): LoggingLevel {
|
||||
return LoggingLevel.Info;
|
||||
}
|
||||
}
|
||||
78
src/services/logger/LoggerFactory.ts
Normal file
78
src/services/logger/LoggerFactory.ts
Normal file
@@ -0,0 +1,78 @@
|
||||
/**
|
||||
* Copyright 2024 Phenix Real Time Solutions, Inc. Confidential and Proprietary. All Rights Reserved.
|
||||
*/
|
||||
// import TelemetryUrl from '/telemetry/TelemetryUrl';
|
||||
import PlatformDetectionService from '../PlatformDetection.service';
|
||||
import TelemetryConfiguration from '../telemetry/TelemetryConfiguration';
|
||||
import TelemetryAppender from '../telemetry/TelemetryApender';
|
||||
// import hostService from 'services/host-url.service';
|
||||
// import userStore from 'services/user-store';
|
||||
import ILogger from './LoggerInterface';
|
||||
import Logger, {LoggingLevel} from './Logger';
|
||||
import Appenders from './Appenders';
|
||||
import LoggingThreshold from './LoggingThreshold';
|
||||
import ConsoleAppender from './ConsoleAppender';
|
||||
import LoggerDefaults from './LoggerDefaults';
|
||||
|
||||
export default class LoggerFactory {
|
||||
private static _loggers: {[category: string]: ILogger} = {};
|
||||
private static _appenders: Appenders = new Appenders();
|
||||
private static _threshold: LoggingThreshold = new LoggingThreshold();
|
||||
private static _telemetryConfiguration: TelemetryConfiguration = new TelemetryConfiguration();
|
||||
|
||||
static get telemetryConfiguration(): TelemetryConfiguration {
|
||||
return this._telemetryConfiguration;
|
||||
}
|
||||
|
||||
static applyLoggerConfig(): void {
|
||||
LoggerFactory.applyConsoleLogger(LoggingLevel['All']);
|
||||
LoggerFactory.applyLoggingLevel();
|
||||
LoggerFactory.applyTelemetryLogger();
|
||||
}
|
||||
|
||||
static getLogger(category: string): ILogger {
|
||||
if (typeof category !== 'string') {
|
||||
category = 'portal';
|
||||
}
|
||||
|
||||
const logger = LoggerFactory._loggers[category];
|
||||
|
||||
if (logger) {
|
||||
return logger;
|
||||
}
|
||||
|
||||
return (LoggerFactory._loggers[category] = new Logger(category, this._appenders, this._threshold));
|
||||
}
|
||||
|
||||
static applyLoggingLevel(): void {
|
||||
this._threshold.setThreshold(LoggingLevel['All']);
|
||||
}
|
||||
|
||||
static applyConsoleLogger(level: LoggingLevel): void {
|
||||
this._appenders.add(new ConsoleAppender(level || LoggerDefaults.defaultConsoleLoggingLevel));
|
||||
}
|
||||
|
||||
static async applyTelemetryConfiguration(level: LoggingLevel): Promise<void> {
|
||||
const browser = PlatformDetectionService.browser;
|
||||
const applicationId = 'phenixrts.com-alex.zinn'; // TEMPORARY --> FOR DEVELOPMENT ONLY
|
||||
this._telemetryConfiguration.threshold = level || LoggerDefaults.defaultTelemetryLoggingLevel;
|
||||
this._telemetryConfiguration.url = 'https://pcast-stg.phenixrts.com/telemetry'; //TelemetryUrl.getTelemetryUrl();
|
||||
this._telemetryConfiguration.environment = 'https://pcast-stg.phenixrts.com'; // TODO(AZ): hostService.getHostUrl();
|
||||
this._telemetryConfiguration.tenancy = applicationId; // TODO(AZ): await userStore.get('applicationId');
|
||||
this._telemetryConfiguration.userId = applicationId;
|
||||
this._telemetryConfiguration.sessionId = 'some-session-id'; // TODOD(AZ): await userStore.get('sessionId');
|
||||
this._telemetryConfiguration.browser = browser ? `${browser}/${PlatformDetectionService.version}` : 'unknown';
|
||||
}
|
||||
|
||||
private static applyTelemetryLogger(): void {
|
||||
LoggerFactory.applyTelemetryConfiguration(LoggingLevel['Info']);
|
||||
|
||||
this._appenders.add(new TelemetryAppender(this._telemetryConfiguration));
|
||||
}
|
||||
|
||||
private constructor() {
|
||||
throw new Error('LoggerFactory is a static class that may not be instantiated');
|
||||
}
|
||||
}
|
||||
|
||||
LoggerFactory.applyLoggerConfig();
|
||||
27
src/services/logger/LoggerInterface.ts
Normal file
27
src/services/logger/LoggerInterface.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
/**
|
||||
* Copyright 2024 Phenix Real Time Solutions, Inc. Confidential and Proprietary. All Rights Reserved.
|
||||
*/
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
import Appenders from './Appenders';
|
||||
import LoggingThreshold from './LoggingThreshold';
|
||||
|
||||
export default interface ILogger {
|
||||
readonly category: string;
|
||||
|
||||
readonly appenders: Appenders;
|
||||
|
||||
readonly threshold: LoggingThreshold;
|
||||
|
||||
trace: (...args: any) => void;
|
||||
|
||||
debug: (...args: any) => void;
|
||||
|
||||
info: (...args: any) => void;
|
||||
|
||||
warn: (...args: any) => void;
|
||||
|
||||
error: (...args: any) => void;
|
||||
|
||||
fatal: (...args: any) => void;
|
||||
}
|
||||
/* eslint-enable */
|
||||
56
src/services/logger/LoggingLevelMapping.ts
Normal file
56
src/services/logger/LoggingLevelMapping.ts
Normal file
@@ -0,0 +1,56 @@
|
||||
/**
|
||||
* Copyright 2024 Phenix Real Time Solutions, Inc. Confidential and Proprietary. All Rights Reserved.
|
||||
*/
|
||||
import {LoggingLevel, LoggingLevelType} from './Logger';
|
||||
|
||||
function assertUnreachable(x: never): never {
|
||||
throw new Error(`Unexpected value [${x}]. This should never be reached`);
|
||||
}
|
||||
|
||||
export default class LoggingLevelMapping {
|
||||
static convertLoggingLevelToLoggingLevelType(loggingLevel: LoggingLevel): LoggingLevelType {
|
||||
switch (loggingLevel) {
|
||||
case LoggingLevel.Off:
|
||||
return 'Off';
|
||||
case LoggingLevel.Trace:
|
||||
return 'Trace';
|
||||
case LoggingLevel.Debug:
|
||||
return 'Debug';
|
||||
case LoggingLevel.Info:
|
||||
return 'Trace';
|
||||
case LoggingLevel.Warn:
|
||||
return 'Warn';
|
||||
case LoggingLevel.Error:
|
||||
return 'Error';
|
||||
case LoggingLevel.Fatal:
|
||||
return 'Fatal';
|
||||
case LoggingLevel.All:
|
||||
return 'All';
|
||||
default:
|
||||
assertUnreachable(loggingLevel);
|
||||
}
|
||||
}
|
||||
|
||||
static convertLoggingLevelTypeToLoggingLevel(loggingLevelType: LoggingLevelType): LoggingLevel {
|
||||
switch (loggingLevelType) {
|
||||
case 'Off':
|
||||
return LoggingLevel.Off;
|
||||
case 'Trace':
|
||||
return LoggingLevel.Trace;
|
||||
case 'Debug':
|
||||
return LoggingLevel.Debug;
|
||||
case 'Info':
|
||||
return LoggingLevel.Info;
|
||||
case 'Warn':
|
||||
return LoggingLevel.Warn;
|
||||
case 'Error':
|
||||
return LoggingLevel.Error;
|
||||
case 'Fatal':
|
||||
return LoggingLevel.Fatal;
|
||||
case 'All':
|
||||
return LoggingLevel.All;
|
||||
default:
|
||||
assertUnreachable(loggingLevelType);
|
||||
}
|
||||
}
|
||||
}
|
||||
17
src/services/logger/LoggingThreshold.ts
Normal file
17
src/services/logger/LoggingThreshold.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
/**
|
||||
* Copyright 2024 Phenix Real Time Solutions, Inc. Confidential and Proprietary. All Rights Reserved.
|
||||
*/
|
||||
import LoggerDefaults from './LoggerDefaults';
|
||||
import {LoggingLevel} from './Logger';
|
||||
|
||||
export default class LoggingThreshold {
|
||||
private _threshold: LoggingLevel = LoggerDefaults.defaultLoggingLevel;
|
||||
|
||||
get value(): LoggingLevel {
|
||||
return this._threshold;
|
||||
}
|
||||
|
||||
setThreshold(threshold: LoggingLevel): void {
|
||||
this._threshold = threshold;
|
||||
}
|
||||
}
|
||||
115
src/services/net/websockets/PhenixWebSocket.ts
Normal file
115
src/services/net/websockets/PhenixWebSocket.ts
Normal file
@@ -0,0 +1,115 @@
|
||||
import Strings from 'lang/Strings';
|
||||
import ILogger from '../../logger/LoggerInterface';
|
||||
import LoggerFactory from '../../logger/LoggerFactory';
|
||||
import {Subject, ReadOnlySubject} from '@techniker-me/tools';
|
||||
import {MQWebSocket} from 'phenix-web-proto'; //TODO(AZ): add types
|
||||
import PCastProtobuf from './proto/pcast.proto.json' with {type: 'json'};
|
||||
import AnalyticsProtobuf from './proto/Analytics.proto.json' with {type: 'json'};
|
||||
import {PhenixWebSocketStatus, PhenixWebSocketStatusMapping} from './PhenixWebSocketStatus';
|
||||
import {PhenixWebSocketMessage, PhenixWebSocketMessageMapping} from './PhenixWebSocketMessage';
|
||||
|
||||
export type AuthenticationResponse = {
|
||||
status: string;
|
||||
applicationId?: string;
|
||||
sessionId?: string;
|
||||
};
|
||||
|
||||
export interface IPhenixWebSocketResponse {
|
||||
status: 'ok';
|
||||
applicationId: string;
|
||||
sessionId: string;
|
||||
redirect: string;
|
||||
roles: string[];
|
||||
}
|
||||
|
||||
export class PhenixWebSocket extends MQWebSocket {
|
||||
private readonly _logger: ILogger;
|
||||
private readonly _status: Subject<PhenixWebSocketStatus> = new Subject<PhenixWebSocketStatus>(PhenixWebSocketStatus.Offline);
|
||||
private readonly _readOnlySubject: ReadOnlySubject<PhenixWebSocketStatus> = new ReadOnlySubject<PhenixWebSocketStatus>(this._status);
|
||||
private readonly _socketId: string = Strings.randomString(10);
|
||||
private _sessionId: string | null = null;
|
||||
private _pendingRequests: number = 0;
|
||||
|
||||
constructor(url: string) {
|
||||
const logger = LoggerFactory.getLogger('PhenixWebSocket');
|
||||
super(url, logger, [PCastProtobuf, AnalyticsProtobuf]);
|
||||
|
||||
this._logger = logger;
|
||||
this.initialize();
|
||||
}
|
||||
|
||||
get status(): ReadOnlySubject<PhenixWebSocketStatus> {
|
||||
return this._readOnlySubject;
|
||||
}
|
||||
|
||||
get pendingRequests(): number {
|
||||
return this._pendingRequests;
|
||||
}
|
||||
|
||||
get socketId(): string {
|
||||
return this._socketId;
|
||||
}
|
||||
|
||||
get sessionId(): string | null {
|
||||
return this._sessionId;
|
||||
}
|
||||
|
||||
set sessionId(sessionId: string | null) {
|
||||
this._sessionId = sessionId;
|
||||
}
|
||||
|
||||
public async sendMessage<T>(kind: PhenixWebSocketMessage, message: T): Promise<IPhenixWebSocketResponse> {
|
||||
if (this._status.value !== PhenixWebSocketStatus.Online) {
|
||||
throw new Error(`Unable to send message, web socket is not [Online] WebSocket status [${PhenixWebSocketStatusMapping.convertPhenixWebSocketStatusToPhenixWebSocketStatusType(this._status.value)}]`);
|
||||
}
|
||||
|
||||
this._pendingRequests++;
|
||||
|
||||
const messageKind = PhenixWebSocketMessageMapping.convertPhenixWebSocketMessageToPhenixWebSocketMessageType(kind);
|
||||
|
||||
this._logger.debug(`Sending [${messageKind}] message [%j]`, message);
|
||||
|
||||
return new Promise<IPhenixWebSocketResponse>((resolve, reject) => {
|
||||
super.sendRequest(messageKind, message, (error: unknown, response: IPhenixWebSocketResponse) => {
|
||||
this._pendingRequests--;
|
||||
|
||||
if (error) {
|
||||
reject(error);
|
||||
} else {
|
||||
resolve(response);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
private setStatus(status: PhenixWebSocketStatus): void {
|
||||
this._status.value = status;
|
||||
}
|
||||
|
||||
private initialize(): void {
|
||||
super.onEvent('connected', () => {
|
||||
this.setStatus(PhenixWebSocketStatus.Online);
|
||||
})
|
||||
|
||||
super.onEvent('disconnected', () => {
|
||||
this.setStatus(PhenixWebSocketStatus.Offline);
|
||||
})
|
||||
|
||||
super.onEvent('error', (error: unknown) => {
|
||||
this._logger.error('Error [%s]', error);
|
||||
this.setStatus(PhenixWebSocketStatus.Error);
|
||||
})
|
||||
|
||||
super.onEvent('reconnecting', () => {
|
||||
this.setStatus(PhenixWebSocketStatus.Reconnecting);
|
||||
})
|
||||
|
||||
super.onEvent('reconnected', () => {
|
||||
this.setStatus(PhenixWebSocketStatus.Online);
|
||||
})
|
||||
|
||||
super.onEvent('timeout', () => {
|
||||
this.setStatus(PhenixWebSocketStatus.Error);
|
||||
})
|
||||
}
|
||||
}
|
||||
33
src/services/net/websockets/PhenixWebSocketMessage.ts
Normal file
33
src/services/net/websockets/PhenixWebSocketMessage.ts
Normal file
@@ -0,0 +1,33 @@
|
||||
import assertUnreachable from 'lang/assertUnreachable';
|
||||
|
||||
export enum PhenixWebSocketMessage {
|
||||
Authenticate = 0,
|
||||
Bye = 1
|
||||
}
|
||||
|
||||
export type PhenixWebSocketMessageType = 'pcast.Authenticate' | 'pcast.Bye';
|
||||
|
||||
export class PhenixWebSocketMessageMapping {
|
||||
public static convertPhenixWebSocketMessageToPhenixWebSocketMessageType(message: PhenixWebSocketMessage): PhenixWebSocketMessageType {
|
||||
switch (message) {
|
||||
case PhenixWebSocketMessage.Authenticate:
|
||||
return 'pcast.Authenticate';
|
||||
case PhenixWebSocketMessage.Bye:
|
||||
return 'pcast.Bye';
|
||||
|
||||
default:
|
||||
assertUnreachable(message);
|
||||
}
|
||||
}
|
||||
|
||||
public static convertPhenixWebSocketMessageTypeToPhenixWebSocketMessage(messageType: PhenixWebSocketMessageType): PhenixWebSocketMessage {
|
||||
switch (messageType) {
|
||||
case 'pcast.Authenticate':
|
||||
return PhenixWebSocketMessage.Authenticate;
|
||||
case 'pcast.Bye':
|
||||
return PhenixWebSocketMessage.Bye;
|
||||
default:
|
||||
assertUnreachable(messageType);
|
||||
}
|
||||
}
|
||||
}
|
||||
44
src/services/net/websockets/PhenixWebSocketStatus.ts
Normal file
44
src/services/net/websockets/PhenixWebSocketStatus.ts
Normal file
@@ -0,0 +1,44 @@
|
||||
import assertUnreachable from '../../../lang/assertUnreachable';
|
||||
|
||||
export enum PhenixWebSocketStatus {
|
||||
Offline = 0,
|
||||
Online = 1,
|
||||
Reconnecting = 2,
|
||||
Error = 3
|
||||
}
|
||||
|
||||
export type PhenixWebSocketStatusType = 'Offline' | 'Online' | 'Reconnecting' | 'Error';
|
||||
|
||||
export class PhenixWebSocketStatusMapping {
|
||||
public static convertPhenixWebSocketStatusToPhenixWebSocketStatusType(status: PhenixWebSocketStatus): PhenixWebSocketStatusType {
|
||||
switch (status) {
|
||||
case PhenixWebSocketStatus.Offline:
|
||||
return 'Offline';
|
||||
case PhenixWebSocketStatus.Online:
|
||||
return 'Online';
|
||||
case PhenixWebSocketStatus.Reconnecting:
|
||||
return 'Reconnecting';
|
||||
case PhenixWebSocketStatus.Error:
|
||||
return 'Error';
|
||||
|
||||
default:
|
||||
assertUnreachable(status);
|
||||
}
|
||||
}
|
||||
|
||||
public static convertPhenixWebSocketStatusTypeToPhenixWebSocketStatus(statusType: PhenixWebSocketStatusType): PhenixWebSocketStatus {
|
||||
switch (statusType) {
|
||||
case 'Offline':
|
||||
return PhenixWebSocketStatus.Offline;
|
||||
case 'Online':
|
||||
return PhenixWebSocketStatus.Online;
|
||||
case 'Reconnecting':
|
||||
return PhenixWebSocketStatus.Reconnecting;
|
||||
case 'Error':
|
||||
return PhenixWebSocketStatus.Error;
|
||||
|
||||
default:
|
||||
assertUnreachable(statusType);
|
||||
}
|
||||
}
|
||||
}
|
||||
391
src/services/net/websockets/proto/analytics.proto.json
Normal file
391
src/services/net/websockets/proto/analytics.proto.json
Normal file
@@ -0,0 +1,391 @@
|
||||
{
|
||||
"package": "analytics",
|
||||
"messages": [
|
||||
{
|
||||
"name": "Usage",
|
||||
"fields": [
|
||||
{
|
||||
"rule": "required",
|
||||
"type": "uint64",
|
||||
"name": "streams",
|
||||
"id": 1
|
||||
},
|
||||
{
|
||||
"rule": "required",
|
||||
"type": "uint64",
|
||||
"name": "users",
|
||||
"id": 2
|
||||
},
|
||||
{
|
||||
"rule": "required",
|
||||
"type": "uint64",
|
||||
"name": "devices",
|
||||
"id": 3
|
||||
},
|
||||
{
|
||||
"rule": "required",
|
||||
"type": "uint64",
|
||||
"name": "minutes",
|
||||
"id": 4
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "UsageByType",
|
||||
"fields": [
|
||||
{
|
||||
"rule": "required",
|
||||
"type": "string",
|
||||
"name": "type",
|
||||
"id": 1
|
||||
},
|
||||
{
|
||||
"rule": "required",
|
||||
"type": "string",
|
||||
"name": "subtype",
|
||||
"id": 2
|
||||
},
|
||||
{
|
||||
"rule": "required",
|
||||
"type": "Usage",
|
||||
"name": "usage",
|
||||
"id": 3
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "UsageByCountry",
|
||||
"fields": [
|
||||
{
|
||||
"rule": "required",
|
||||
"type": "string",
|
||||
"name": "continent",
|
||||
"id": 1
|
||||
},
|
||||
{
|
||||
"rule": "required",
|
||||
"type": "string",
|
||||
"name": "country",
|
||||
"id": 2
|
||||
},
|
||||
{
|
||||
"rule": "repeated",
|
||||
"type": "UsageByType",
|
||||
"name": "usageByType",
|
||||
"id": 3
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "GetGeographicUsage",
|
||||
"fields": [
|
||||
{
|
||||
"rule": "repeated",
|
||||
"type": "string",
|
||||
"name": "applicationIds",
|
||||
"id": 1
|
||||
},
|
||||
{
|
||||
"rule": "required",
|
||||
"type": "uint64",
|
||||
"name": "start",
|
||||
"id": 2
|
||||
},
|
||||
{
|
||||
"rule": "required",
|
||||
"type": "uint64",
|
||||
"name": "end",
|
||||
"id": 3
|
||||
},
|
||||
{
|
||||
"rule": "optional",
|
||||
"type": "string",
|
||||
"name": "continuationId",
|
||||
"id": 4
|
||||
},
|
||||
{
|
||||
"rule": "optional",
|
||||
"type": "string",
|
||||
"name": "routeKey",
|
||||
"id": 5
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "GetGeographicUsageResponse",
|
||||
"fields": [
|
||||
{
|
||||
"rule": "required",
|
||||
"type": "string",
|
||||
"name": "status",
|
||||
"id": 1
|
||||
},
|
||||
{
|
||||
"rule": "optional",
|
||||
"type": "Usage",
|
||||
"name": "usage",
|
||||
"id": 2
|
||||
},
|
||||
{
|
||||
"rule": "repeated",
|
||||
"type": "UsageByType",
|
||||
"name": "usageByType",
|
||||
"id": 3
|
||||
},
|
||||
{
|
||||
"rule": "repeated",
|
||||
"type": "UsageByCountry",
|
||||
"name": "usageByCountry",
|
||||
"id": 4
|
||||
},
|
||||
{
|
||||
"rule": "optional",
|
||||
"type": "string",
|
||||
"name": "continuationId",
|
||||
"id": 5
|
||||
},
|
||||
{
|
||||
"rule": "optional",
|
||||
"type": "string",
|
||||
"name": "routeKey",
|
||||
"id": 6
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "CDF",
|
||||
"fields": [
|
||||
{
|
||||
"rule": "repeated",
|
||||
"type": "double",
|
||||
"name": "data",
|
||||
"id": 1
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "GetTimeToFirstFrameCDF",
|
||||
"fields": [
|
||||
{
|
||||
"rule": "repeated",
|
||||
"type": "string",
|
||||
"name": "applicationIds",
|
||||
"id": 1
|
||||
},
|
||||
{
|
||||
"rule": "required",
|
||||
"type": "uint64",
|
||||
"name": "start",
|
||||
"id": 2
|
||||
},
|
||||
{
|
||||
"rule": "required",
|
||||
"type": "uint64",
|
||||
"name": "end",
|
||||
"id": 3
|
||||
},
|
||||
{
|
||||
"rule": "required",
|
||||
"type": "Kind",
|
||||
"name": "kind",
|
||||
"id": 4
|
||||
},
|
||||
{
|
||||
"rule": "optional",
|
||||
"type": "string",
|
||||
"name": "continuationId",
|
||||
"id": 5
|
||||
},
|
||||
{
|
||||
"rule": "optional",
|
||||
"type": "string",
|
||||
"name": "routeKey",
|
||||
"id": 6
|
||||
}
|
||||
],
|
||||
"enums": [
|
||||
{
|
||||
"name": "Kind",
|
||||
"values": [
|
||||
{
|
||||
"name": "All",
|
||||
"id": 0
|
||||
},
|
||||
{
|
||||
"name": "RealTime",
|
||||
"id": 1
|
||||
},
|
||||
{
|
||||
"name": "Live",
|
||||
"id": 2
|
||||
},
|
||||
{
|
||||
"name": "Dash",
|
||||
"id": 3
|
||||
},
|
||||
{
|
||||
"name": "Hls",
|
||||
"id": 4
|
||||
},
|
||||
{
|
||||
"name": "PeerAssisted",
|
||||
"id": 5
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "GetTimeToFirstFrameCDFResponse",
|
||||
"fields": [
|
||||
{
|
||||
"rule": "required",
|
||||
"type": "string",
|
||||
"name": "status",
|
||||
"id": 1
|
||||
},
|
||||
{
|
||||
"rule": "optional",
|
||||
"type": "uint64",
|
||||
"name": "count",
|
||||
"id": 2
|
||||
},
|
||||
{
|
||||
"rule": "optional",
|
||||
"type": "double",
|
||||
"name": "average",
|
||||
"id": 3
|
||||
},
|
||||
{
|
||||
"rule": "optional",
|
||||
"type": "CDF",
|
||||
"name": "cdf",
|
||||
"id": 4
|
||||
},
|
||||
{
|
||||
"rule": "optional",
|
||||
"type": "string",
|
||||
"name": "continuationId",
|
||||
"id": 5
|
||||
},
|
||||
{
|
||||
"rule": "optional",
|
||||
"type": "string",
|
||||
"name": "routeKey",
|
||||
"id": 6
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "GetActiveUsers",
|
||||
"fields": [
|
||||
{
|
||||
"rule": "repeated",
|
||||
"type": "string",
|
||||
"name": "applicationIds",
|
||||
"id": 1
|
||||
},
|
||||
{
|
||||
"rule": "required",
|
||||
"type": "uint64",
|
||||
"name": "snapshotTime",
|
||||
"id": 2
|
||||
},
|
||||
{
|
||||
"rule": "optional",
|
||||
"type": "string",
|
||||
"name": "continuationId",
|
||||
"id": 3
|
||||
},
|
||||
{
|
||||
"rule": "optional",
|
||||
"type": "string",
|
||||
"name": "routeKey",
|
||||
"id": 4
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "UsersAndSessionsGrouped",
|
||||
"fields": [
|
||||
{
|
||||
"rule": "required",
|
||||
"type": "string",
|
||||
"name": "groupName",
|
||||
"id": 1
|
||||
},
|
||||
{
|
||||
"rule": "required",
|
||||
"type": "uint64",
|
||||
"name": "users",
|
||||
"id": 2
|
||||
},
|
||||
{
|
||||
"rule": "required",
|
||||
"type": "uint64",
|
||||
"name": "sessions",
|
||||
"id": 3
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "GetActiveUsersResponse",
|
||||
"fields": [
|
||||
{
|
||||
"rule": "required",
|
||||
"type": "string",
|
||||
"name": "status",
|
||||
"id": 1
|
||||
},
|
||||
{
|
||||
"rule": "optional",
|
||||
"type": "uint64",
|
||||
"name": "users",
|
||||
"id": 2
|
||||
},
|
||||
{
|
||||
"rule": "optional",
|
||||
"type": "uint64",
|
||||
"name": "sessions",
|
||||
"id": 3
|
||||
},
|
||||
{
|
||||
"rule": "repeated",
|
||||
"type": "UsersAndSessionsGrouped",
|
||||
"name": "byPlatform",
|
||||
"id": 4
|
||||
},
|
||||
{
|
||||
"rule": "repeated",
|
||||
"type": "UsersAndSessionsGrouped",
|
||||
"name": "byManufacturer",
|
||||
"id": 5
|
||||
},
|
||||
{
|
||||
"rule": "repeated",
|
||||
"type": "UsersAndSessionsGrouped",
|
||||
"name": "byCity",
|
||||
"id": 6
|
||||
},
|
||||
{
|
||||
"rule": "repeated",
|
||||
"type": "UsersAndSessionsGrouped",
|
||||
"name": "byCountry",
|
||||
"id": 7
|
||||
},
|
||||
{
|
||||
"rule": "optional",
|
||||
"type": "string",
|
||||
"name": "continuationId",
|
||||
"id": 8
|
||||
},
|
||||
{
|
||||
"rule": "optional",
|
||||
"type": "string",
|
||||
"name": "routeKey",
|
||||
"id": 9
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
1777
src/services/net/websockets/proto/pcast.proto.json
Normal file
1777
src/services/net/websockets/proto/pcast.proto.json
Normal file
File diff suppressed because it is too large
Load Diff
25
src/services/telemetry/TelemetryApender.ts
Normal file
25
src/services/telemetry/TelemetryApender.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
/**
|
||||
* Copyright 2024 Phenix Real Time Solutions, Inc. Confidential and Proprietary. All Rights Reserved.
|
||||
*/
|
||||
import TelemetryService from './TelemetryService';
|
||||
import TelemetryConfiguration from './TelemetryConfiguration';
|
||||
import {IAppender} from '../logger/IAppender';
|
||||
import {LoggingLevel} from '../logger/Logger';
|
||||
|
||||
export default class TelemetryAppender implements IAppender {
|
||||
private readonly _telemetryService: TelemetryService;
|
||||
private readonly _threshold: LoggingLevel;
|
||||
|
||||
constructor(telemetryConfiguration: TelemetryConfiguration) {
|
||||
this._threshold = telemetryConfiguration.threshold;
|
||||
this._telemetryService = new TelemetryService(telemetryConfiguration);
|
||||
}
|
||||
|
||||
async log(logLevel: LoggingLevel, message: string, category: string, date: Date): Promise<void> {
|
||||
if (logLevel < this._threshold) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._telemetryService.push(logLevel, message, category, date);
|
||||
}
|
||||
}
|
||||
75
src/services/telemetry/TelemetryConfiguration.ts
Normal file
75
src/services/telemetry/TelemetryConfiguration.ts
Normal file
@@ -0,0 +1,75 @@
|
||||
/**
|
||||
* Copyright 2024 Phenix Real Time Solutions, Inc. Confidential and Proprietary. All Rights Reserved.
|
||||
*/
|
||||
import {LoggingLevel} from '../logger/Logger';
|
||||
import LoggerDefaults from '../logger/LoggerDefaults';
|
||||
|
||||
export default class TelemetryConfiguration {
|
||||
private _url = 'https://telemetry.phenixrts.com/telemetry/logs';
|
||||
private _tenancy = '';
|
||||
private _userId = '';
|
||||
private _sessionId = '';
|
||||
private _environment = '';
|
||||
private _threshold = LoggerDefaults.defaultTelemetryLoggingLevel;
|
||||
private _browser = '';
|
||||
|
||||
get url(): string {
|
||||
return this._url;
|
||||
}
|
||||
|
||||
set url(url: string) {
|
||||
const telemetryUrl = new URL(url);
|
||||
|
||||
telemetryUrl.pathname = telemetryUrl.pathname + '/logs';
|
||||
|
||||
this._url = telemetryUrl.toString();
|
||||
}
|
||||
|
||||
get environment(): string {
|
||||
return this._environment;
|
||||
}
|
||||
|
||||
set environment(environment: string) {
|
||||
this._environment = environment;
|
||||
}
|
||||
|
||||
get browser(): string {
|
||||
return this._browser;
|
||||
}
|
||||
|
||||
set browser(browser: string) {
|
||||
this._browser = browser;
|
||||
}
|
||||
|
||||
get tenancy(): string {
|
||||
return this._tenancy;
|
||||
}
|
||||
|
||||
set tenancy(tenancy: string) {
|
||||
this._tenancy = tenancy;
|
||||
}
|
||||
|
||||
get userId(): string {
|
||||
return this._userId;
|
||||
}
|
||||
|
||||
set userId(userId: string) {
|
||||
this._userId = userId;
|
||||
}
|
||||
|
||||
get sessionId(): string {
|
||||
return this._sessionId;
|
||||
}
|
||||
|
||||
set sessionId(sessionId: string) {
|
||||
this._sessionId = sessionId;
|
||||
}
|
||||
|
||||
get threshold(): LoggingLevel {
|
||||
return this._threshold;
|
||||
}
|
||||
|
||||
set threshold(threshold: LoggingLevel) {
|
||||
this._threshold = threshold;
|
||||
}
|
||||
}
|
||||
131
src/services/telemetry/TelemetryService.ts
Normal file
131
src/services/telemetry/TelemetryService.ts
Normal file
@@ -0,0 +1,131 @@
|
||||
/**
|
||||
* Copyright 2024 Phenix Real Time Solutions, Inc. Confidential and Proprietary. All Rights Reserved.
|
||||
*/
|
||||
import config from '../../config';
|
||||
import {LoggingLevel} from '../logger/Logger';
|
||||
import TelemetryConfiguration from './TelemetryConfiguration';
|
||||
|
||||
// Extend Window interface to include custom properties
|
||||
declare global {
|
||||
interface Window {
|
||||
__phenixPageLoadTime?: number;
|
||||
__pageLoadTime?: number;
|
||||
}
|
||||
}
|
||||
|
||||
const requestSizeLimit = 8192;
|
||||
const pageLoadTime = window.__phenixPageLoadTime || window.__pageLoadTime || Date.now();
|
||||
|
||||
interface ILogItem {
|
||||
timestamp: string;
|
||||
tenancy: string;
|
||||
level: string;
|
||||
category: string;
|
||||
message: string;
|
||||
sessionId: string;
|
||||
userId: string;
|
||||
version: string;
|
||||
environment: string;
|
||||
fullQualifiedName: string;
|
||||
source: string;
|
||||
runtime: number;
|
||||
}
|
||||
|
||||
export default class TelemetryService {
|
||||
private readonly _telemetryConfiguration: TelemetryConfiguration;
|
||||
|
||||
private _logs: Array<ILogItem> = [];
|
||||
private _isSending: boolean = false;
|
||||
private _domain = location.hostname;
|
||||
|
||||
constructor(telemetryConfiguration: TelemetryConfiguration) {
|
||||
this._telemetryConfiguration = telemetryConfiguration;
|
||||
}
|
||||
|
||||
push(logLevel: LoggingLevel, message: string, category: string, timestamp: Date): void {
|
||||
const now = Date.now();
|
||||
const runtime = (now - pageLoadTime) / 1000;
|
||||
const logRecord = {
|
||||
timestamp: timestamp.toISOString(),
|
||||
tenancy: this._telemetryConfiguration.tenancy,
|
||||
userId: this._telemetryConfiguration.userId,
|
||||
level: LoggingLevel[logLevel],
|
||||
runtime,
|
||||
category,
|
||||
message,
|
||||
sessionId: this._telemetryConfiguration.sessionId,
|
||||
version: config.controlCenterVersion,
|
||||
environment: this._telemetryConfiguration.environment,
|
||||
fullQualifiedName: this._domain,
|
||||
source: `Portal (${this._telemetryConfiguration.browser})`
|
||||
} as ILogItem;
|
||||
|
||||
if (logLevel < LoggingLevel.Error) {
|
||||
this._logs.push(logRecord);
|
||||
} else {
|
||||
this._logs.unshift(logRecord);
|
||||
}
|
||||
|
||||
// @ts-expect-error: Unused variable intentionally
|
||||
const ignored = this.sendLogsIfAble();
|
||||
}
|
||||
|
||||
private async sendLogs(logMessages: Array<ILogItem>): Promise<Response | void> {
|
||||
const formData = new FormData();
|
||||
|
||||
formData.append('jsonBody', JSON.stringify({records: logMessages}));
|
||||
|
||||
return await fetch(this._telemetryConfiguration.url, {
|
||||
method: 'POST',
|
||||
body: formData
|
||||
});
|
||||
}
|
||||
|
||||
private async sendLogsIfAble(): Promise<Response | void> {
|
||||
if (this._logs.length <= 0 || this._isSending) {
|
||||
return;
|
||||
}
|
||||
|
||||
let numberOfLogsToSend = 0;
|
||||
let sizeOfLogsToSend = 0;
|
||||
|
||||
this._isSending = true;
|
||||
|
||||
const getLogSize = (log: ILogItem): number => Object.values(log).reduce((sum, item) => sum + (item ? `${item}`.length : 0), 0);
|
||||
|
||||
while (this._logs.length > numberOfLogsToSend && getLogSize(this._logs[numberOfLogsToSend]) + sizeOfLogsToSend < requestSizeLimit) {
|
||||
sizeOfLogsToSend += getLogSize(this._logs[numberOfLogsToSend]);
|
||||
numberOfLogsToSend++;
|
||||
}
|
||||
|
||||
if (!numberOfLogsToSend) {
|
||||
this._logs[numberOfLogsToSend].message = this._logs[numberOfLogsToSend].message.substring(
|
||||
0,
|
||||
getLogSize(this._logs[numberOfLogsToSend]) + (requestSizeLimit - getLogSize(this._logs[numberOfLogsToSend]))
|
||||
);
|
||||
numberOfLogsToSend = 1;
|
||||
}
|
||||
|
||||
const logMessages = this._logs.slice(0, numberOfLogsToSend);
|
||||
|
||||
this._logs = this._logs.slice(numberOfLogsToSend);
|
||||
|
||||
return this.sendLogs(logMessages)
|
||||
.then(response => {
|
||||
this._isSending = false;
|
||||
|
||||
// @ts-expect-error: Unused variable intentionally
|
||||
|
||||
const ignored = this.sendLogsIfAble();
|
||||
|
||||
return response;
|
||||
})
|
||||
.catch(() => {
|
||||
this._isSending = false;
|
||||
|
||||
// @ts-expect-error: Unused variable intentionally
|
||||
|
||||
const ignored = this.sendLogsIfAble();
|
||||
});
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user