Async config file (#682)
* initial * only donwload config once * formatting * update sample config * sentry * refactor load state * fix build yaml * Upper case enums * change how defaults work. review fixes * abstract initialization * copyright * gitignore styleing * refactor initialization * use dafualt as fallback * internalInstance rename * review * remove acidentally added posthog file * DSN rename * update Copyright * remove olm from the initializer Co-authored-by: Timo K <timok@element.io>
This commit is contained in:
parent
806a9032e1
commit
78a313c373
10 changed files with 293 additions and 88 deletions
5
.github/workflows/build.yaml
vendored
5
.github/workflows/build.yaml
vendored
|
@ -4,9 +4,6 @@ on:
|
|||
branches: [main]
|
||||
env:
|
||||
VITE_DEFAULT_HOMESERVER: "https://call.ems.host"
|
||||
VITE_SENTRY_DSN: https://b1e328d49be3402ba96101338989fb35@sentry.matrix.org/41
|
||||
VITE_SENTRY_ENVIRONMENT: main-branch-cd
|
||||
VITE_RAGESHAKE_SUBMIT_URL: https://element.io/bugreports/submit
|
||||
jobs:
|
||||
build:
|
||||
name: Build
|
||||
|
@ -17,7 +14,7 @@ jobs:
|
|||
- name: Yarn cache
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
cache: 'yarn'
|
||||
cache: "yarn"
|
||||
- name: Install dependencies
|
||||
run: "yarn install"
|
||||
- name: Build
|
||||
|
|
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -5,3 +5,4 @@ dist
|
|||
dist-ssr
|
||||
*.local
|
||||
.idea/
|
||||
config.json
|
||||
|
|
13
sample.config.json
Normal file
13
sample.config.json
Normal file
|
@ -0,0 +1,13 @@
|
|||
{
|
||||
"posthog": {
|
||||
"api_key": "examplePosthogKey",
|
||||
"api_host": "https://posthog.com"
|
||||
},
|
||||
"sentry": {
|
||||
"environment": "main-branch",
|
||||
"DSN": "https://examplePublicKey@o0.ingest.sentry.io/0"
|
||||
},
|
||||
"rageshake": {
|
||||
"submit_url": "http://localhost:9110/api/submit"
|
||||
}
|
||||
}
|
|
@ -3,8 +3,6 @@
|
|||
set -ex
|
||||
|
||||
export VITE_DEFAULT_HOMESERVER=https://call.ems.host
|
||||
export VITE_SENTRY_DSN=https://b1e328d49be3402ba96101338989fb35@sentry.matrix.org/41
|
||||
export VITE_RAGESHAKE_SUBMIT_URL=https://element.io/bugreports/submit
|
||||
export VITE_PRODUCT_NAME="Element Call"
|
||||
|
||||
git clone https://github.com/matrix-org/matrix-js-sdk.git
|
||||
|
|
77
src/App.tsx
77
src/App.tsx
|
@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React, { Suspense } from "react";
|
||||
import React, { Suspense, useEffect, useState } from "react";
|
||||
import { BrowserRouter as Router, Switch, Route } from "react-router-dom";
|
||||
import * as Sentry from "@sentry/react";
|
||||
import { OverlayProvider } from "@react-aria/overlays";
|
||||
|
@ -28,7 +28,8 @@ import { ClientProvider } from "./ClientContext";
|
|||
import { usePageFocusStyle } from "./usePageFocusStyle";
|
||||
import { SequenceDiagramViewerPage } from "./SequenceDiagramViewerPage";
|
||||
import { InspectorContextProvider } from "./room/GroupCallInspector";
|
||||
import { CrashView } from "./FullScreenView";
|
||||
import { CrashView, LoadingView } from "./FullScreenView";
|
||||
import { Initializer } from "./initializer";
|
||||
|
||||
const SentryRoute = Sentry.withSentryRouting(Route);
|
||||
|
||||
|
@ -37,42 +38,54 @@ interface AppProps {
|
|||
}
|
||||
|
||||
export default function App({ history }: AppProps) {
|
||||
const [loaded, setLoaded] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
Initializer.init()?.then(() => {
|
||||
setLoaded(true);
|
||||
});
|
||||
});
|
||||
|
||||
usePageFocusStyle();
|
||||
|
||||
const errorPage = <CrashView />;
|
||||
|
||||
return (
|
||||
<Router history={history}>
|
||||
<Suspense fallback={null}>
|
||||
<ClientProvider>
|
||||
<InspectorContextProvider>
|
||||
<Sentry.ErrorBoundary fallback={errorPage}>
|
||||
<OverlayProvider>
|
||||
<Switch>
|
||||
<SentryRoute exact path="/">
|
||||
<HomePage />
|
||||
</SentryRoute>
|
||||
<SentryRoute exact path="/login">
|
||||
<LoginPage />
|
||||
</SentryRoute>
|
||||
<SentryRoute exact path="/register">
|
||||
<RegisterPage />
|
||||
</SentryRoute>
|
||||
<SentryRoute path="/room/:roomId?">
|
||||
<RoomPage />
|
||||
</SentryRoute>
|
||||
<SentryRoute path="/inspector">
|
||||
<SequenceDiagramViewerPage />
|
||||
</SentryRoute>
|
||||
<SentryRoute path="*">
|
||||
<RoomRedirect />
|
||||
</SentryRoute>
|
||||
</Switch>
|
||||
</OverlayProvider>
|
||||
</Sentry.ErrorBoundary>
|
||||
</InspectorContextProvider>
|
||||
</ClientProvider>
|
||||
</Suspense>
|
||||
{loaded ? (
|
||||
<Suspense fallback={null}>
|
||||
<ClientProvider>
|
||||
<InspectorContextProvider>
|
||||
<Sentry.ErrorBoundary fallback={errorPage}>
|
||||
<OverlayProvider>
|
||||
<Switch>
|
||||
<SentryRoute exact path="/">
|
||||
<HomePage />
|
||||
</SentryRoute>
|
||||
<SentryRoute exact path="/login">
|
||||
<LoginPage />
|
||||
</SentryRoute>
|
||||
<SentryRoute exact path="/register">
|
||||
<RegisterPage />
|
||||
</SentryRoute>
|
||||
<SentryRoute path="/room/:roomId?">
|
||||
<RoomPage />
|
||||
</SentryRoute>
|
||||
<SentryRoute path="/inspector">
|
||||
<SequenceDiagramViewerPage />
|
||||
</SentryRoute>
|
||||
<SentryRoute path="*">
|
||||
<RoomRedirect />
|
||||
</SentryRoute>
|
||||
</Switch>
|
||||
</OverlayProvider>
|
||||
</Sentry.ErrorBoundary>
|
||||
</InspectorContextProvider>
|
||||
</ClientProvider>
|
||||
</Suspense>
|
||||
) : (
|
||||
<LoadingView />
|
||||
)}
|
||||
</Router>
|
||||
);
|
||||
}
|
||||
|
|
64
src/config/Config.ts
Normal file
64
src/config/Config.ts
Normal file
|
@ -0,0 +1,64 @@
|
|||
/*
|
||||
Copyright 2021-2022 New Vector Ltd
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import { DEFAULT_CONFIG, IConfigOptions } from "./ConfigOptions";
|
||||
|
||||
export class Config {
|
||||
private static internalInstance: Config;
|
||||
public static get instance(): Config {
|
||||
if (!this.internalInstance)
|
||||
throw new Error("Config instance read before config got initialized");
|
||||
return this.internalInstance;
|
||||
}
|
||||
public static init(): Promise<void> {
|
||||
if (Config?.internalInstance?.initPromise) {
|
||||
return Config.internalInstance.initPromise;
|
||||
}
|
||||
Config.internalInstance = new Config();
|
||||
Config.internalInstance.initPromise = new Promise<void>((resolve) => {
|
||||
downloadConfig("../config.json").then((config) => {
|
||||
Config.internalInstance.config = { ...DEFAULT_CONFIG, ...config };
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
return Config.internalInstance.initPromise;
|
||||
}
|
||||
|
||||
public config: IConfigOptions;
|
||||
private initPromise: Promise<void>;
|
||||
}
|
||||
|
||||
async function downloadConfig(
|
||||
configJsonFilename: string
|
||||
): Promise<IConfigOptions> {
|
||||
const url = new URL(configJsonFilename, window.location.href);
|
||||
url.searchParams.set("cachebuster", Date.now().toString());
|
||||
const res = await fetch(url, {
|
||||
cache: "no-cache",
|
||||
method: "GET",
|
||||
});
|
||||
|
||||
if (res.status === 404 || res.status === 0) {
|
||||
// Lack of a config isn't an error, we should just use the defaults.
|
||||
// Also treat a blank config as no config, assuming the status code is 0, because we don't get 404s from file:
|
||||
// URIs so this is the only way we can not fail if the file doesn't exist when loading from a file:// URI.
|
||||
return {} as IConfigOptions;
|
||||
}
|
||||
|
||||
if (res.ok) {
|
||||
return res.json();
|
||||
}
|
||||
}
|
19
src/config/ConfigOptions.ts
Normal file
19
src/config/ConfigOptions.ts
Normal file
|
@ -0,0 +1,19 @@
|
|||
export interface IConfigOptions {
|
||||
posthog?: {
|
||||
api_key: string;
|
||||
};
|
||||
sentry?: {
|
||||
DSN: string;
|
||||
environment: string;
|
||||
};
|
||||
rageshake?: {
|
||||
submit_url: string;
|
||||
};
|
||||
}
|
||||
|
||||
export const DEFAULT_CONFIG: IConfigOptions = {
|
||||
sentry: { DSN: "", environment: "production" },
|
||||
rageshake: {
|
||||
submit_url: "https://element.io/bugreports/submit",
|
||||
},
|
||||
};
|
146
src/initializer.tsx
Normal file
146
src/initializer.tsx
Normal file
|
@ -0,0 +1,146 @@
|
|||
/*
|
||||
Copyright 2022 Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import { Integrations } from "@sentry/tracing";
|
||||
import i18n from "i18next";
|
||||
import { initReactI18next } from "react-i18next";
|
||||
import LanguageDetector from "i18next-browser-languagedetector";
|
||||
import Backend from "i18next-http-backend";
|
||||
import * as Sentry from "@sentry/react";
|
||||
|
||||
import { getUrlParams } from "./UrlParams";
|
||||
import { Config } from "./config/Config";
|
||||
import { DEFAULT_CONFIG } from "./config/ConfigOptions";
|
||||
|
||||
enum LoadState {
|
||||
None,
|
||||
Loading,
|
||||
Loaded,
|
||||
}
|
||||
|
||||
class DependencyLoadStates {
|
||||
// TODO: decide where olm should be initialized (see TODO comment below)
|
||||
// olm: LoadState = LoadState.None;
|
||||
config: LoadState = LoadState.None;
|
||||
sentry: LoadState = LoadState.None;
|
||||
i18n: LoadState = LoadState.None;
|
||||
|
||||
allDepsAreLoaded() {
|
||||
return !Object.values(this).some((s) => s !== LoadState.Loaded);
|
||||
}
|
||||
}
|
||||
|
||||
export class Initializer {
|
||||
private static internalInstance: Initializer;
|
||||
|
||||
public static init(): Promise<void> | null {
|
||||
if (Initializer?.internalInstance?.initPromise) {
|
||||
return null;
|
||||
}
|
||||
Initializer.internalInstance = new Initializer();
|
||||
Initializer.internalInstance.initPromise = new Promise<void>((resolve) => {
|
||||
// initStep calls itself recursivly until everything is initialized in the correct order.
|
||||
// Then the promise gets resolved.
|
||||
Initializer.internalInstance.initStep(resolve);
|
||||
});
|
||||
return Initializer.internalInstance.initPromise;
|
||||
}
|
||||
|
||||
loadStates = new DependencyLoadStates();
|
||||
|
||||
initStep(resolve: (value: void | PromiseLike<void>) => void) {
|
||||
// TODO: Olm is initialized with the client currently (see `initClient()` and `olm.ts`)
|
||||
// we need to decide if we want to init it here or keep it in initClient
|
||||
// if (this.loadStates.olm === LoadState.None) {
|
||||
// this.loadStates.olm = LoadState.Loading;
|
||||
// // TODO: https://gitlab.matrix.org/matrix-org/olm/-/issues/10
|
||||
// window.OLM_OPTIONS = {};
|
||||
// Olm.init({ locateFile: () => olmWasmPath }).then(() => {
|
||||
// this.loadStates.olm = LoadState.Loaded;
|
||||
// this.initStep(resolve);
|
||||
// });
|
||||
// }
|
||||
|
||||
// config
|
||||
if (this.loadStates.config === LoadState.None) {
|
||||
this.loadStates.config = LoadState.Loading;
|
||||
Config.init().then(() => {
|
||||
this.loadStates.config = LoadState.Loaded;
|
||||
this.initStep(resolve);
|
||||
});
|
||||
}
|
||||
|
||||
//sentry (only initialize after the config is ready)
|
||||
if (
|
||||
this.loadStates.sentry === LoadState.None &&
|
||||
this.loadStates.config === LoadState.Loaded
|
||||
) {
|
||||
Sentry.init({
|
||||
dsn: Config.instance.config.sentry?.DSN ?? DEFAULT_CONFIG.sentry.DSN,
|
||||
environment:
|
||||
Config.instance.config.sentry.environment ??
|
||||
DEFAULT_CONFIG.sentry.environment,
|
||||
integrations: [
|
||||
new Integrations.BrowserTracing({
|
||||
routingInstrumentation:
|
||||
Sentry.reactRouterV5Instrumentation(history),
|
||||
}),
|
||||
],
|
||||
tracesSampleRate: 1.0,
|
||||
});
|
||||
this.loadStates.sentry = LoadState.Loaded;
|
||||
}
|
||||
|
||||
//i18n
|
||||
if (this.loadStates.i18n === LoadState.None) {
|
||||
const languageDetector = new LanguageDetector();
|
||||
languageDetector.addDetector({
|
||||
name: "urlFragment",
|
||||
// Look for a language code in the URL's fragment
|
||||
lookup: () => getUrlParams().lang ?? undefined,
|
||||
});
|
||||
|
||||
i18n
|
||||
.use(Backend)
|
||||
.use(languageDetector)
|
||||
.use(initReactI18next)
|
||||
.init({
|
||||
fallbackLng: "en-GB",
|
||||
defaultNS: "app",
|
||||
keySeparator: false,
|
||||
nsSeparator: false,
|
||||
pluralSeparator: "|",
|
||||
contextSeparator: "|",
|
||||
interpolation: {
|
||||
escapeValue: false, // React has built-in XSS protections
|
||||
},
|
||||
detection: {
|
||||
// No localStorage detectors or caching here, since we don't have any way
|
||||
// of letting the user manually select a language
|
||||
order: ["urlFragment", "navigator"],
|
||||
caches: [],
|
||||
},
|
||||
});
|
||||
this.loadStates.i18n = LoadState.Loaded;
|
||||
}
|
||||
|
||||
if (this.loadStates.allDepsAreLoaded()) {
|
||||
// resolve if there is no dependency that is not loaded
|
||||
resolve();
|
||||
}
|
||||
}
|
||||
private initPromise: Promise<void>;
|
||||
}
|
48
src/main.tsx
48
src/main.tsx
|
@ -23,17 +23,10 @@ import "matrix-js-sdk/src/browser-index";
|
|||
import React, { StrictMode } from "react";
|
||||
import { createRoot } from "react-dom/client";
|
||||
import { createBrowserHistory } from "history";
|
||||
import * as Sentry from "@sentry/react";
|
||||
import { Integrations } from "@sentry/tracing";
|
||||
import i18n from "i18next";
|
||||
import { initReactI18next } from "react-i18next";
|
||||
import Backend from "i18next-http-backend";
|
||||
import LanguageDetector from "i18next-browser-languagedetector";
|
||||
|
||||
import "./index.css";
|
||||
import App from "./App";
|
||||
import { init as initRageshake } from "./settings/rageshake";
|
||||
import { getUrlParams } from "./UrlParams";
|
||||
|
||||
initRageshake();
|
||||
|
||||
|
@ -108,47 +101,6 @@ if (import.meta.env.VITE_CUSTOM_THEME) {
|
|||
|
||||
const history = createBrowserHistory();
|
||||
|
||||
Sentry.init({
|
||||
dsn: import.meta.env.VITE_SENTRY_DSN as string,
|
||||
environment:
|
||||
(import.meta.env.VITE_SENTRY_ENVIRONMENT as string) ?? "production",
|
||||
integrations: [
|
||||
new Integrations.BrowserTracing({
|
||||
routingInstrumentation: Sentry.reactRouterV5Instrumentation(history),
|
||||
}),
|
||||
],
|
||||
tracesSampleRate: 1.0,
|
||||
});
|
||||
|
||||
const languageDetector = new LanguageDetector();
|
||||
languageDetector.addDetector({
|
||||
name: "urlFragment",
|
||||
// Look for a language code in the URL's fragment
|
||||
lookup: () => getUrlParams().lang ?? undefined,
|
||||
});
|
||||
|
||||
i18n
|
||||
.use(Backend)
|
||||
.use(languageDetector)
|
||||
.use(initReactI18next)
|
||||
.init({
|
||||
fallbackLng: "en-GB",
|
||||
defaultNS: "app",
|
||||
keySeparator: false,
|
||||
nsSeparator: false,
|
||||
pluralSeparator: "|",
|
||||
contextSeparator: "|",
|
||||
interpolation: {
|
||||
escapeValue: false, // React has built-in XSS protections
|
||||
},
|
||||
detection: {
|
||||
// No localStorage detectors or caching here, since we don't have any way
|
||||
// of letting the user manually select a language
|
||||
order: ["urlFragment", "navigator"],
|
||||
caches: [],
|
||||
},
|
||||
});
|
||||
|
||||
root.render(
|
||||
<StrictMode>
|
||||
<App history={history} />
|
||||
|
|
|
@ -24,6 +24,8 @@ import { getLogsForReport } from "./rageshake";
|
|||
import { useClient } from "../ClientContext";
|
||||
import { InspectorContext } from "../room/GroupCallInspector";
|
||||
import { useModalTriggerState } from "../Modal";
|
||||
import { Config } from "../config/Config";
|
||||
import { DEFAULT_CONFIG } from "../config/ConfigOptions";
|
||||
|
||||
interface RageShakeSubmitOptions {
|
||||
sendLogs: boolean;
|
||||
|
@ -252,8 +254,8 @@ export function useSubmitRageshake(): {
|
|||
}
|
||||
|
||||
await fetch(
|
||||
(import.meta.env.VITE_RAGESHAKE_SUBMIT_URL as string) ||
|
||||
"https://element.io/bugreports/submit",
|
||||
Config.instance.config.rageshake?.submit_url ??
|
||||
DEFAULT_CONFIG.rageshake.submit_url,
|
||||
{
|
||||
method: "POST",
|
||||
body,
|
||||
|
|
Loading…
Reference in a new issue