Add authentication and assets
26
src/App.tsx
Normal file
@@ -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<string>('phenixrts.com-alex.zinn');
|
||||
const [secret, setSecret] = useState<string>('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 (
|
||||
<>
|
||||
<h1>Hello World</h1>
|
||||
<div>
|
||||
<input type="text" value={applicationId} onChange={e => setApplicationId(e.target.value)} />
|
||||
<br />
|
||||
<input type="text" value={secret} onChange={e => setSecret(e.target.value)} />
|
||||
</div>
|
||||
<button onClick={handleAuthenticate}>Authenticate</button>
|
||||
</>
|
||||
);
|
||||
}
|
||||
BIN
src/assets/images/background-1415x959.png
Normal file
|
After Width: | Height: | Size: 321 KiB |
BIN
src/assets/images/calendar-24x24.png
Normal file
|
After Width: | Height: | Size: 652 B |
1
src/assets/images/caret-down.svg
Normal file
@@ -0,0 +1 @@
|
||||
<?xml version="1.0" ?><!DOCTYPE svg PUBLIC '-//W3C//DTD SVG 1.1//EN' 'http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd'><svg enable-background="new 0 0 29 14" height="6px" id="Layer_1" version="1.1" viewBox="0 0 29 14" width="29px" xml:space="preserve" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"><polygon fill="#F9F9F9" points="0.15,0 14.5,14.35 28.85,0 "/></svg>
|
||||
|
After Width: | Height: | Size: 400 B |
1
src/assets/images/caret-up.svg
Normal file
@@ -0,0 +1 @@
|
||||
<?xml version="1.0" ?><!DOCTYPE svg PUBLIC '-//W3C//DTD SVG 1.1//EN' 'http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd'><svg enable-background="new 0 0 29 14" height="8px" id="Layer_1" version="1.1" viewBox="0 0 29 14" width="29px" xml:space="preserve" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"><polygon fill="#f9f9f9" points="0.15,14 14.5,-0.35 28.85,14 "/></svg>
|
||||
|
After Width: | Height: | Size: 402 B |
BIN
src/assets/images/chart-down-50x33.png
Normal file
|
After Width: | Height: | Size: 1.2 KiB |
BIN
src/assets/images/chart-up-50x33.png
Normal file
|
After Width: | Height: | Size: 1.1 KiB |
1
src/assets/images/icon/error.svg
Normal file
@@ -0,0 +1 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?><!-- Generator: Gravit.io --><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" style="isolation:isolate" viewBox="0 0 206.751 186.399" width="206.751pt" height="186.399pt"><defs><clipPath id="_clipPath_Wy0NWkkA4mKqtolphxnGK0Cf0TrWZqlo"><rect width="206.751" height="186.399"/></clipPath></defs><g clip-path="url(#_clipPath_Wy0NWkkA4mKqtolphxnGK0Cf0TrWZqlo)"><g><path d=" M 179.102 178.898 L 103.306 178.559 L 27.51 178.22 C 9.581 178.14 2.36 165.502 11.394 150.015 L 49.585 84.544 L 87.777 19.072 C 96.811 3.586 111.366 3.651 120.261 19.218 L 157.865 85.028 L 195.469 150.839 C 204.364 166.406 197.03 178.979 179.102 178.898 Z " fill="none" vector-effect="non-scaling-stroke" stroke-width="5" stroke="rgb(247,13,26)" stroke-linejoin="miter" stroke-linecap="square" stroke-miterlimit="3"/><line x1="103.306" y1="47.028" x2="103.306" y2="120.028" vector-effect="non-scaling-stroke" stroke-width="5" stroke="rgb(247,13,26)" stroke-linejoin="miter" stroke-linecap="square" stroke-miterlimit="3"/><line x1="103.306" y1="138.028" x2="103.306" y2="143.028" vector-effect="non-scaling-stroke" stroke-width="5" stroke="rgb(247,13,26)" stroke-linejoin="miter" stroke-linecap="square" stroke-miterlimit="3"/></g></g></svg>
|
||||
|
After Width: | Height: | Size: 1.3 KiB |
10
src/assets/images/icon/hash-plus.svg
Normal file
@@ -0,0 +1,10 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg width="19px" height="18px" viewBox="0 0 19 18" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
|
||||
<g id="01-home-copy-7" transform="translate(-1214.000000, -84.000000)" fill="#FFFFFF" fill-rule="nonzero">
|
||||
<g id="icon/white/add-channel" transform="translate(1211.000000, 80.000000)">
|
||||
<path d="M18,14 C18.5522847,14 19,14.4477153 19,15 L18.999068,17 L21,17 C21.5522847,17 22,17.4477153 22,18 C22,18.5522847 21.5522847,19 21,19 L18.999068,19 L19,21 C19,21.5522847 18.5522847,22 18,22 C17.4477153,22 17,21.5522847 17,21 L16.999068,18.999 L15,19 C14.4477153,19 14,18.5522847 14,18 C14,17.4477153 14.4477153,17 15,17 L16.999068,16.999 L17,15 C17,14.4477153 17.4477153,14 18,14 Z M14,4 C14.4287818,4 14.8254386,4.13754318 15.1482567,4.37091574 C15.4794061,4.13462978 15.8829334,4 16.3080311,4 L18,4 C19.0836085,4 19.9620467,4.87843824 19.9620467,5.96204672 C19.9620467,6.23272939 19.9060382,6.5004841 19.7975435,6.748472 L19.687068,6.999 L19.763932,7 C20.8685015,7 21.763932,7.8954305 21.763932,9 C21.763932,9.31049019 21.6916418,9.61671632 21.5527864,9.89442719 L20.5527864,11.8944272 C20.3949049,12.2101902 20.1601982,12.4717535 19.8778882,12.6610569 C19.3634108,12.2470293 18.7105538,12 18,12 C17.1114527,12 16.3131288,12.3862919 15.7638055,13.0000983 L15.083068,13 L15.916068,11 L18.381966,11 C18.7607381,11 19.1070012,10.7859976 19.2763932,10.4472136 L19.5,10 C19.6706654,9.65866925 19.5323138,9.24361439 19.190983,9.07294902 C19.0950363,9.02497564 18.9892377,9 18.881966,9 L16.750068,9 L17.6153846,6.92307692 C17.7569961,6.58320938 17.5962778,6.19289353 17.2564103,6.05128205 C17.1751635,6.01742923 17.0880173,6 17,6 L16.6666667,6 C16.2629658,6 15.8988593,6.24273768 15.7435897,6.61538462 L14.749068,9 L12.750068,9 L13.6153846,6.92307692 C13.7569961,6.58320938 13.5962778,6.19289353 13.2564103,6.05128205 C13.1751635,6.01742923 13.0880173,6 13,6 L12.6666667,6 C12.2629658,6 11.8988593,6.24273768 11.7435897,6.61538462 L10.749068,9 L8.61803399,9 C8.23926193,9 7.89299881,9.21400238 7.7236068,9.5527864 L7.5,10 C7.32933463,10.3413307 7.46768625,10.7563856 7.80901699,10.927051 C7.90496374,10.9750244 8.01076227,11 8.11803399,11 L9.91606798,11 L9.08206798,13 L6.61803399,13 C6.23926193,13 5.89299881,13.2140024 5.7236068,13.5527864 L5.5,14 C5.32933463,14.3413307 5.46768625,14.7563856 5.80901699,14.927051 C5.90496374,14.9750244 6.01076227,15 6.11803399,15 L8.24906798,15 L7.38461538,17.0769231 C7.24300391,17.4167906 7.40372221,17.8071065 7.74358974,17.9487179 C7.82483652,17.9825708 7.91198265,18 8,18 L8.33333333,18 C8.73703418,18 9.1011407,17.7572623 9.25641026,17.3846154 L10.249068,15 L12.249068,15 L11.3846154,17.0769231 C11.2430039,17.4167906 11.4037222,17.8071065 11.7435897,17.9487179 C11.8248365,17.9825708 11.9119827,18 12,18 L12.0354373,18.0000497 C12.0120835,18.1633354 12,18.3302566 12,18.5 C12,19.037207 12.1210295,19.5461461 12.3373087,20.0010375 L11,20 C10.5712182,20 10.1745614,19.8624568 9.85174329,19.6290843 C9.52059393,19.8653702 9.11706659,20 8.69196885,20 L7,20 C5.91639152,20 5.03795328,19.1215618 5.03795328,18.0379533 C5.03795328,17.7672706 5.09396179,17.4995159 5.2024565,17.251528 L5.31206798,17 L5.23606798,17 C4.13149848,17 3.23606798,16.1045695 3.23606798,15 C3.23606798,14.6895098 3.30835816,14.3832837 3.4472136,14.1055728 L4.4472136,12.1055728 C4.62888297,11.7422341 4.91227191,11.4506579 5.25285994,11.2583596 C5.24175599,11.1747423 5.23606798,11.0880354 5.23606798,11 C5.23606798,10.6895098 5.30835816,10.3832837 5.4472136,10.1055728 L6.4472136,8.10557281 C6.78599762,7.42800475 7.47852386,7 8.23606798,7 L9.68706798,6.999 L10.4757165,5.19836233 C10.7942054,4.47038768 11.5134355,4 12.3080311,4 L14,4 Z M15,15 L14.9990501,15.0355804 C14.6965332,15.0789368 14.406503,15.160979 14.1341327,15.276534 L14.249068,15 L15,15 Z M13.916068,11 L13.082068,13 L11.083068,13 L11.916068,11 L13.916068,11 Z" id="Combined-Shape"></path>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 4.0 KiB |
53
src/assets/images/icon/menu.svg
Normal file
@@ -0,0 +1,53 @@
|
||||
<?xml version="1.0" encoding="iso-8859-1"?>
|
||||
<!-- Generator: Adobe Illustrator 19.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||
<svg version="1.1" id="Capa_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||
viewBox="0 0 512 512" style="enable-background:new 0 0 512 512;" xml:space="preserve">
|
||||
<g>
|
||||
<g>
|
||||
<path style="fill:#F9F9F9" d="M501.333,96H10.667C4.779,96,0,100.779,0,106.667s4.779,10.667,10.667,10.667h490.667c5.888,0,10.667-4.779,10.667-10.667
|
||||
S507.221,96,501.333,96z"/>
|
||||
</g>
|
||||
</g>
|
||||
<g>
|
||||
<g>
|
||||
<path style="fill:#F9F9F9" d="M501.333,245.333H10.667C4.779,245.333,0,250.112,0,256s4.779,10.667,10.667,10.667h490.667
|
||||
c5.888,0,10.667-4.779,10.667-10.667S507.221,245.333,501.333,245.333z"/>
|
||||
</g>
|
||||
</g>
|
||||
<g>
|
||||
<g>
|
||||
<path style="fill:#F9F9F9" d="M501.333,394.667H10.667C4.779,394.667,0,399.445,0,405.333C0,411.221,4.779,416,10.667,416h490.667
|
||||
c5.888,0,10.667-4.779,10.667-10.667C512,399.445,507.221,394.667,501.333,394.667z"/>
|
||||
</g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.2 KiB |
1
src/assets/images/icon/ok.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg fill="#00bc8c" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 50 50" width="250px" height="250px"><path fill="none" stroke="#00bc8c" stroke-miterlimit="10" stroke-width="2" d="M25,3C12.85,3,3,12.85,3,25s9.85,22,22,22 s22-9.85,22-22S37.15,3,25,3z"/><path fill="none" stroke="#00bc8c" stroke-linecap="round" stroke-miterlimit="10" stroke-width="2" d="M16,24.444L24.143,32L35,16"/></svg>
|
||||
|
After Width: | Height: | Size: 390 B |
12
src/assets/images/icon/refresh.svg
Normal file
@@ -0,0 +1,12 @@
|
||||
<?xml version="1.0" encoding="iso-8859-1"?>
|
||||
<!-- Generator: Adobe Illustrator 19.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||
<svg width="19px" height="18px" version="1.1" id="Capa_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||
viewBox="0 0 341.333 341.333" style="enable-background:new 0 0 341.333 341.333;" xml:space="preserve">
|
||||
<g>
|
||||
<g>
|
||||
<path fill="#ffffff" d="M341.227,149.333V0l-50.133,50.133C260.267,19.2,217.707,0,170.56,0C76.267,0,0.107,76.373,0.107,170.667
|
||||
s76.16,170.667,170.453,170.667c79.467,0,146.027-54.4,164.907-128h-44.373c-17.6,49.707-64.747,85.333-120.533,85.333
|
||||
c-70.72,0-128-57.28-128-128s57.28-128,128-128c35.307,0,66.987,14.72,90.133,37.867l-68.8,68.8H341.227z"/>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 796 B |
BIN
src/assets/images/logo-no-text.png
Normal file
|
After Width: | Height: | Size: 10 KiB |
BIN
src/assets/images/phenix-logo-101x41.png
Executable file
|
After Width: | Height: | Size: 4.7 KiB |
BIN
src/assets/images/phenix-offline-screen-1920x1080.gif
Normal file
|
After Width: | Height: | Size: 1.5 MiB |
BIN
src/assets/images/search-150x150.png
Normal file
|
After Width: | Height: | Size: 2.5 KiB |
BIN
src/assets/images/spinners/loading-icon-32x32.gif
Normal file
|
After Width: | Height: | Size: 24 KiB |
BIN
src/assets/images/symbol-lock-24x24.png
Executable file
|
After Width: | Height: | Size: 374 B |
BIN
src/assets/images/symbol-person-24x24.png
Executable file
|
After Width: | Height: | Size: 563 B |
6
src/config/index.ts
Normal file
@@ -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']};
|
||||
3
src/config/version.json
Normal file
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"version": "local-2025-08-31T10:16:53.841Z (2024.3.2)"
|
||||
}
|
||||
68
src/index.css
Normal file
@@ -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;
|
||||
}
|
||||
}
|
||||
11
src/lang/Strings.ts
Normal file
@@ -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');
|
||||
}
|
||||
}
|
||||
3
src/lang/assertUnreachable.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export default function assertUnreachable(x: never): never {
|
||||
throw new Error(`Error: Reached un-reachable code with [${x}]`);
|
||||
}
|
||||
12
src/main.tsx
Normal file
@@ -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(
|
||||
<Provider store={store}>
|
||||
<App />
|
||||
</Provider>
|
||||
);
|
||||
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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
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
@@ -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
@@ -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();
|
||||
});
|
||||
}
|
||||
}
|
||||
13
src/store/index.ts
Normal file
@@ -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<typeof store.getState>;
|
||||
// Inferred type: {posts: PostsState, comments: C
|
||||
export type AppDispatch = typeof store.dispatch;
|
||||
export type AppStore = typeof store;
|
||||
export const useAppDispatch = useDispatch.withTypes<AppDispatch>();
|
||||
export const useAppSelector = useSelector.withTypes<RootState>();
|
||||
169
src/store/slices/Authentication.slice.ts
Normal file
@@ -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<IPhenixWebSocketResponse, {applicationId: string; secret: string}>(
|
||||
'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<boolean>) => {
|
||||
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<string>) => {
|
||||
state.sessionId = action.payload;
|
||||
},
|
||||
setIsAuthenticated: (state, action: PayloadAction<boolean>) => {
|
||||
state.isAuthenticated = action.payload;
|
||||
},
|
||||
setRoles: (state, action: PayloadAction<string[]>) => {
|
||||
state.roles = action.payload;
|
||||
},
|
||||
setApplicationId: (state, action: PayloadAction<string>) => {
|
||||
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;
|
||||
10
src/store/store.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import {configureStore} from '@reduxjs/toolkit';
|
||||
import AuthenticationState from './slices/Authentication.slice';
|
||||
|
||||
const store = configureStore({
|
||||
reducer: {
|
||||
authentication: AuthenticationState
|
||||
}
|
||||
});
|
||||
|
||||
export default store;
|
||||
1
src/vite-env.d.ts
vendored
Normal file
@@ -0,0 +1 @@
|
||||
/// <reference types="vite/client" />
|
||||