diff --git a/src/App.tsx b/src/App.tsx new file mode 100644 index 0000000..dbd512e --- /dev/null +++ b/src/App.tsx @@ -0,0 +1,26 @@ +import {JSX, useState} from 'react'; +import {useAppDispatch} from './store'; +import {authenticateCredentialsThunk} from './store/slices/Authentication.slice'; + +export default function App(): JSX.Element { + const dispatch = useAppDispatch(); + const [applicationId, setApplicationId] = useState('phenixrts.com-alex.zinn'); + const [secret, setSecret] = useState('AMAsDzr.dIuGMZ.Zu52Dt~MQvP!DZwYg'); + + const handleAuthenticate = async () => { + const response = await dispatch(authenticateCredentialsThunk({applicationId, secret})); + console.log(`${new Date().toISOString()} AuthenticationResponse [%o]`, response.payload); + }; + + return ( + <> +

Hello World

+
+ setApplicationId(e.target.value)} /> +
+ setSecret(e.target.value)} /> +
+ + + ); +} diff --git a/src/assets/images/background-1415x959.png b/src/assets/images/background-1415x959.png new file mode 100644 index 0000000..40caaef Binary files /dev/null and b/src/assets/images/background-1415x959.png differ diff --git a/src/assets/images/calendar-24x24.png b/src/assets/images/calendar-24x24.png new file mode 100644 index 0000000..263e3f3 Binary files /dev/null and b/src/assets/images/calendar-24x24.png differ diff --git a/src/assets/images/caret-down.svg b/src/assets/images/caret-down.svg new file mode 100644 index 0000000..95ce7d4 --- /dev/null +++ b/src/assets/images/caret-down.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/assets/images/caret-up.svg b/src/assets/images/caret-up.svg new file mode 100644 index 0000000..4cb6f2a --- /dev/null +++ b/src/assets/images/caret-up.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/assets/images/chart-down-50x33.png b/src/assets/images/chart-down-50x33.png new file mode 100644 index 0000000..7b9df1e Binary files /dev/null and b/src/assets/images/chart-down-50x33.png differ diff --git a/src/assets/images/chart-up-50x33.png b/src/assets/images/chart-up-50x33.png new file mode 100644 index 0000000..c3b857e Binary files /dev/null and b/src/assets/images/chart-up-50x33.png differ diff --git a/src/assets/images/icon/error.svg b/src/assets/images/icon/error.svg new file mode 100644 index 0000000..e13c93a --- /dev/null +++ b/src/assets/images/icon/error.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/assets/images/icon/hash-plus.svg b/src/assets/images/icon/hash-plus.svg new file mode 100644 index 0000000..b6be2ae --- /dev/null +++ b/src/assets/images/icon/hash-plus.svg @@ -0,0 +1,10 @@ + + + + + + + + + + \ No newline at end of file diff --git a/src/assets/images/icon/menu.svg b/src/assets/images/icon/menu.svg new file mode 100644 index 0000000..acc80d0 --- /dev/null +++ b/src/assets/images/icon/menu.svg @@ -0,0 +1,53 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/assets/images/icon/ok.svg b/src/assets/images/icon/ok.svg new file mode 100644 index 0000000..69da772 --- /dev/null +++ b/src/assets/images/icon/ok.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/assets/images/icon/refresh.svg b/src/assets/images/icon/refresh.svg new file mode 100644 index 0000000..ef4e31f --- /dev/null +++ b/src/assets/images/icon/refresh.svg @@ -0,0 +1,12 @@ + + + + + + + + + diff --git a/src/assets/images/logo-no-text.png b/src/assets/images/logo-no-text.png new file mode 100644 index 0000000..d000110 Binary files /dev/null and b/src/assets/images/logo-no-text.png differ diff --git a/src/assets/images/phenix-logo-101x41.png b/src/assets/images/phenix-logo-101x41.png new file mode 100755 index 0000000..93a3f7b Binary files /dev/null and b/src/assets/images/phenix-logo-101x41.png differ diff --git a/src/assets/images/phenix-offline-screen-1920x1080.gif b/src/assets/images/phenix-offline-screen-1920x1080.gif new file mode 100644 index 0000000..71620a2 Binary files /dev/null and b/src/assets/images/phenix-offline-screen-1920x1080.gif differ diff --git a/src/assets/images/search-150x150.png b/src/assets/images/search-150x150.png new file mode 100644 index 0000000..865cf13 Binary files /dev/null and b/src/assets/images/search-150x150.png differ diff --git a/src/assets/images/spinners/loading-icon-32x32.gif b/src/assets/images/spinners/loading-icon-32x32.gif new file mode 100644 index 0000000..5331edb Binary files /dev/null and b/src/assets/images/spinners/loading-icon-32x32.gif differ diff --git a/src/assets/images/symbol-lock-24x24.png b/src/assets/images/symbol-lock-24x24.png new file mode 100755 index 0000000..4825e13 Binary files /dev/null and b/src/assets/images/symbol-lock-24x24.png differ diff --git a/src/assets/images/symbol-person-24x24.png b/src/assets/images/symbol-person-24x24.png new file mode 100755 index 0000000..d3940aa Binary files /dev/null and b/src/assets/images/symbol-person-24x24.png differ diff --git a/src/config/index.ts b/src/config/index.ts new file mode 100644 index 0000000..03f71f5 --- /dev/null +++ b/src/config/index.ts @@ -0,0 +1,6 @@ +/** + * Copyright 2024 Phenix Real Time Solutions, Inc. Confidential and Proprietary. All Rights Reserved. + */ +import controlVersion from './version.json' with {type: 'json'}; + +export default {controlCenterVersion: controlVersion['version']}; diff --git a/src/config/version.json b/src/config/version.json new file mode 100644 index 0000000..9837a3a --- /dev/null +++ b/src/config/version.json @@ -0,0 +1,3 @@ +{ + "version": "local-2025-08-31T10:16:53.841Z (2024.3.2)" +} diff --git a/src/index.css b/src/index.css new file mode 100644 index 0000000..08a3ac9 --- /dev/null +++ b/src/index.css @@ -0,0 +1,68 @@ +:root { + font-family: system-ui, Avenir, Helvetica, Arial, sans-serif; + line-height: 1.5; + font-weight: 400; + + color-scheme: light dark; + color: rgba(255, 255, 255, 0.87); + background-color: #242424; + + font-synthesis: none; + text-rendering: optimizeLegibility; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} + +a { + font-weight: 500; + color: #646cff; + text-decoration: inherit; +} +a:hover { + color: #535bf2; +} + +body { + margin: 0; + display: flex; + place-items: center; + min-width: 320px; + min-height: 100vh; +} + +h1 { + font-size: 3.2em; + line-height: 1.1; +} + +button { + border-radius: 8px; + border: 1px solid transparent; + padding: 0.6em 1.2em; + font-size: 1em; + font-weight: 500; + font-family: inherit; + background-color: #1a1a1a; + cursor: pointer; + transition: border-color 0.25s; +} +button:hover { + border-color: #646cff; +} +button:focus, +button:focus-visible { + outline: 4px auto -webkit-focus-ring-color; +} + +@media (prefers-color-scheme: light) { + :root { + color: #213547; + background-color: #ffffff; + } + a:hover { + color: #747bff; + } + button { + background-color: #f9f9f9; + } +} diff --git a/src/lang/Strings.ts b/src/lang/Strings.ts new file mode 100644 index 0000000..57f54ac --- /dev/null +++ b/src/lang/Strings.ts @@ -0,0 +1,11 @@ +export default class Strings { + public static randomString(length: number): string { + return Math.random() + .toString(36) + .substring(2, 2 + length); + } + + private constructor() { + throw new Error('Strings is a static class that may not be instantiated'); + } +} diff --git a/src/lang/assertUnreachable.ts b/src/lang/assertUnreachable.ts new file mode 100644 index 0000000..77e30d9 --- /dev/null +++ b/src/lang/assertUnreachable.ts @@ -0,0 +1,3 @@ +export default function assertUnreachable(x: never): never { + throw new Error(`Error: Reached un-reachable code with [${x}]`); +} diff --git a/src/main.tsx b/src/main.tsx new file mode 100644 index 0000000..130eb9f --- /dev/null +++ b/src/main.tsx @@ -0,0 +1,12 @@ +import {createRoot} from 'react-dom/client'; +import {Provider} from 'react-redux'; +import store from './store'; +import App from './App.tsx'; + +import './index.css'; + +createRoot(document.getElementById('root')!).render( + + + +); diff --git a/src/services/Authentication.service.ts b/src/services/Authentication.service.ts new file mode 100644 index 0000000..bbbc71d --- /dev/null +++ b/src/services/Authentication.service.ts @@ -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 { + 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 { + await this._phenixWebSocket.sendMessage(PhenixWebSocketMessage.Bye, { + sessionId: this.sessionId, + reason: 'signout' + }); + } +} + +export default AuthenticationService.getInstance(); diff --git a/src/services/PlatformDetection.service.ts b/src/services/PlatformDetection.service.ts new file mode 100644 index 0000000..643a6d1 --- /dev/null +++ b/src/services/PlatformDetection.service.ts @@ -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; + } + } +} diff --git a/src/services/logger/Appenders.ts b/src/services/logger/Appenders.ts new file mode 100644 index 0000000..34574af --- /dev/null +++ b/src/services/logger/Appenders.ts @@ -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 = []; + + get value(): Array { + 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); + } +} diff --git a/src/services/logger/ConsoleAppender.ts b/src/services/logger/ConsoleAppender.ts new file mode 100644 index 0000000..c375fe8 --- /dev/null +++ b/src/services/logger/ConsoleAppender.ts @@ -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; + } +} diff --git a/src/services/logger/IAppender.ts b/src/services/logger/IAppender.ts new file mode 100644 index 0000000..8ec65fb --- /dev/null +++ b/src/services/logger/IAppender.ts @@ -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; +} diff --git a/src/services/logger/Logger.ts b/src/services/logger/Logger.ts new file mode 100644 index 0000000..f5a037c --- /dev/null +++ b/src/services/logger/Logger.ts @@ -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; + } +} diff --git a/src/services/logger/LoggerDefaults.ts b/src/services/logger/LoggerDefaults.ts new file mode 100644 index 0000000..3d2becc --- /dev/null +++ b/src/services/logger/LoggerDefaults.ts @@ -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; + } +} diff --git a/src/services/logger/LoggerFactory.ts b/src/services/logger/LoggerFactory.ts new file mode 100644 index 0000000..db4e085 --- /dev/null +++ b/src/services/logger/LoggerFactory.ts @@ -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 { + 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(); diff --git a/src/services/logger/LoggerInterface.ts b/src/services/logger/LoggerInterface.ts new file mode 100644 index 0000000..bc9ce40 --- /dev/null +++ b/src/services/logger/LoggerInterface.ts @@ -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 */ diff --git a/src/services/logger/LoggingLevelMapping.ts b/src/services/logger/LoggingLevelMapping.ts new file mode 100644 index 0000000..ec90dd7 --- /dev/null +++ b/src/services/logger/LoggingLevelMapping.ts @@ -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); + } + } +} diff --git a/src/services/logger/LoggingThreshold.ts b/src/services/logger/LoggingThreshold.ts new file mode 100644 index 0000000..d441f83 --- /dev/null +++ b/src/services/logger/LoggingThreshold.ts @@ -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; + } +} diff --git a/src/services/net/websockets/PhenixWebSocket.ts b/src/services/net/websockets/PhenixWebSocket.ts new file mode 100644 index 0000000..e9b2452 --- /dev/null +++ b/src/services/net/websockets/PhenixWebSocket.ts @@ -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 = new Subject(PhenixWebSocketStatus.Offline); + private readonly _readOnlySubject: ReadOnlySubject = new ReadOnlySubject(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 { + 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(kind: PhenixWebSocketMessage, message: T): Promise { + 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((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); + }) + } +} \ No newline at end of file diff --git a/src/services/net/websockets/PhenixWebSocketMessage.ts b/src/services/net/websockets/PhenixWebSocketMessage.ts new file mode 100644 index 0000000..b36f3a8 --- /dev/null +++ b/src/services/net/websockets/PhenixWebSocketMessage.ts @@ -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); + } + } +} diff --git a/src/services/net/websockets/PhenixWebSocketStatus.ts b/src/services/net/websockets/PhenixWebSocketStatus.ts new file mode 100644 index 0000000..ebfc6c0 --- /dev/null +++ b/src/services/net/websockets/PhenixWebSocketStatus.ts @@ -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); + } + } +} diff --git a/src/services/net/websockets/proto/analytics.proto.json b/src/services/net/websockets/proto/analytics.proto.json new file mode 100644 index 0000000..17725c1 --- /dev/null +++ b/src/services/net/websockets/proto/analytics.proto.json @@ -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 + } + ] + } + ] +} diff --git a/src/services/net/websockets/proto/pcast.proto.json b/src/services/net/websockets/proto/pcast.proto.json new file mode 100644 index 0000000..d3fdd3b --- /dev/null +++ b/src/services/net/websockets/proto/pcast.proto.json @@ -0,0 +1,1777 @@ +{ + "package": "pcast", + "messages": [ + { + "name": "Authenticate", + "fields": [ + { + "rule": "optional", + "type": "uint32", + "name": "apiVersion", + "id": 9, + "options": { + "default": 0 + } + }, + { + "rule": "required", + "type": "string", + "name": "clientVersion", + "id": 1 + }, + { + "rule": "optional", + "type": "string", + "name": "device", + "id": 12 + }, + { + "rule": "required", + "type": "string", + "name": "deviceId", + "id": 2 + }, + { + "rule": "optional", + "type": "string", + "name": "manufacturer", + "id": 13 + }, + { + "rule": "required", + "type": "string", + "name": "platform", + "id": 3 + }, + { + "rule": "required", + "type": "string", + "name": "platformVersion", + "id": 4 + }, + { + "rule": "required", + "type": "string", + "name": "authenticationToken", + "id": 5 + }, + { + "rule": "optional", + "type": "string", + "name": "connectionId", + "id": 6 + }, + { + "rule": "optional", + "type": "string", + "name": "connectionRouteKey", + "id": 10 + }, + { + "rule": "optional", + "type": "string", + "name": "remoteAddress", + "id": 11 + }, + { + "rule": "optional", + "type": "string", + "name": "sessionId", + "id": 7 + }, + { + "rule": "optional", + "type": "string", + "name": "applicationId", + "id": 8 + } + ] + }, + { + "name": "AuthenticateResponse", + "fields": [ + { + "rule": "required", + "type": "string", + "name": "status", + "id": 1 + }, + { + "rule": "optional", + "type": "string", + "name": "sessionId", + "id": 2 + }, + { + "rule": "optional", + "type": "string", + "name": "redirect", + "id": 3 + }, + { + "rule": "repeated", + "type": "string", + "name": "roles", + "id": 4 + } + ] + }, + { + "name": "Bye", + "fields": [ + { + "rule": "required", + "type": "string", + "name": "sessionId", + "id": 1 + }, + { + "rule": "required", + "type": "string", + "name": "reason", + "id": 2 + } + ] + }, + { + "name": "ByeResponse", + "fields": [ + { + "rule": "required", + "type": "string", + "name": "status", + "id": 1 + } + ] + }, + { + "name": "SessionDescription", + "fields": [ + { + "rule": "required", + "type": "Type", + "name": "type", + "id": 1, + "options": { + "default": "Offer" + } + }, + { + "rule": "required", + "type": "string", + "name": "sdp", + "id": 2 + } + ], + "enums": [ + { + "name": "Type", + "values": [ + { + "name": "Offer", + "id": 0 + }, + { + "name": "Answer", + "id": 1 + } + ] + } + ] + }, + { + "name": "CreateStream", + "fields": [ + { + "rule": "required", + "type": "string", + "name": "sessionId", + "id": 1 + }, + { + "rule": "optional", + "type": "string", + "name": "originStreamId", + "id": 2 + }, + { + "rule": "repeated", + "type": "string", + "name": "options", + "id": 3 + }, + { + "rule": "optional", + "type": "string", + "name": "connectUri", + "id": 8 + }, + { + "rule": "repeated", + "type": "string", + "name": "connectOptions", + "id": 9 + }, + { + "rule": "repeated", + "type": "string", + "name": "tags", + "id": 4 + }, + { + "rule": "optional", + "type": "SetRemoteDescription", + "name": "setRemoteDescription", + "id": 5 + }, + { + "rule": "optional", + "type": "CreateOfferDescription", + "name": "createOfferDescription", + "id": 6 + }, + { + "rule": "optional", + "type": "CreateAnswerDescription", + "name": "createAnswerDescription", + "id": 7 + } + ] + }, + { + "name": "IceServer", + "fields": [ + { + "rule": "repeated", + "type": "string", + "name": "urls", + "id": 1 + }, + { + "rule": "optional", + "type": "string", + "name": "username", + "id": 2 + }, + { + "rule": "optional", + "type": "string", + "name": "credential", + "id": 3 + } + ] + }, + { + "name": "RtcConfiguration", + "fields": [ + { + "rule": "optional", + "type": "BundlePolicy", + "name": "bundlePolicy", + "id": 1 + }, + { + "rule": "optional", + "type": "uint32", + "name": "iceCandidatePoolSize", + "id": 3 + }, + { + "rule": "repeated", + "type": "IceServer", + "name": "iceServers", + "id": 4 + }, + { + "rule": "optional", + "type": "IceTransportPolicy", + "name": "iceTransportPolicy", + "id": 5 + }, + { + "rule": "optional", + "type": "string", + "name": "peerIdentity", + "id": 6 + }, + { + "rule": "optional", + "type": "RtcpMuxPolicy", + "name": "rtcpMuxPolicy", + "id": 7 + } + ], + "enums": [ + { + "name": "BundlePolicy", + "values": [ + { + "name": "BundlePolicyBalanced", + "id": 1 + }, + { + "name": "BundlePolicyMaxCompat", + "id": 2 + }, + { + "name": "BundlePolicyMaxBundle", + "id": 3 + } + ] + }, + { + "name": "IceTransportPolicy", + "values": [ + { + "name": "IceTransportPolicyAll", + "id": 1 + }, + { + "name": "IceTransportPolicyPublic", + "id": 2 + }, + { + "name": "IceTransportPolicyRelay", + "id": 3 + } + ] + }, + { + "name": "RtcpMuxPolicy", + "values": [ + { + "name": "RtcpMuxPolicyNegotiate", + "id": 1 + }, + { + "name": "RtcpMuxPolicyRequire", + "id": 2 + } + ] + } + ] + }, + { + "name": "CreateStreamResponse", + "fields": [ + { + "rule": "required", + "type": "string", + "name": "status", + "id": 1 + }, + { + "rule": "optional", + "type": "string", + "name": "streamId", + "id": 2 + }, + { + "rule": "optional", + "type": "string", + "name": "instanceRouteKey", + "id": 5 + }, + { + "rule": "repeated", + "type": "string", + "name": "streamUris", + "id": 8 + }, + { + "rule": "optional", + "type": "RtcConfiguration", + "name": "rtcConfiguration", + "id": 9 + }, + { + "rule": "optional", + "type": "SetRemoteDescriptionResponse", + "name": "setRemoteDescriptionResponse", + "id": 3 + }, + { + "rule": "optional", + "type": "CreateOfferDescriptionResponse", + "name": "createOfferDescriptionResponse", + "id": 4 + }, + { + "rule": "optional", + "type": "CreateAnswerDescriptionResponse", + "name": "createAnswerDescriptionResponse", + "id": 6 + }, + { + "rule": "repeated", + "type": "string", + "name": "options", + "id": 7 + }, + { + "rule": "optional", + "type": "uint64", + "name": "offset", + "id": 10, + "options": { + "default": 0 + } + } + ] + }, + { + "name": "SetLocalDescription", + "fields": [ + { + "rule": "required", + "type": "string", + "name": "streamId", + "id": 1 + }, + { + "rule": "required", + "type": "SessionDescription", + "name": "sessionDescription", + "id": 2 + }, + { + "rule": "optional", + "type": "uint32", + "name": "apiVersion", + "id": 3, + "options": { + "default": 0 + } + } + ] + }, + { + "name": "SetLocalDescriptionResponse", + "fields": [ + { + "rule": "required", + "type": "string", + "name": "status", + "id": 1 + }, + { + "rule": "repeated", + "type": "string", + "name": "options", + "id": 2 + } + ] + }, + { + "name": "SetRemoteDescription", + "fields": [ + { + "rule": "required", + "type": "string", + "name": "streamId", + "id": 1 + }, + { + "rule": "required", + "type": "SessionDescription", + "name": "sessionDescription", + "id": 2 + }, + { + "rule": "optional", + "type": "uint32", + "name": "apiVersion", + "id": 3, + "options": { + "default": 0 + } + } + ] + }, + { + "name": "SetRemoteDescriptionResponse", + "fields": [ + { + "rule": "required", + "type": "string", + "name": "status", + "id": 1 + }, + { + "rule": "optional", + "type": "SessionDescription", + "name": "sessionDescription", + "id": 2 + }, + { + "rule": "repeated", + "type": "string", + "name": "options", + "id": 3 + } + ] + }, + { + "name": "CreateOfferDescription", + "fields": [ + { + "rule": "required", + "type": "string", + "name": "streamId", + "id": 1 + }, + { + "rule": "repeated", + "type": "string", + "name": "options", + "id": 2 + }, + { + "rule": "optional", + "type": "uint32", + "name": "apiVersion", + "id": 3, + "options": { + "default": 0 + } + } + ] + }, + { + "name": "CreateOfferDescriptionResponse", + "fields": [ + { + "rule": "required", + "type": "string", + "name": "status", + "id": 1 + }, + { + "rule": "optional", + "type": "SessionDescription", + "name": "sessionDescription", + "id": 2 + }, + { + "rule": "repeated", + "type": "string", + "name": "options", + "id": 3 + } + ] + }, + { + "name": "CreateAnswerDescription", + "fields": [ + { + "rule": "required", + "type": "string", + "name": "streamId", + "id": 1 + }, + { + "rule": "repeated", + "type": "string", + "name": "options", + "id": 2 + }, + { + "rule": "optional", + "type": "uint32", + "name": "apiVersion", + "id": 3, + "options": { + "default": 0 + } + } + ] + }, + { + "name": "CreateAnswerDescriptionResponse", + "fields": [ + { + "rule": "required", + "type": "string", + "name": "status", + "id": 1 + }, + { + "rule": "optional", + "type": "SessionDescription", + "name": "sessionDescription", + "id": 2 + }, + { + "rule": "repeated", + "type": "string", + "name": "options", + "id": 3 + } + ] + }, + { + "name": "IceCandidate", + "fields": [ + { + "rule": "required", + "type": "string", + "name": "candidate", + "id": 1 + }, + { + "rule": "required", + "type": "uint32", + "name": "sdpMLineIndex", + "id": 2 + }, + { + "rule": "required", + "type": "string", + "name": "sdpMid", + "id": 3 + } + ] + }, + { + "name": "AddIceCandidates", + "fields": [ + { + "rule": "required", + "type": "string", + "name": "streamId", + "id": 1 + }, + { + "rule": "repeated", + "type": "IceCandidate", + "name": "candidates", + "id": 2 + }, + { + "rule": "repeated", + "type": "string", + "name": "options", + "id": 3 + }, + { + "rule": "optional", + "type": "uint32", + "name": "apiVersion", + "id": 4, + "options": { + "default": 0 + } + } + ] + }, + { + "name": "AddIceCandidatesResponse", + "fields": [ + { + "rule": "required", + "type": "string", + "name": "status", + "id": 1 + }, + { + "rule": "repeated", + "type": "string", + "name": "options", + "id": 2 + } + ] + }, + { + "name": "UpdateStreamState", + "fields": [ + { + "rule": "required", + "type": "string", + "name": "streamId", + "id": 1 + }, + { + "rule": "required", + "type": "string", + "name": "signalingState", + "id": 2 + }, + { + "rule": "required", + "type": "string", + "name": "iceGatheringState", + "id": 3 + }, + { + "rule": "required", + "type": "string", + "name": "iceConnectionState", + "id": 4 + }, + { + "rule": "optional", + "type": "uint32", + "name": "apiVersion", + "id": 5, + "options": { + "default": 0 + } + } + ] + }, + { + "name": "UpdateStreamStateResponse", + "fields": [ + { + "rule": "required", + "type": "string", + "name": "status", + "id": 1 + }, + { + "rule": "repeated", + "type": "string", + "name": "options", + "id": 2 + } + ] + }, + { + "name": "DestroyStream", + "fields": [ + { + "rule": "required", + "type": "string", + "name": "streamId", + "id": 1 + }, + { + "rule": "optional", + "type": "string", + "name": "reason", + "id": 2 + }, + { + "rule": "repeated", + "type": "string", + "name": "options", + "id": 3 + } + ] + }, + { + "name": "DestroyStreamResponse", + "fields": [ + { + "rule": "required", + "type": "string", + "name": "status", + "id": 1 + } + ] + }, + { + "name": "ConnectionDisconnected", + "fields": [ + { + "rule": "required", + "type": "string", + "name": "connectionId", + "id": 1 + }, + { + "rule": "required", + "type": "uint32", + "name": "reasonCode", + "id": 2 + }, + { + "rule": "optional", + "type": "string", + "name": "description", + "id": 3 + } + ] + }, + { + "name": "ConnectionDisconnectedResponse", + "fields": [ + { + "rule": "required", + "type": "string", + "name": "status", + "id": 1 + } + ] + }, + { + "name": "StreamStarted", + "fields": [ + { + "rule": "required", + "type": "string", + "name": "sessionId", + "id": 1 + }, + { + "rule": "required", + "type": "string", + "name": "streamId", + "id": 2 + }, + { + "rule": "repeated", + "type": "string", + "name": "tags", + "id": 3 + } + ] + }, + { + "name": "SourceStreamStarted", + "fields": [ + { + "rule": "required", + "type": "string", + "name": "sessionId", + "id": 1 + }, + { + "rule": "required", + "type": "string", + "name": "streamId", + "id": 2 + }, + { + "rule": "repeated", + "type": "string", + "name": "capabilities", + "id": 3 + }, + { + "rule": "repeated", + "type": "string", + "name": "tags", + "id": 4 + } + ] + }, + { + "name": "StreamEnded", + "fields": [ + { + "rule": "required", + "type": "string", + "name": "sessionId", + "id": 1 + }, + { + "rule": "required", + "type": "string", + "name": "streamId", + "id": 2 + }, + { + "rule": "required", + "type": "string", + "name": "reason", + "id": 3 + }, + { + "rule": "repeated", + "type": "string", + "name": "tags", + "id": 4 + }, + { + "rule": "optional", + "type": "string", + "name": "continuationId", + "id": 5 + }, + { + "rule": "optional", + "type": "string", + "name": "routeKey", + "id": 6 + } + ] + }, + { + "name": "SourceStreamEnded", + "fields": [ + { + "rule": "required", + "type": "string", + "name": "sessionId", + "id": 1 + }, + { + "rule": "required", + "type": "string", + "name": "streamId", + "id": 2 + }, + { + "rule": "required", + "type": "string", + "name": "reason", + "id": 3 + }, + { + "rule": "repeated", + "type": "string", + "name": "capabilities", + "id": 4 + }, + { + "rule": "repeated", + "type": "string", + "name": "tags", + "id": 5 + } + ] + }, + { + "name": "StreamEndedResponse", + "fields": [ + { + "rule": "required", + "type": "string", + "name": "status", + "id": 1 + }, + { + "rule": "optional", + "type": "string", + "name": "continuationId", + "id": 2 + }, + { + "rule": "optional", + "type": "string", + "name": "routeKey", + "id": 3 + } + ] + }, + { + "name": "StreamIdle", + "fields": [ + { + "rule": "required", + "type": "string", + "name": "sessionId", + "id": 1 + }, + { + "rule": "required", + "type": "string", + "name": "streamId", + "id": 2 + }, + { + "rule": "repeated", + "type": "string", + "name": "options", + "id": 3 + }, + { + "rule": "repeated", + "type": "string", + "name": "tags", + "id": 4 + } + ] + }, + { + "name": "StreamArchived", + "fields": [ + { + "rule": "required", + "type": "string", + "name": "sessionId", + "id": 1 + }, + { + "rule": "required", + "type": "string", + "name": "streamId", + "id": 2 + }, + { + "rule": "required", + "type": "uint64", + "name": "startTime", + "id": 4 + }, + { + "rule": "required", + "type": "string", + "name": "uri", + "id": 3 + } + ] + }, + { + "name": "SessionEnded", + "fields": [ + { + "rule": "required", + "type": "string", + "name": "sessionId", + "id": 1 + }, + { + "rule": "required", + "type": "string", + "name": "reason", + "id": 2 + }, + { + "rule": "required", + "type": "float", + "name": "duration", + "id": 3 + } + ] + }, + { + "name": "ResourceIdle", + "fields": [ + { + "rule": "required", + "type": "string", + "name": "sessionId", + "id": 1 + }, + { + "rule": "required", + "type": "string", + "name": "routeKey", + "id": 2 + } + ] + }, + { + "name": "ResourceIdleResponse", + "fields": [ + { + "rule": "required", + "type": "string", + "name": "status", + "id": 1 + } + ] + }, + { + "name": "StreamPlaylist", + "fields": [ + { + "rule": "required", + "type": "string", + "name": "sessionId", + "id": 1 + }, + { + "rule": "required", + "type": "string", + "name": "streamId", + "id": 2 + }, + { + "rule": "required", + "type": "PlaylistType", + "name": "playlistType", + "id": 3 + }, + { + "rule": "required", + "type": "string", + "name": "uri", + "id": 4 + } + ], + "enums": [ + { + "name": "PlaylistType", + "values": [ + { + "name": "Live", + "id": 0 + }, + { + "name": "OnDemand", + "id": 1 + } + ] + } + ] + }, + { + "name": "SendEventToClient", + "fields": [ + { + "rule": "required", + "type": "string", + "name": "connectionId", + "id": 1 + }, + { + "rule": "required", + "type": "string", + "name": "type", + "id": 2 + }, + { + "rule": "required", + "type": "bytes", + "name": "payload", + "id": 3 + } + ] + }, + { + "name": "SendEventToClientResponse", + "fields": [ + { + "rule": "required", + "type": "string", + "name": "status", + "id": 1 + } + ] + }, + { + "name": "SendRequestToClient", + "fields": [ + { + "rule": "required", + "type": "string", + "name": "connectionId", + "id": 1 + }, + { + "rule": "required", + "type": "string", + "name": "type", + "id": 2 + }, + { + "rule": "required", + "type": "bytes", + "name": "payload", + "id": 3 + } + ] + }, + { + "name": "SendRequestToClientResponse", + "fields": [ + { + "rule": "required", + "type": "string", + "name": "status", + "id": 1 + }, + { + "rule": "optional", + "type": "string", + "name": "type", + "id": 2 + }, + { + "rule": "optional", + "type": "bytes", + "name": "payload", + "id": 3 + } + ] + }, + { + "name": "SetupStream", + "fields": [ + { + "rule": "required", + "type": "string", + "name": "streamToken", + "id": 1 + }, + { + "rule": "required", + "type": "CreateStream", + "name": "createStream", + "id": 2 + } + ] + }, + { + "name": "SetupStreamResponse", + "fields": [ + { + "rule": "required", + "type": "string", + "name": "status", + "id": 1 + }, + { + "rule": "optional", + "type": "CreateStreamResponse", + "name": "createStreamResponse", + "id": 2 + } + ] + }, + { + "name": "SetupPlaylistStream", + "fields": [ + { + "rule": "required", + "type": "string", + "name": "sessionId", + "id": 1 + }, + { + "rule": "required", + "type": "string", + "name": "streamToken", + "id": 2 + }, + { + "rule": "repeated", + "type": "string", + "name": "options", + "id": 3 + }, + { + "rule": "repeated", + "type": "string", + "name": "tags", + "id": 4 + } + ] + }, + { + "name": "PlaylistStreamManifest", + "fields": [ + { + "rule": "required", + "type": "string", + "name": "manifest", + "id": 1 + }, + { + "rule": "required", + "type": "bool", + "name": "isProtectedContent", + "id": 2 + } + ] + }, + { + "name": "SetupPlaylistStreamResponse", + "fields": [ + { + "rule": "required", + "type": "string", + "name": "status", + "id": 1 + }, + { + "rule": "repeated", + "type": "PlaylistStreamManifest", + "name": "manifests", + "id": 2 + }, + { + "rule": "optional", + "type": "uint64", + "name": "offset", + "id": 3, + "options": { + "default": 0 + } + } + ] + }, + { + "name": "StreamDataQuality", + "fields": [ + { + "rule": "required", + "type": "string", + "name": "sessionId", + "id": 1 + }, + { + "rule": "required", + "type": "string", + "name": "streamId", + "id": 2 + }, + { + "rule": "required", + "type": "uint64", + "name": "timestamp", + "id": 3 + }, + { + "rule": "required", + "type": "DataQualityStatus", + "name": "status", + "id": 4 + }, + { + "rule": "required", + "type": "DataQualityReason", + "name": "reason", + "id": 5 + } + ], + "enums": [ + { + "name": "DataQualityStatus", + "values": [ + { + "name": "NoData", + "id": 0 + }, + { + "name": "AudioOnly", + "id": 1 + }, + { + "name": "All", + "id": 2 + } + ] + }, + { + "name": "DataQualityReason", + "values": [ + { + "name": "None", + "id": 0 + }, + { + "name": "UploadLimited", + "id": 1 + }, + { + "name": "DownloadLimited", + "id": 2 + }, + { + "name": "PublisherLimited", + "id": 3 + }, + { + "name": "NetworkLimited", + "id": 4 + } + ] + } + ] + }, + { + "name": "StreamDataQualityResponse", + "fields": [ + { + "rule": "required", + "type": "string", + "name": "status", + "id": 1 + } + ] + }, + { + "name": "CallbackEvent", + "fields": [ + { + "rule": "optional", + "type": "uint32", + "name": "apiVersion", + "id": 1, + "options": { + "default": 0 + } + }, + { + "rule": "required", + "type": "string", + "name": "entity", + "id": 2 + }, + { + "rule": "required", + "type": "string", + "name": "what", + "id": 3 + }, + { + "rule": "required", + "type": "string", + "name": "data", + "id": 4 + }, + { + "rule": "optional", + "type": "string", + "name": "sessionId", + "id": 5 + } + ] + }, + { + "name": "Uri", + "fields": [ + { + "rule": "optional", + "type": "string", + "name": "protocol", + "id": 1, + "options": { + "default": "http" + } + }, + { + "rule": "required", + "type": "string", + "name": "host", + "id": 2 + }, + { + "rule": "optional", + "type": "uint32", + "name": "port", + "id": 3, + "options": { + "default": 80 + } + }, + { + "rule": "optional", + "type": "string", + "name": "method", + "id": 4, + "options": { + "default": "POST" + } + }, + { + "rule": "optional", + "type": "string", + "name": "path", + "id": 5, + "options": { + "default": "/" + } + } + ] + }, + { + "name": "SetApplicationCallback", + "fields": [ + { + "rule": "required", + "type": "string", + "name": "applicationId", + "id": 1 + }, + { + "rule": "required", + "type": "string", + "name": "secret", + "id": 2 + }, + { + "rule": "required", + "type": "Uri", + "name": "callback", + "id": 3 + } + ] + }, + { + "name": "SetApplicationCallbackResponse", + "fields": [ + { + "rule": "required", + "type": "string", + "name": "status", + "id": 1 + } + ] + }, + { + "name": "IssueAuthenticationToken", + "fields": [ + { + "rule": "required", + "type": "string", + "name": "applicationId", + "id": 1 + }, + { + "rule": "required", + "type": "string", + "name": "secret", + "id": 2 + }, + { + "rule": "repeated", + "type": "string", + "name": "capabilities", + "id": 3 + } + ] + }, + { + "name": "IssueAuthenticationTokenResponse", + "fields": [ + { + "rule": "required", + "type": "string", + "name": "status", + "id": 1 + }, + { + "rule": "optional", + "type": "string", + "name": "authenticationToken", + "id": 2 + } + ] + }, + { + "name": "IssueStreamToken", + "fields": [ + { + "rule": "required", + "type": "string", + "name": "applicationId", + "id": 1 + }, + { + "rule": "required", + "type": "string", + "name": "secret", + "id": 2 + }, + { + "rule": "required", + "type": "string", + "name": "sessionId", + "id": 3 + }, + { + "rule": "optional", + "type": "string", + "name": "originStreamId", + "id": 4 + }, + { + "rule": "repeated", + "type": "string", + "name": "capabilities", + "id": 5 + } + ] + }, + { + "name": "IssueStreamTokenResponse", + "fields": [ + { + "rule": "required", + "type": "string", + "name": "status", + "id": 1 + }, + { + "rule": "optional", + "type": "string", + "name": "streamToken", + "id": 2 + } + ] + }, + { + "name": "IssueDrmToken", + "fields": [ + { + "rule": "required", + "type": "string", + "name": "applicationId", + "id": 1 + }, + { + "rule": "required", + "type": "string", + "name": "secret", + "id": 2 + }, + { + "rule": "optional", + "type": "string", + "name": "sessionId", + "id": 3 + }, + { + "rule": "required", + "type": "string", + "name": "originStreamId", + "id": 4 + }, + { + "rule": "repeated", + "type": "string", + "name": "capabilities", + "id": 5 + } + ] + }, + { + "name": "IssueDrmTokenResponse", + "fields": [ + { + "rule": "required", + "type": "string", + "name": "status", + "id": 1 + }, + { + "rule": "optional", + "type": "string", + "name": "drmToken", + "id": 2 + } + ] + }, + { + "name": "TerminateStream", + "fields": [ + { + "rule": "required", + "type": "string", + "name": "applicationId", + "id": 1 + }, + { + "rule": "required", + "type": "string", + "name": "secret", + "id": 2 + }, + { + "rule": "optional", + "type": "string", + "name": "streamId", + "id": 3, + "oneof": "streamOrToken" + }, + { + "rule": "optional", + "type": "string", + "name": "streamToken", + "id": 5, + "oneof": "streamOrToken" + }, + { + "rule": "optional", + "type": "string", + "name": "reason", + "id": 4 + } + ], + "oneofs": { + "streamOrToken": [3, 5] + } + }, + { + "name": "TerminateStreamResponse", + "fields": [ + { + "rule": "required", + "type": "string", + "name": "status", + "id": 1 + } + ] + }, + { + "name": "Stream", + "fields": [ + { + "rule": "required", + "type": "string", + "name": "streamId", + "id": 1 + } + ] + }, + { + "name": "ListStreams", + "fields": [ + { + "rule": "required", + "type": "string", + "name": "applicationId", + "id": 1 + }, + { + "rule": "required", + "type": "string", + "name": "secret", + "id": 2 + }, + { + "rule": "optional", + "type": "string", + "name": "start", + "id": 3 + }, + { + "rule": "required", + "type": "uint32", + "name": "length", + "id": 4 + }, + { + "rule": "repeated", + "type": "string", + "name": "options", + "id": 5 + } + ] + }, + { + "name": "ListStreamsResponse", + "fields": [ + { + "rule": "required", + "type": "string", + "name": "status", + "id": 1 + }, + { + "rule": "optional", + "type": "string", + "name": "start", + "id": 2 + }, + { + "rule": "optional", + "type": "uint32", + "name": "length", + "id": 3 + }, + { + "rule": "repeated", + "type": "Stream", + "name": "streams", + "id": 4 + } + ] + } + ] +} diff --git a/src/services/telemetry/TelemetryApender.ts b/src/services/telemetry/TelemetryApender.ts new file mode 100644 index 0000000..746c22b --- /dev/null +++ b/src/services/telemetry/TelemetryApender.ts @@ -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 { + if (logLevel < this._threshold) { + return; + } + + this._telemetryService.push(logLevel, message, category, date); + } +} diff --git a/src/services/telemetry/TelemetryConfiguration.ts b/src/services/telemetry/TelemetryConfiguration.ts new file mode 100644 index 0000000..844b82d --- /dev/null +++ b/src/services/telemetry/TelemetryConfiguration.ts @@ -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; + } +} diff --git a/src/services/telemetry/TelemetryService.ts b/src/services/telemetry/TelemetryService.ts new file mode 100644 index 0000000..5a30c2d --- /dev/null +++ b/src/services/telemetry/TelemetryService.ts @@ -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 = []; + 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): Promise { + 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 { + 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(); + }); + } +} diff --git a/src/store/index.ts b/src/store/index.ts new file mode 100644 index 0000000..5058403 --- /dev/null +++ b/src/store/index.ts @@ -0,0 +1,13 @@ +import {useDispatch, useSelector} from 'react-redux'; +import store from './store'; + +export default store; + +// Use throughout the app instead of plain `useDispatch` and `useSelector` +// Infer the `RootState`, `AppDispatch`, and `AppStore` types from the store itself +export type RootState = ReturnType; +// Inferred type: {posts: PostsState, comments: C +export type AppDispatch = typeof store.dispatch; +export type AppStore = typeof store; +export const useAppDispatch = useDispatch.withTypes(); +export const useAppSelector = useSelector.withTypes(); diff --git a/src/store/slices/Authentication.slice.ts b/src/store/slices/Authentication.slice.ts new file mode 100644 index 0000000..5087a1d --- /dev/null +++ b/src/store/slices/Authentication.slice.ts @@ -0,0 +1,169 @@ +import {createSlice, PayloadAction, createAsyncThunk, createSelector} from '@reduxjs/toolkit'; +import AuthenticationService from '../../services/Authentication.service'; +import {PhenixWebSocketStatusType} from 'services/net/websockets/PhenixWebSocketStatus'; +import {IPhenixWebSocketResponse} from 'services/net/websockets/PhenixWebSocket'; + +export interface IAuthenticationState { + applicationId: string | null; + secret: string | null; + isAuthenticated: boolean; + isLoading: boolean; + status: PhenixWebSocketStatusType; + error: string | null; + sessionId: string | null; + roles: string[]; +} + +const initialAuthenticationState: IAuthenticationState = { + applicationId: null, + sessionId: null, + isAuthenticated: false, + isLoading: false, + status: 'Offline', + roles: [], + error: null, + secret: null +}; + +// Memoized selectors +export const selectAuthentication = (state: {authentication: IAuthenticationState}) => state.authentication; + +export const selectIsLoading = createSelector([selectAuthentication], authentication => authentication.isLoading); + +export const selectIsAuthenticated = createSelector([selectAuthentication], authentication => authentication.isAuthenticated); + +export const selectError = createSelector([selectAuthentication], authentication => authentication.error); + +export const selectStatus = createSelector([selectAuthentication], authentication => authentication.status); + +export const selectCredentials = createSelector([selectAuthentication], authentication => ({ + id: authentication.applicationId, + secret: authentication.secret +})); + +export const selectSessionInfo = createSelector([selectAuthentication], authentication => ({ + sessionId: authentication.sessionId, + roles: authentication.roles +})); + +const authenticateCredentialsThunk = createAsyncThunk( + 'authentication/authenticate', + async (credentials, {rejectWithValue}) => { + try { + const response = await AuthenticationService.authenticate(credentials.applicationId, credentials.secret); + + return response as IPhenixWebSocketResponse; + } catch (error) { + // Convert error to serializable format + const errorMessage = error instanceof Error ? error.message : 'Authentication failed'; + return rejectWithValue(errorMessage); + } + } +); + +const signoutThunk = createAsyncThunk('authentication/signout', async (_, {rejectWithValue}) => { + try { + return await AuthenticationService.signout(); + } catch (error) { + // Convert error to serializable format + const errorMessage = error instanceof Error ? error.message : 'Signout failed'; + return rejectWithValue(errorMessage); + } +}); + +const authenticationSlice = createSlice({ + name: 'authentication', + initialState: {...initialAuthenticationState}, + reducers: { + setIsLoading: (state, action: PayloadAction) => { + state.isLoading = action.payload; + }, + setCredentials: (state, action: PayloadAction<{applicationId: string; secret: string}>) => { + state.applicationId = action.payload.applicationId; + state.secret = action.payload.secret; + }, + clearState: state => { + state.applicationId = null; + state.sessionId = null; + state.isAuthenticated = false; + state.isLoading = false; + state.error = null; + state.secret = null; + state.status = 'Offline'; + state.roles = []; + }, + setSessionId: (state, action: PayloadAction) => { + state.sessionId = action.payload; + }, + setIsAuthenticated: (state, action: PayloadAction) => { + state.isAuthenticated = action.payload; + }, + setRoles: (state, action: PayloadAction) => { + state.roles = action.payload; + }, + setApplicationId: (state, action: PayloadAction) => { + state.applicationId = action.payload; + } + }, + extraReducers: builder => { + builder + .addCase(authenticateCredentialsThunk.pending, state => { + state.isLoading = true; + state.error = null; + }) + .addCase(authenticateCredentialsThunk.fulfilled, (state, action) => { + const authenticationResponse = action.payload; + + if (authenticationResponse.status === 'ok') { + state.applicationId = authenticationResponse.applicationId ?? null; + state.sessionId = authenticationResponse.sessionId ?? null; + state.isAuthenticated = true; + state.roles = authenticationResponse.roles ?? []; + } else { + state.applicationId = null; + state.sessionId = null; + state.isAuthenticated = false; + state.secret = null; + state.roles = []; + } + + state.status = 'Online'; + state.isLoading = false; + state.error = null; + }) + .addCase(authenticateCredentialsThunk.rejected, (state, action) => { + state.applicationId = null; + state.sessionId = null; + state.isAuthenticated = false; + state.isLoading = false; + state.error = (action.payload as string) || 'Authentication failed'; + state.secret = null; + state.status = 'Offline'; + state.roles = []; + }) + .addCase(signoutThunk.pending, state => { + state.isLoading = true; + state.error = null; + }) + .addCase(signoutThunk.fulfilled, state => { + state.isAuthenticated = false; + state.isLoading = false; + state.error = null; + state.secret = null; + state.status = 'Offline'; + state.roles = []; + }) + .addCase(signoutThunk.rejected, (state, action) => { + state.isAuthenticated = false; + state.isLoading = false; + state.error = (action.payload as string) || 'Signout failed'; + state.secret = null; + state.status = 'Offline'; + state.roles = []; + }); + } +}); + +export const {setIsLoading, setCredentials, clearState, setSessionId, setIsAuthenticated, setRoles, setApplicationId} = authenticationSlice.actions; +export {authenticateCredentialsThunk}; +export default authenticationSlice.reducer; \ No newline at end of file diff --git a/src/store/store.ts b/src/store/store.ts new file mode 100644 index 0000000..1ce81d0 --- /dev/null +++ b/src/store/store.ts @@ -0,0 +1,10 @@ +import {configureStore} from '@reduxjs/toolkit'; +import AuthenticationState from './slices/Authentication.slice'; + +const store = configureStore({ + reducer: { + authentication: AuthenticationState + } +}); + +export default store; diff --git a/src/vite-env.d.ts b/src/vite-env.d.ts new file mode 100644 index 0000000..11f02fe --- /dev/null +++ b/src/vite-env.d.ts @@ -0,0 +1 @@ +///