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:
Timo 2022-11-03 19:43:41 +01:00 committed by GitHub
parent 806a9032e1
commit 78a313c373
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
10 changed files with 293 additions and 88 deletions

View file

@ -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
View file

@ -5,3 +5,4 @@ dist
dist-ssr
*.local
.idea/
config.json

13
sample.config.json Normal file
View 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"
}
}

View file

@ -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

View file

@ -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
View 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();
}
}

View 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
View 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>;
}

View file

@ -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} />

View file

@ -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,