import * as Sentry from '@sentry/ember'; import AdminXSettings from '../components/admin-x/settings'; import AuthConfiguration from 'ember-simple-auth/configuration'; import React from 'react'; import ReactDOM from 'react-dom'; import Route from '@ember/routing/route'; import ShortcutsRoute from 'ghost-admin/mixins/shortcuts-route'; import ctrlOrCmd from 'ghost-admin/utils/ctrl-or-cmd'; import windowProxy from 'ghost-admin/utils/window-proxy'; import {Debug} from '@sentry/integrations'; import {Replay} from '@sentry/replay'; import {beforeSend} from 'ghost-admin/utils/sentry'; import {importComponent} from '../components/admin-x/admin-x-component'; import {inject} from 'ghost-admin/decorators/inject'; import { isAjaxError, isNotFoundError, isUnauthorizedError } from 'ember-ajax/errors'; import {isArray as isEmberArray} from '@ember/array'; import { isMaintenanceError, isVersionMismatchError } from 'ghost-admin/services/ajax'; import {later} from '@ember/runloop'; import {inject as service} from '@ember/service'; function K() { return this; } let shortcuts = {}; shortcuts.esc = {action: 'closeMenus', scope: 'default'}; shortcuts[`${ctrlOrCmd}+s`] = {action: 'save', scope: 'all'}; // make globals available for any pulled in UMD components // - avoids external components needing to bundle React and running into multiple version errors window.React = React; window.ReactDOM = ReactDOM; export default Route.extend(ShortcutsRoute, { ajax: service(), configManager: service(), feature: service(), ghostPaths: service(), notifications: service(), router: service(), session: service(), settings: service(), ui: service(), whatsNew: service(), billing: service(), shortcuts, routeAfterAuthentication: 'home', init() { this._super(...arguments); this.router.on('routeDidChange', () => { this.notifications.displayDelayed(); }); this.ui.initBodyDragHandlers(); }, config: inject(), async beforeModel() { await this.session.setup(); return this.prepareApp(); }, async afterModel(model, transition) { this._super(...arguments); if (this.get('session.isAuthenticated')) { this.session.appLoadTransition = transition; } this._appLoaded = true; }, actions: { closeMenus() { this.ui.closeMenus(); }, didTransition() { this.session.appLoadTransition = null; this.send('closeMenus'); // Need a tiny delay here to allow the router to update to the current route later(() => { Sentry.setTag('route', this.router.currentRouteName); }, 2); }, authorizationFailed() { windowProxy.replaceLocation(AuthConfiguration.rootURL); }, // noop default for unhandled save (used from shortcuts) save: K, error(error, transition) { // unauthoirized errors are already handled in the ajax service if (isUnauthorizedError(error)) { return false; } if (isNotFoundError(error)) { if (transition) { transition.abort(); } let routeInfo = transition.to; let router = this.router; let params = []; for (let key of Object.keys(routeInfo.params)) { params.push(routeInfo.params[key]); } let url = router.urlFor(routeInfo.name, ...params) .replace(/^#\//, '') .replace(/^\//, '') .replace(/^ghost\//, ''); return this.replaceWith('error404', url); } if (isVersionMismatchError(error)) { if (transition) { transition.abort(); } this.upgradeStatus.requireUpgrade(); if (this._appLoaded) { return false; } } if (isMaintenanceError(error)) { if (transition) { transition.abort(); } this.upgradeStatus.maintenanceAlert(); if (this._appLoaded) { return false; } } if (isAjaxError(error) || error && error.payload && isEmberArray(error.payload.errors)) { this.notifications.showAPIError(error); // don't show the 500 page if we weren't navigating if (!transition) { return false; } } // fallback to 500 error page return true; } }, willDestroy() { this.ui.cleanupBodyDragHandlers(); }, async prepareApp() { await this.configManager.fetchUnauthenticated(); // init Sentry here rather than app.js so that we can use API-supplied // sentry_dsn and sentry_env rather than building it into release assets if (this.config.sentry_dsn) { const sentryConfig = { dsn: this.config.sentry_dsn, environment: this.config.sentry_env, release: `ghost@${this.config.version}`, beforeSend, ignoreErrors: [ // TransitionAborted errors surface from normal application behaviour // - https://github.com/emberjs/ember.js/issues/12505 /^TransitionAborted$/, // ResizeObserver loop errors occur often from extensions and // embedded content, generally harmless and not useful to report /^ResizeObserver loop completed with undelivered notifications/, /^ResizeObserver loop limit exceeded/, // When tasks in ember-concurrency are canceled, they sometimes lead to unhandled Promise rejections // This doesn't affect the application and is not useful to report // - http://ember-concurrency.com/docs/cancelation 'TaskCancelation' ], integrations: [] }; try { // Session Replay on errors // Docs: https://docs.sentry.io/platforms/javascript/session-replay sentryConfig.replaysOnErrorSampleRate = 1.0; sentryConfig.integrations.push( // Replace with `Sentry.replayIntegration()` once we've migrated to @sentry/ember 8.x // Docs: https://docs.sentry.io/platforms/javascript/migration/v7-to-v8/#removal-of-sentryreplay-package new Replay({ mask: ['.koenig-lexical', '.gh-dashboard'], unmask: ['[role="menu"]', '[data-testid="settings-panel"]', '.gh-nav'], maskAllText: false, maskAllInputs: true, blockAllMedia: true }) ); } catch (e) { // no-op, Session Replay is not critical console.error('Error enabling Sentry Replay:', e); // eslint-disable-line no-console } if (this.config.sentry_env === 'development') { sentryConfig.integrations.push(new Debug()); } Sentry.init(sentryConfig); } if (this.session.isAuthenticated) { try { await this.session.populateUser(); } catch (e) { await this.session.invalidate(); } await this.session.postAuthPreparation(); } if (this.config.hostSettings?.forceUpgrade) { // enforce opening the BMA in a force upgrade state this.billing.openBillingWindow(this.router.currentURL, '/pro'); } // Preload settings to avoid a delay when opening setTimeout(() => { importComponent(AdminXSettings.packageName); }, 1000); } });