diff --git a/app/gui/analytics/src/remote_log.rs b/app/gui/analytics/src/remote_log.rs index 62bb3a824e4..75ccaa8a52a 100644 --- a/app/gui/analytics/src/remote_log.rs +++ b/app/gui/analytics/src/remote_log.rs @@ -18,17 +18,15 @@ mod js { #[wasm_bindgen(inline_js = " export function remote_log(msg, value) { - try { - window.ensoglApp.remoteLog(msg,value) - } catch (error) { - console.error(\"Error while logging message. \" + error ); - } + window.ensoglApp.remoteLog(msg, value).catch((error) => { + console.error(`Error while logging message. ${error}`) + }) } export function remote_log_value(msg, field_name, value) { const data = {} data[field_name] = value - remote_log(msg,data) + remote_log(msg, data) } ")] extern "C" { diff --git a/app/ide-desktop/lib/content-config/src/config.json b/app/ide-desktop/lib/content-config/src/config.json index eff732d5010..e5676ae6acf 100644 --- a/app/ide-desktop/lib/content-config/src/config.json +++ b/app/ide-desktop/lib/content-config/src/config.json @@ -3,11 +3,6 @@ "authentication": { "value": true, "description": "Determines whether user authentication is enabled. This option is always true when executed in the cloud." - }, - "dataCollection": { - "value": true, - "description": "Determines whether anonymous usage data is to be collected.", - "primary": false } }, "groups": { diff --git a/app/ide-desktop/lib/content/src/index.ts b/app/ide-desktop/lib/content/src/index.ts index 9af95aa6429..b6cd8380520 100644 --- a/app/ide-desktop/lib/content/src/index.ts +++ b/app/ide-desktop/lib/content/src/index.ts @@ -10,6 +10,7 @@ import * as dashboard from 'enso-authentication' import * as detect from 'enso-common/src/detect' import * as app from '../../../../../target/ensogl-pack/linked-dist' +import * as remoteLog from './remoteLog' import GLOBAL_CONFIG from '../../../../gui/config.yaml' assert { type: 'yaml' } const logger = app.log.logger @@ -159,7 +160,7 @@ class Main implements AppRunner { /** Run an app instance with the specified configuration. * This includes the scene to run and the WebSocket endpoints to the backend. */ - async runApp(inputConfig?: StringConfig | null) { + async runApp(inputConfig?: StringConfig | null, accessToken?: string) { this.stopApp() /** FIXME: https://github.com/enso-org/enso/issues/6475 @@ -174,7 +175,7 @@ class Main implements AppRunner { inputConfig ) - this.app = new app.App({ + const newApp = new app.App({ config, configOptions: contentConfig.OPTIONS, packageInfo: { @@ -183,12 +184,27 @@ class Main implements AppRunner { }, }) + // We override the remote logger stub with the "real" one. Eventually the runner should not be aware of the + // remote logger at all, and it should be integrated with our logging infrastructure. + const remoteLogger = accessToken != null ? new remoteLog.RemoteLogger(accessToken) : null + newApp.remoteLog = async (message: string, metadata: unknown) => { + if (newApp.config.options.dataCollection.value && remoteLogger) { + await remoteLogger.remoteLog(message, metadata) + } else { + const logMessage = [ + 'Not sending log to remote server. Data collection is disabled.', + `Message: "${message}"`, + `Metadata: ${JSON.stringify(metadata)}`, + ].join(' ') + + logger.log(logMessage) + } + } + this.app = newApp + if (!this.app.initialized) { console.error('Failed to initialize the application.') } else { - if (contentConfig.OPTIONS.options.dataCollection.value) { - // TODO: Add remote-logging here. - } if (!(await checkMinSupportedVersion(contentConfig.OPTIONS))) { displayDeprecatedVersionDialog() } else { @@ -270,7 +286,7 @@ class Main implements AppRunner { isAuthenticationDisabled: !config.shouldUseAuthentication, shouldShowDashboard: config.shouldUseNewDashboard, initialProjectName: config.initialProjectName, - onAuthenticated: () => { + onAuthenticated: (accessToken?: string) => { if (config.isInAuthenticationFlow) { const initialUrl = localStorage.getItem(INITIAL_URL_KEY) if (initialUrl != null) { @@ -287,7 +303,7 @@ class Main implements AppRunner { ideElement.style.display = '' } if (this.app == null) { - void this.runApp(config.inputConfig) + void this.runApp(config.inputConfig, accessToken) } } }, diff --git a/app/ide-desktop/lib/content/src/remoteLog.ts b/app/ide-desktop/lib/content/src/remoteLog.ts new file mode 100644 index 00000000000..91f323268ed --- /dev/null +++ b/app/ide-desktop/lib/content/src/remoteLog.ts @@ -0,0 +1,75 @@ +/** @file Defines the {@link RemoteLogger} class and {@link remoteLog} function for sending logs to a remote server. + * {@link RemoteLogger} provides a convenient way to manage remote logging with access token authorization. */ + +import * as app from '../../../../../target/ensogl-pack/linked-dist' +import * as authConfig from '../../dashboard/src/authentication/src/config' + +const logger = app.log.logger + +// ================= +// === Constants === +// ================= + +/** URL address where remote logs should be sent. */ +const REMOTE_LOG_URL = new URL(`${authConfig.ACTIVE_CONFIG.apiUrl}/logs`) + +// ==================== +// === RemoteLogger === +// ==================== + +// === Class === + +/** Helper class facilitating sending logs to a remote. */ +export class RemoteLogger { + /** Initialize a new instance. + * @param accessToken - JWT token used to authenticate within the cloud. */ + constructor(public accessToken: string) { + this.accessToken = accessToken + } + + /** Sends a log message to a remote. + * @param message - The log message to send. + * @param metadata - Additional metadata to send along with the log. + * @returns Promise which resolves when the log message has been sent. */ + async remoteLog(message: string, metadata: unknown): Promise { + await remoteLog(this.accessToken, message, metadata) + } +} + +// === Underlying logic === + +/** Sends a log message to a remote server using the provided access token. + * + * @param accessToken - The access token for authentication. + * @param message - The message to be logged on the server. + * @param metadata - Additional metadata to include in the log. + * @throws Will throw an error if the response from the server is not okay (response status is not 200). + * @returns Returns a promise that resolves when the log message is successfully sent. */ +export async function remoteLog( + accessToken: string, + message: string, + metadata: unknown +): Promise { + try { + const headers: HeadersInit = new Headers() + headers.set('Content-Type', 'application/json') + headers.set('Authorization', `Bearer ${accessToken}`) + const response = await fetch(REMOTE_LOG_URL, { + method: 'POST', + headers, + body: JSON.stringify({ message, metadata }), + }) + if (!response.ok) { + const errorMessage = `Error while sending log to a remote: Status ${response.status}.` + try { + const text = await response.text() + throw new Error(`${errorMessage} Response: ${text}.`) + } catch (error) { + throw new Error(`${errorMessage} Failed to read response: ${String(error)}.`) + } + } + } catch (error) { + logger.error(error) + throw error + } +} diff --git a/app/ide-desktop/lib/dashboard/src/authentication/src/authentication/providers/auth.tsx b/app/ide-desktop/lib/dashboard/src/authentication/src/authentication/providers/auth.tsx index 27be9cae7ea..7c4aea89fbd 100644 --- a/app/ide-desktop/lib/dashboard/src/authentication/src/authentication/providers/auth.tsx +++ b/app/ide-desktop/lib/dashboard/src/authentication/src/authentication/providers/auth.tsx @@ -166,7 +166,7 @@ export interface AuthProviderProps { supportsLocalBackend: boolean authService: authServiceModule.AuthService /** Callback to execute once the user has authenticated successfully. */ - onAuthenticated: () => void + onAuthenticated: (accessToken?: string) => void children: React.ReactNode } @@ -290,7 +290,7 @@ export function AuthProvider(props: AuthProviderProps) { /** Execute the callback that should inform the Electron app that the user has logged in. * This is done to transition the app from the authentication/dashboard view to the IDE. */ - onAuthenticated() + onAuthenticated(accessToken) } setUserSession(newUserSession) diff --git a/app/ide-desktop/lib/dashboard/src/authentication/src/authentication/service.tsx b/app/ide-desktop/lib/dashboard/src/authentication/src/authentication/service.tsx index 1c45c3cf1aa..3839c7d770d 100644 --- a/app/ide-desktop/lib/dashboard/src/authentication/src/authentication/service.tsx +++ b/app/ide-desktop/lib/dashboard/src/authentication/src/authentication/service.tsx @@ -62,6 +62,17 @@ const BASE_AMPLIFY_CONFIG = { /** Collection of configuration details for Amplify user pools, sorted by deployment environment. */ const AMPLIFY_CONFIGS = { + /** Configuration for @indiv0's Cognito user pool. */ + npekin: { + userPoolId: newtype.asNewtype('eu-west-1_AXX1gMvpx'), + userPoolWebClientId: newtype.asNewtype( + '1rpnb2n1ijn6o5529a7ob017o' + ), + domain: newtype.asNewtype( + 'npekin-enso-domain.auth.eu-west-1.amazoncognito.com' + ), + ...BASE_AMPLIFY_CONFIG, + } satisfies Partial, /** Configuration for @pbuchu's Cognito user pool. */ pbuchu: { userPoolId: newtype.asNewtype('eu-west-1_jSF1RbgPK'), diff --git a/app/ide-desktop/lib/dashboard/src/authentication/src/config.ts b/app/ide-desktop/lib/dashboard/src/authentication/src/config.ts index eb677a3ed15..b994b6d0052 100644 --- a/app/ide-desktop/lib/dashboard/src/authentication/src/config.ts +++ b/app/ide-desktop/lib/dashboard/src/authentication/src/config.ts @@ -26,11 +26,16 @@ const CLOUD_REDIRECTS = { /** All possible API URLs, sorted by environment. */ const API_URLS = { pbuchu: newtype.asNewtype('https://xw0g8j3tsb.execute-api.eu-west-1.amazonaws.com'), + npekin: newtype.asNewtype('https://s02ejyepk1.execute-api.eu-west-1.amazonaws.com'), production: newtype.asNewtype('https://7aqkn3tnbc.execute-api.eu-west-1.amazonaws.com'), } /** All possible configuration options, sorted by environment. */ const CONFIGS = { + npekin: { + cloudRedirect: CLOUD_REDIRECTS.development, + apiUrl: API_URLS.npekin, + } satisfies Config, pbuchu: { cloudRedirect: CLOUD_REDIRECTS.development, apiUrl: API_URLS.pbuchu, @@ -61,7 +66,7 @@ export interface Config { /** Possible values for the environment/user we're running for and whose infrastructure we're * testing against. */ -export type Environment = 'pbuchu' | 'production' +export type Environment = 'npekin' | 'pbuchu' | 'production' // =========== // === API === diff --git a/app/ide-desktop/lib/dashboard/src/authentication/src/dashboard/components/ide.tsx b/app/ide-desktop/lib/dashboard/src/authentication/src/dashboard/components/ide.tsx index 632410619ee..ac99be1cf63 100644 --- a/app/ide-desktop/lib/dashboard/src/authentication/src/dashboard/components/ide.tsx +++ b/app/ide-desktop/lib/dashboard/src/authentication/src/dashboard/components/ide.tsx @@ -1,6 +1,7 @@ /** @file Container that launches the IDE. */ import * as React from 'react' +import * as auth from '../../authentication/providers/auth' import * as backendModule from '../backend' import * as backendProvider from '../../providers/backend' @@ -26,10 +27,11 @@ export interface IdeProps { appRunner: AppRunner } -/** The ontainer that launches the IDE. */ +/** The container that launches the IDE. */ function Ide(props: IdeProps) { const { project, appRunner } = props const { backend } = backendProvider.useBackend() + const { accessToken } = auth.useNonPartialUserSession() React.useEffect(() => { void (async () => { @@ -78,20 +80,25 @@ function Ide(props: IdeProps) { : { projectManagerUrl: GLOBAL_CONFIG.projectManagerEndpoint, } - await appRunner.runApp({ - loader: { - assetsUrl: `${assetsRoot}dynamic-assets`, - wasmUrl: `${assetsRoot}pkg-opt.wasm`, - jsUrl: `${assetsRoot}pkg${JS_EXTENSION[backend.type]}`, + await appRunner.runApp( + { + loader: { + assetsUrl: `${assetsRoot}dynamic-assets`, + wasmUrl: `${assetsRoot}pkg-opt.wasm`, + jsUrl: `${assetsRoot}pkg${JS_EXTENSION[backend.type]}`, + }, + engine: { + ...engineConfig, + preferredVersion: engineVersion, + }, + startup: { + project: project.packageName, + }, }, - engine: { - ...engineConfig, - preferredVersion: engineVersion, - }, - startup: { - project: project.packageName, - }, - }) + // Here we actually need explicit undefined. + // eslint-disable-next-line no-restricted-syntax + accessToken ?? undefined + ) } if (backend.type === backendModule.BackendType.local) { await runNewProject() diff --git a/app/ide-desktop/lib/types/types.d.ts b/app/ide-desktop/lib/types/types.d.ts index 924c68b0489..4082158cbef 100644 --- a/app/ide-desktop/lib/types/types.d.ts +++ b/app/ide-desktop/lib/types/types.d.ts @@ -13,5 +13,5 @@ interface StringConfig { * open a new IDE instance. */ interface AppRunner { stopApp: () => void - runApp: (config?: StringConfig) => Promise + runApp: (config?: StringConfig, accessToken?: string) => Promise } diff --git a/lib/rust/ensogl/pack/js/src/runner/config.json b/lib/rust/ensogl/pack/js/src/runner/config.json index 64a5d57f79f..cbff8a7b8f3 100644 --- a/lib/rust/ensogl/pack/js/src/runner/config.json +++ b/lib/rust/ensogl/pack/js/src/runner/config.json @@ -3,6 +3,10 @@ "debug": { "value": false, "description": "Controls the debug mode for the application. In this mode, all logs are printed to the console, and EnsoGL extensions are loaded. Otherwise, logs are hidden unless explicitly shown with 'showLogs'." + }, + "dataCollection": { + "value": true, + "description": "Determines whether anonymous usage data is to be collected." } }, "groups": { diff --git a/lib/rust/ensogl/pack/js/src/runner/index.ts b/lib/rust/ensogl/pack/js/src/runner/index.ts index 0a9ee57a1d6..aba03279175 100644 --- a/lib/rust/ensogl/pack/js/src/runner/index.ts +++ b/lib/rust/ensogl/pack/js/src/runner/index.ts @@ -238,12 +238,11 @@ export class App { } /** Log the message on the remote server. */ - // eslint-disable-next-line @typescript-eslint/no-unused-vars - remoteLog(message: string, data: any) { - // FIXME [PB]: https://github.com/enso-org/cloud-v2/issues/359 - // Implement remote logging. This should be done after cloud integration. - // Function interface is left intentionally for readability. - // Remove typescript error suppression after resolving fixme. + // This method is assumed to be overriden by the App's owner. Eventually it should be removed from the runner + // altogether, as it is not its responsibility. + // eslint-disable-next-line @typescript-eslint/require-await, @typescript-eslint/no-unused-vars + async remoteLog(message: string, data: any) { + console.warn('Remote logging is not set up.') } /** Initialize the browser. Set the background color, print user-facing warnings, etc. */ @@ -281,8 +280,9 @@ export class App { } } } + /** Sets application stop to true and calls drop method which removes all rust memory references - * and calls all destructors. */ + * and calls all destructors. */ stop() { this.stopped = true this.wasm?.drop()