From 89c7c1aab5cb2d881fdf11e688762cbb305d81be Mon Sep 17 00:00:00 2001 From: alisman Date: Fri, 11 Nov 2022 16:52:21 -0500 Subject: [PATCH] Handle errors loading annoation data more transparently with event bus and alerts. Add error boundary for js errors --- package.json | 1 + .../src/store/DefaultMutationMapperStore.ts | 5 +- src/AppStore.ts | 51 ++-- src/appBootstrapper.tsx | 10 +- src/appShell/App/Container.tsx | 145 +++++++--- .../groupComparison/GroupComparisonStore.ts | 1 - .../PatientViewPageStore.ts | 14 +- .../column/AnnotationColumnFormatter.tsx | 5 +- src/pages/resultsView/ResultsViewPageStore.ts | 263 +++++++++++++++++- .../mutationMapper/MutationMapperToolStore.ts | 33 ++- .../tools/oncoprinter/OncoprinterStore.ts | 2 +- src/shared/api/genomeNexusClientInstance.ts | 1 - src/shared/api/oncokbClientInstance.ts | 11 + .../components/errorScreen/ErrorAlert.tsx | 27 ++ .../components/errorScreen/ErrorScreen.tsx | 9 +- ...rorScreen.scss => errorScreen.module.scss} | 22 ++ .../errorScreen/errorScreen.module.scss.d.ts | 8 + src/shared/enums/ErrorEnums.ts | 3 - src/shared/errorMessages.ts | 7 + src/shared/events/eventBus.js | 5 + src/shared/lib/StoreUtils.ts | 25 +- src/shared/lib/comparison/AnalysisStore.ts | 62 ++++- src/shared/lib/errorFormatter.spec.ts | 6 +- src/shared/lib/tracking.ts | 5 +- yarn.lock | 5 + 25 files changed, 614 insertions(+), 112 deletions(-) create mode 100644 src/shared/components/errorScreen/ErrorAlert.tsx rename src/shared/components/errorScreen/{errorScreen.scss => errorScreen.module.scss} (60%) create mode 100644 src/shared/components/errorScreen/errorScreen.module.scss.d.ts delete mode 100644 src/shared/enums/ErrorEnums.ts create mode 100644 src/shared/errorMessages.ts create mode 100644 src/shared/events/eventBus.js diff --git a/package.json b/package.json index b70000ad9c6..6e1ed4f7e7c 100644 --- a/package.json +++ b/package.json @@ -194,6 +194,7 @@ "jquery": "3.6.0", "jquery-migrate": "3.0.0", "js-combinatorics": "^0.5.2", + "js-event-bus": "^1.1.1", "json-fn": "^1.1.1", "jsonpath": "^1.1.1", "jspdf": "^1.3.3", diff --git a/packages/react-mutation-mapper/src/store/DefaultMutationMapperStore.ts b/packages/react-mutation-mapper/src/store/DefaultMutationMapperStore.ts index d0764d34120..25b727da4d2 100644 --- a/packages/react-mutation-mapper/src/store/DefaultMutationMapperStore.ts +++ b/packages/react-mutation-mapper/src/store/DefaultMutationMapperStore.ts @@ -536,6 +536,9 @@ class DefaultMutationMapperStore return undefined; } }, + onError: () => { + // allow client level handler to work + }, }, undefined ); @@ -975,7 +978,7 @@ class DefaultMutationMapperStore map: { [entrezGeneId: number]: boolean }, next: CancerGene ) => { - if (next.oncokbAnnotated) { + if (next && next.oncokbAnnotated) { map[next.entrezGeneId] = true; } return map; diff --git a/src/AppStore.ts b/src/AppStore.ts index 9c47bdb1afd..5248dd1ad4c 100644 --- a/src/AppStore.ts +++ b/src/AppStore.ts @@ -10,11 +10,13 @@ import client from 'shared/api/cbioportalClientInstance'; import { sendSentryMessage } from './shared/lib/tracking'; import { FeatureFlagStore } from 'shared/FeatureFlagStore'; -export type SiteError = { - errorObj: any; - dismissed: boolean; - title?: string; -}; +export class SiteError { + constructor( + public errorObj: any, + public displayType: 'alert' | 'site' = 'site', + public title?: string + ) {} +} export class AppStore { constructor( @@ -27,9 +29,9 @@ export class AppStore { sendSentryMessage('ERRORHANDLER:' + error); } catch (ex) {} - if (error.status && /400|500|403/.test(error.status)) { + if (error.status && /400|500|5\d\d|403/.test(error.status)) { sendSentryMessage('ERROR DIALOG SHOWN:' + error); - this.siteErrors.push({ errorObj: error, dismissed: false }); + this.siteErrors.push(new SiteError(new Error(error))); } }); } @@ -44,7 +46,9 @@ export class AppStore { @observable private _appReady = false; - @observable siteErrors: SiteError[] = []; + siteErrors = observable.array(); + + alertErrors = observable.array(); @observable.ref userName: string | undefined = undefined; @@ -76,30 +80,29 @@ export class AppStore { } } - @computed get undismissedSiteErrors() { - return _.filter(this.siteErrors.slice(), err => !err.dismissed); - } - @computed get isErrorCondition() { - return this.undismissedSiteErrors.length > 0; + return this.siteErrors.length > 0; } @action public dismissErrors() { - this.siteErrors = this.siteErrors.map(err => { - err.dismissed = true; - return err; - }); + this.siteErrors.clear(); } - @action public addError(err: String | SiteError) { - if (_.isString(err)) { - this.siteErrors.push({ - errorObj: { message: err }, - dismissed: false, - }); + @action + public dismissError(err: SiteError) { + this.siteErrors.remove(err); + } + + @action public addError(err: SiteError | string) { + if (typeof err === 'string') { + this.siteErrors.push(new SiteError(new Error(err))); } else { - this.siteErrors.push({ errorObj: err, dismissed: false }); + if (err.displayType === 'alert') { + this.alertErrors.push(err); + } else { + this.siteErrors.push(err); + } } } diff --git a/src/appBootstrapper.tsx b/src/appBootstrapper.tsx index bf626cecbb2..e18cfa269b3 100755 --- a/src/appBootstrapper.tsx +++ b/src/appBootstrapper.tsx @@ -22,10 +22,10 @@ import * as superagent from 'superagent'; import { buildCBioPortalPageUrl } from './shared/api/urls'; import browser from 'bowser'; import { setNetworkListener } from './shared/lib/ajaxQuiet'; -import { initializeTracking } from 'shared/lib/tracking'; +import { initializeTracking, sendToLoggly } from 'shared/lib/tracking'; import superagentCache from 'superagent-cache'; import { getBrowserWindow } from 'cbioportal-frontend-commons'; -import { AppStore } from './AppStore'; +import { AppStore, SiteError } from './AppStore'; import { handleLongUrls } from 'shared/lib/handleLongUrls'; import 'shared/polyfill/canvasToBlob'; import { setCurrentURLHeader } from 'shared/lib/extraHeader'; @@ -33,6 +33,7 @@ import Container from 'appShell/App/Container'; import { IServerConfig } from 'config/IAppConfig'; import { initializeGenericAssayServerConfig } from 'shared/lib/GenericAssayUtils/GenericAssayConfig'; import { FeatureFlagStore } from 'shared/FeatureFlagStore'; +import eventBus from 'shared/events/eventBus'; export interface ICBioWindow { globalStores: { @@ -163,6 +164,11 @@ const stores = { browserWindow.globalStores = stores; +eventBus.on('error', (err: SiteError) => { + sendToLoggly(err?.errorObj?.message); + stores.appStore.addError(err); +}); + //@ts-ignore const end = superagent.Request.prototype.end; diff --git a/src/appShell/App/Container.tsx b/src/appShell/App/Container.tsx index 1bd01e41fcb..aa66fb43caf 100644 --- a/src/appShell/App/Container.tsx +++ b/src/appShell/App/Container.tsx @@ -30,6 +30,10 @@ import { import makeRoutes from 'routes'; import { AppContext } from 'cbioportal-frontend-commons'; import { IAppContext } from 'cbioportal-frontend-commons'; +import { ErrorAlert } from 'shared/components/errorScreen/ErrorAlert'; +import { ErrorInfo } from 'react'; +import { observable } from 'mobx'; +import { sendToLoggly } from 'shared/lib/tracking'; interface IContainerProps { location: Location; @@ -96,57 +100,110 @@ export default class Container extends React.Component { return ( -
- - - {getServerConfig().skin_title} - - + +
+ + + {getServerConfig().skin_title} + + -
- +
+ - {shouldShowStudyViewWarning() && } + {shouldShowStudyViewWarning() && } - {shouldShowGenieWarning() && } + {shouldShowGenieWarning() && } -
- +
+ +
+ + +
+ + Return to homepage + + } + errorLog={formatErrorLog( + this.appStore.siteErrors + )} + errorMessages={formatErrorMessages( + this.appStore.siteErrors + )} + /> +
+
+ +
+ + + {makeRoutes()} +
+
+
- - -
- - Return to homepage - - } - errorLog={formatErrorLog( - this.appStore.undismissedSiteErrors - )} - errorMessages={formatErrorMessages( - this.appStore.undismissedSiteErrors - )} - /> -
-
- -
{makeRoutes()}
-
-
-
+ ); } } + +class ErrorBoundary extends React.Component< + any, + { hasError: boolean; error?: Error } +> { + @observable hasError = false; + + constructor(props: any) { + super(props); + this.state = { + hasError: false, + }; + } + + static getDerivedStateFromError(error: any) { + // Update state so the next render will show the fallback UI. + return { hasError: true, error }; + } + + componentDidCatch(error: Error, errorInfo: ErrorInfo) { + // You can also log the error to an error reporting service + + sendToLoggly(error.message, 'ERROR_JS'); + this.hasError = true; + } + + render() { + if (this.state.hasError) { + // fallback UI + return ( + + ); + } else { + return this.props.children; + } + } +} diff --git a/src/pages/groupComparison/GroupComparisonStore.ts b/src/pages/groupComparison/GroupComparisonStore.ts index 05dc2228ecd..c5134d5c515 100644 --- a/src/pages/groupComparison/GroupComparisonStore.ts +++ b/src/pages/groupComparison/GroupComparisonStore.ts @@ -10,7 +10,6 @@ import { SampleFilter, CancerStudy, MutationMultipleStudyFilter, - SampleMolecularIdentifier, GenePanelDataMultipleStudyFilter, Mutation, Gene, diff --git a/src/pages/patientView/clinicalInformation/PatientViewPageStore.ts b/src/pages/patientView/clinicalInformation/PatientViewPageStore.ts index 789bb4c3b95..d7900a61820 100644 --- a/src/pages/patientView/clinicalInformation/PatientViewPageStore.ts +++ b/src/pages/patientView/clinicalInformation/PatientViewPageStore.ts @@ -136,7 +136,7 @@ import { groupTrialMatchesById } from '../trialMatch/TrialMatchTableUtils'; import { GeneFilterOption } from '../mutation/GeneFilterMenu'; import TumorColumnFormatter from '../mutation/column/TumorColumnFormatter'; import { getVariantAlleleFrequency } from 'shared/lib/MutationUtils'; -import { AppStore, SiteError } from 'AppStore'; +import { AppStore, SiteError } from '../../../AppStore'; import { getGeneFilterDefault } from './PatientViewPageStoreUtil'; import { checkNonProfiledGenesExist } from '../PatientViewPageUtils'; import autobind from 'autobind-decorator'; @@ -761,11 +761,7 @@ export class PatientViewPageStore { this.sampleId ), onError: (err: Error) => { - this.appStore.siteErrors.push({ - errorObj: err, - dismissed: false, - title: 'Samples / Patients not valid', - } as SiteError); + this.appStore.siteErrors.push(new SiteError(err)); }, }, [] @@ -1684,6 +1680,7 @@ export class PatientViewPageStore { return Promise.resolve([]); } }, + onError: () => {}, }, [] ); @@ -1719,7 +1716,7 @@ export class PatientViewPageStore { map: { [entrezGeneId: number]: boolean }, next: CancerGene ) => { - if (next.oncokbAnnotated) { + if (next?.oncokbAnnotated) { map[next.entrezGeneId] = true; } return map; @@ -1731,6 +1728,7 @@ export class PatientViewPageStore { return Promise.resolve({}); } }, + onError: () => {}, }, {} ); @@ -2453,6 +2451,7 @@ export class PatientViewPageStore { this.oncoKbAnnotatedGenes, this.mutationData ), + onError: () => {}, }, ONCOKB_DEFAULT ); @@ -2714,5 +2713,6 @@ export class PatientViewPageStore { .value(); }, default: {}, + onError: () => {}, }); } diff --git a/src/pages/patientView/copyNumberAlterations/column/AnnotationColumnFormatter.tsx b/src/pages/patientView/copyNumberAlterations/column/AnnotationColumnFormatter.tsx index 672aeb4993d..b98e6cc2460 100644 --- a/src/pages/patientView/copyNumberAlterations/column/AnnotationColumnFormatter.tsx +++ b/src/pages/patientView/copyNumberAlterations/column/AnnotationColumnFormatter.tsx @@ -53,7 +53,10 @@ export default class AnnotationColumnFormatter { let oncoKbGeneExist = false; let isOncoKbCancerGene = false; - if (oncoKbCancerGenes && !(oncoKbCancerGenes instanceof Error)) { + if ( + oncoKbCancerGenes && + !(oncoKbCancerGenes.result instanceof Error) + ) { oncoKbGeneExist = _.find( oncoKbCancerGenes.result, diff --git a/src/pages/resultsView/ResultsViewPageStore.ts b/src/pages/resultsView/ResultsViewPageStore.ts index 1f4683e6735..cbb2465016b 100644 --- a/src/pages/resultsView/ResultsViewPageStore.ts +++ b/src/pages/resultsView/ResultsViewPageStore.ts @@ -196,10 +196,9 @@ import { makeProfiledInClinicalAttributes, } from '../../shared/components/oncoprint/ResultsViewOncoprintUtils'; import { annotateAlterationTypes } from '../../shared/lib/oql/annotateAlterationTypes'; -import { ErrorMessages } from '../../shared/enums/ErrorEnums'; import sessionServiceClient from '../../shared/api/sessionServiceInstance'; import comparisonClient from '../../shared/api/comparisonGroupClientInstance'; -import { AppStore } from '../../AppStore'; +import { AppStore, SiteError } from '../../AppStore'; import { getNumSamples } from '../groupComparison/GroupComparisonUtils'; import autobind from 'autobind-decorator'; import { @@ -299,6 +298,8 @@ import { getAlterationData } from 'shared/components/oncoprint/OncoprintUtils'; import { PageUserSession } from 'shared/userSession/PageUserSession'; import { PageType } from 'shared/userSession/PageType'; import { ClinicalTrackConfig } from 'shared/components/oncoprint/Oncoprint'; +import eventBus from 'shared/events/eventBus'; +import { ErrorMessages } from 'shared/errorMessages'; import AnalysisStore from 'shared/lib/comparison/AnalysisStore'; import { compileMutations, @@ -3962,6 +3963,9 @@ export class ResultsViewPageStore extends AnalysisStore } return _.flatten(await Promise.all(promises)); }, + onError: e => { + eventBus.emit('error', null, new SiteError(e)); + }, }, [] ); @@ -4266,10 +4270,6 @@ export class ResultsViewPageStore extends AnalysisStore : getServerConfig().ensembl_transcript_url; } - @computed get genomeNexusClient() { - return new GenomeNexusAPI(this.referenceGenomeBuild); - } - //this is only required to show study name and description on the results page //CancerStudy objects for all the cohortIds readonly queriedStudies = remoteData({ @@ -4588,7 +4588,7 @@ export class ResultsViewPageStore extends AnalysisStore ) { return genes; } else { - throw new Error(ErrorMessages.InvalidGenes); + throw new Error(ErrorMessages.INVALID_GENES); } }, onResult: (genes: Gene[]) => { @@ -4780,6 +4780,14 @@ export class ResultsViewPageStore extends AnalysisStore }, }); + readonly entrezGeneIdToGene = remoteData<{ [entrezGeneId: number]: Gene }>({ + await: () => [this.genes], + invoke: () => + Promise.resolve( + _.keyBy(this.genes.result!, gene => gene.entrezGeneId) + ), + }); + readonly genesetLinkMap = remoteData<{ [genesetId: string]: string }>({ invoke: async () => { if (this.genesetIds && this.genesetIds.length) { @@ -4989,6 +4997,23 @@ export class ResultsViewPageStore extends AnalysisStore } ); + readonly _filteredAndAnnotatedMutationsReport = remoteData({ + await: () => [ + this.mutations, + this.getMutationPutativeDriverInfo, + this.entrezGeneIdToGene, + ], + invoke: () => { + return Promise.resolve( + filterAndAnnotateMutations( + this.mutations.result!, + this.getMutationPutativeDriverInfo.result!, + this.entrezGeneIdToGene.result! + ) + ); + }, + }); + readonly _filteredAndAnnotatedStructuralVariantsReport = remoteData({ await: () => [ this.structuralVariants, @@ -5004,6 +5029,27 @@ export class ResultsViewPageStore extends AnalysisStore }, }); + // readonly filteredAndAnnotatedMutations = remoteData({ + // await: () => [ + // this._filteredAndAnnotatedMutationsReport, + // this.filteredSampleKeyToSample, + // ], + // invoke: () => { + // const filteredMutations = compileMutations( + // this._filteredAndAnnotatedMutationsReport.result!, + // !this.driverAnnotationSettings.includeVUS, + // !this.includeGermlineMutations + // ); + // const filteredSampleKeyToSample = this.filteredSampleKeyToSample + // .result!; + // return Promise.resolve( + // filteredMutations.filter( + // m => m.uniqueSampleKey in filteredSampleKeyToSample + // ) + // ); + // }, + // }); + readonly filteredAndAnnotatedStructuralVariants = remoteData< AnnotatedStructuralVariant[] >({ @@ -5129,6 +5175,78 @@ export class ResultsViewPageStore extends AnalysisStore }, })); + readonly getMutationPutativeDriverInfo = remoteData({ + await: () => { + const toAwait = []; + if (this.driverAnnotationSettings.oncoKb) { + toAwait.push(this.oncoKbMutationAnnotationForOncoprint); + } + if (this.driverAnnotationSettings.hotspots) { + toAwait.push(this.isHotspotForOncoprint); + } + if (this.driverAnnotationSettings.cbioportalCount) { + toAwait.push(this.getCBioportalCount); + } + if (this.driverAnnotationSettings.cosmicCount) { + toAwait.push(this.getCosmicCount); + } + return toAwait; + }, + invoke: () => { + return Promise.resolve((mutation: Mutation): { + oncoKb: string; + hotspots: boolean; + cbioportalCount: boolean; + cosmicCount: boolean; + customDriverBinary: boolean; + customDriverTier?: string; + } => { + const getOncoKbMutationAnnotationForOncoprint = this + .oncoKbMutationAnnotationForOncoprint.result!; + const oncoKbDatum: + | IndicatorQueryResp + | undefined + | null + | false = + this.driverAnnotationSettings.oncoKb && + getOncoKbMutationAnnotationForOncoprint && + !( + getOncoKbMutationAnnotationForOncoprint instanceof Error + ) && + getOncoKbMutationAnnotationForOncoprint(mutation); + + const isHotspotDriver = + this.driverAnnotationSettings.hotspots && + !(this.isHotspotForOncoprint.result instanceof Error) && + this.isHotspotForOncoprint.result!(mutation); + const cbioportalCountExceeded = + this.driverAnnotationSettings.cbioportalCount && + this.getCBioportalCount.isComplete && + this.getCBioportalCount.result!(mutation) >= + this.driverAnnotationSettings.cbioportalCountThreshold; + const cosmicCountExceeded = + this.driverAnnotationSettings.cosmicCount && + this.getCosmicCount.isComplete && + this.getCosmicCount.result!(mutation) >= + this.driverAnnotationSettings.cosmicCountThreshold; + + // Note: custom driver annotations are part of the incoming datum + return evaluateMutationPutativeDriverInfo( + mutation, + oncoKbDatum, + this.driverAnnotationSettings.hotspots, + isHotspotDriver, + this.driverAnnotationSettings.cbioportalCount, + cbioportalCountExceeded, + this.driverAnnotationSettings.cosmicCount, + cosmicCountExceeded, + this.driverAnnotationSettings.customBinary, + this.driverAnnotationSettings.driverTiers + ); + }); + }, + }); + readonly getStructuralVariantPutativeDriverInfo = remoteData({ await: () => { const toAwait = []; @@ -5273,6 +5391,23 @@ export class ResultsViewPageStore extends AnalysisStore }, }); + //we need seperate oncokb data because oncoprint requires onkb queries across cancertype + //mutations tab the opposite + readonly oncoKbDataForOncoprint = remoteData( + { + await: () => [this.mutations, this.oncoKbAnnotatedGenes], + invoke: async () => + fetchOncoKbDataForOncoprint( + this.oncoKbAnnotatedGenes, + this.mutations + ), + onError: (err: Error) => { + // fail silently, leave the error handling responsibility to the data consumer + }, + }, + ONCOKB_DEFAULT + ); + readonly structuralVariantOncoKbDataForOncoprint = remoteData< IOncoKbData | Error >( @@ -5336,6 +5471,16 @@ export class ResultsViewPageStore extends AnalysisStore ); } + readonly oncoKbMutationAnnotationForOncoprint = remoteData< + Error | ((mutation: Mutation) => IndicatorQueryResp | undefined) + >({ + await: () => [this.oncoKbDataForOncoprint], + invoke: () => + makeGetOncoKbMutationAnnotationForOncoprint( + this.oncoKbDataForOncoprint + ), + }); + readonly oncoKbStructuralVariantAnnotationForOncoprint = remoteData< | Error | (( @@ -5381,6 +5526,109 @@ export class ResultsViewPageStore extends AnalysisStore ), }); + readonly cbioportalMutationCountData = remoteData<{ + [mutationCountByPositionKey: string]: number; + }>({ + await: () => [this.mutations], + invoke: async () => { + const mutationPositionIdentifiers = _.values( + countMutations(this.mutations.result!) + ); + + if (mutationPositionIdentifiers.length > 0) { + const data = await internalClient.fetchMutationCountsByPositionUsingPOST( + { + mutationPositionIdentifiers, + } + ); + return _.mapValues( + _.groupBy(data, mutationCountByPositionKey), + (counts: MutationCountByPosition[]) => + _.sumBy(counts, c => c.count) + ); + } else { + return {}; + } + }, + }); + + readonly getCBioportalCount: MobxPromise< + (mutation: Mutation) => number + > = remoteData({ + await: () => [this.cbioportalMutationCountData], + invoke: () => { + return Promise.resolve((mutation: Mutation): number => { + const key = mutationCountByPositionKey(mutation); + return this.cbioportalMutationCountData.result![key] || -1; + }); + }, + }); + //COSMIC count + readonly cosmicCountsByKeywordAndStart = remoteData({ + await: () => [this.mutations], + invoke: async () => { + const keywords = _.uniq( + this.mutations + .result!.filter((m: Mutation) => { + // keyword is what we use to query COSMIC count with, so we need + // the unique list of mutation keywords to query. If a mutation has + // no keyword, it cannot be queried for. + return !!m.keyword; + }) + .map((m: Mutation) => m.keyword) + ); + + if (keywords.length > 0) { + const data = await internalClient.fetchCosmicCountsUsingPOST({ + keywords, + }); + const map = new ComplexKeyCounter(); + for (const d of data) { + const position = getProteinPositionFromProteinChange( + d.proteinChange + ); + if (position) { + map.add( + { + keyword: d.keyword, + start: position.start, + }, + d.count + ); + } + } + return map; + } else { + return new ComplexKeyCounter(); + } + }, + }); + + readonly getCosmicCount: MobxPromise< + (mutation: Mutation) => number + > = remoteData({ + await: () => [this.cosmicCountsByKeywordAndStart], + invoke: () => { + return Promise.resolve((mutation: Mutation): number => { + const targetPosObj = getProteinPositionFromProteinChange( + mutation.proteinChange + ); + if (targetPosObj) { + const keyword = mutation.keyword; + const cosmicCount = this.cosmicCountsByKeywordAndStart.result!.get( + { + keyword, + start: targetPosObj.start, + } + ); + return cosmicCount; + } else { + return -1; + } + }); + }, + }); + readonly molecularProfileIdToProfiledFilteredSamples = remoteData({ await: () => [ this.filteredSamples, @@ -5557,6 +5805,7 @@ export class ResultsViewPageStore extends AnalysisStore ), }); + readonly geneCache = new GeneCache(); readonly genesetCache = new GenesetCache(); private _numericGeneMolecularDataCache = new MobxPromiseCache< diff --git a/src/pages/staticPages/tools/mutationMapper/MutationMapperToolStore.ts b/src/pages/staticPages/tools/mutationMapper/MutationMapperToolStore.ts index ce6732351c9..d0c03fde041 100644 --- a/src/pages/staticPages/tools/mutationMapper/MutationMapperToolStore.ts +++ b/src/pages/staticPages/tools/mutationMapper/MutationMapperToolStore.ts @@ -53,6 +53,9 @@ import { getGenomeNexusHgvsgUrl } from 'shared/api/urls'; import { GENOME_NEXUS_ARG_FIELD_ENUM } from 'shared/constants'; import { getServerConfig } from 'config/config'; import { REFERENCE_GENOME } from 'shared/lib/referenceGenomeUtils'; +import eventBus from 'shared/events/eventBus'; +import { SiteError } from 'AppStore'; +import { ErrorMessages } from 'shared/errorMessages'; export default class MutationMapperToolStore { @observable mutationData: Partial[] | undefined; @@ -104,15 +107,41 @@ export default class MutationMapperToolStore { } @computed get genomeNexusClient() { - return this.grch38GenomeNexusUrl + const client = this.grch38GenomeNexusUrl ? new GenomeNexusAPI(this.grch38GenomeNexusUrl) : defaultGenomeNexusClient; + + client.addErrorHandler(err => { + eventBus.emit( + 'error', + null, + new SiteError( + new Error(ErrorMessages.GENOME_NEXUS_LOAD_ERROR), + 'alert' + ) + ); + }); + + return client; } @computed get genomeNexusInternalClient() { - return this.grch38GenomeNexusUrl + const client = this.grch38GenomeNexusUrl ? new GenomeNexusAPIInternal(this.grch38GenomeNexusUrl) : defaultGenomeNexusInternalClient; + + client.addErrorHandler(err => { + eventBus.emit( + 'error', + null, + new SiteError( + new Error(ErrorMessages.GENOME_NEXUS_LOAD_ERROR), + 'alert' + ) + ); + }); + + return client; } readonly hugoGeneSymbols = remoteData( diff --git a/src/pages/staticPages/tools/oncoprinter/OncoprinterStore.ts b/src/pages/staticPages/tools/oncoprinter/OncoprinterStore.ts index 01686abeaa8..bfc7e0a8dd9 100644 --- a/src/pages/staticPages/tools/oncoprinter/OncoprinterStore.ts +++ b/src/pages/staticPages/tools/oncoprinter/OncoprinterStore.ts @@ -335,7 +335,7 @@ export default class OncoprinterStore { map: { [entrezGeneId: number]: boolean }, next: CancerGene ) => { - if (next.oncokbAnnotated) { + if (next?.oncokbAnnotated) { map[next.entrezGeneId] = true; } return map; diff --git a/src/shared/api/genomeNexusClientInstance.ts b/src/shared/api/genomeNexusClientInstance.ts index 377d945d810..e7d5b325fca 100644 --- a/src/shared/api/genomeNexusClientInstance.ts +++ b/src/shared/api/genomeNexusClientInstance.ts @@ -13,6 +13,5 @@ async function checkVersion(client: GenomeNexusAPI) { } const client = new GenomeNexusAPI(); -//checkVersion(client); export default client; diff --git a/src/shared/api/oncokbClientInstance.ts b/src/shared/api/oncokbClientInstance.ts index 03cd116e5ff..68bd317dd1b 100644 --- a/src/shared/api/oncokbClientInstance.ts +++ b/src/shared/api/oncokbClientInstance.ts @@ -1,5 +1,16 @@ import { OncoKbAPI } from 'oncokb-ts-api-client'; +import eventBus from 'shared/events/eventBus'; +import { ErrorMessages } from 'shared/errorMessages'; +import { SiteError } from '../../AppStore'; const client = new OncoKbAPI(); +client.addErrorHandler(err => { + eventBus.emit( + 'error', + null, + new SiteError(new Error(ErrorMessages.ONCOKB_LOAD_ERROR), 'alert') + ); +}); + export default client; diff --git a/src/shared/components/errorScreen/ErrorAlert.tsx b/src/shared/components/errorScreen/ErrorAlert.tsx new file mode 100644 index 00000000000..a62abf1b4bb --- /dev/null +++ b/src/shared/components/errorScreen/ErrorAlert.tsx @@ -0,0 +1,27 @@ +import * as React from 'react'; +import { AppStore } from 'AppStore'; +import { observer } from 'mobx-react'; +import styles from './errorScreen.module.scss'; +import classNames from 'classnames'; +import _ from 'lodash'; + +export const ErrorAlert: React.FunctionComponent<{ + appStore: AppStore; +}> = observer(function({ appStore }) { + const errorGroups = _.groupBy( + appStore.alertErrors, + e => e.errorObj.message + ); + + return appStore.alertErrors.length ? ( +
+ {_.map(errorGroups, (errors, message) => { + return

{message}

; + })} + appStore.alertErrors.clear()} + /> +
+ ) : null; +}); diff --git a/src/shared/components/errorScreen/ErrorScreen.tsx b/src/shared/components/errorScreen/ErrorScreen.tsx index ecd1147e380..058f46cb386 100644 --- a/src/shared/components/errorScreen/ErrorScreen.tsx +++ b/src/shared/components/errorScreen/ErrorScreen.tsx @@ -2,7 +2,7 @@ import * as React from 'react'; import _ from 'lodash'; import { getBrowserWindow } from 'cbioportal-frontend-commons'; import { observer } from 'mobx-react'; -import './errorScreen.scss'; +import styles from './errorScreen.module.scss'; import { getServerConfig } from 'config/config'; import { buildCBioPortalPageUrl } from 'shared/api/urls'; import { computed, makeObservable } from 'mobx'; @@ -57,8 +57,11 @@ export default class ErrorScreen extends React.Component< const subject = 'cBioPortal user reported error'; return ( -
- +
+ cBioPortal Logo { - return await client.utilsCancerGeneListGetUsingGET_1({}); + return await client.utilsCancerGeneListGetUsingGET_1({}).catch(d => { + eventBus.emit( + 'error', + null, + new SiteError(new Error(ErrorMessages.ONCOKB_LOAD_ERROR), 'alert') + ); + return d; + }); } export async function fetchOncoKbInfo( client: OncoKbAPI = oncokbClient ): Promise { - return await client.infoGetUsingGET_1({}); + return await client.infoGetUsingGET_1({}).catch(d => { + eventBus.emit( + 'error', + null, + new SiteError(new Error(ErrorMessages.ONCOKB_LOAD_ERROR), 'alert') + ); + return d; + }); } export async function fetchOncoKbData( @@ -1625,7 +1643,8 @@ export async function fetchOncoKbDataForOncoprint( 'ONCOGENIC' ); } catch (e) { - result = new Error(); + result = new Error(ErrorMessages.ONCOKB_LOAD_ERROR); + eventBus.emit('error', null, new SiteError(result, 'alert')); } return result; } else { diff --git a/src/shared/lib/comparison/AnalysisStore.ts b/src/shared/lib/comparison/AnalysisStore.ts index 1e7e21e8313..573d6b30b2e 100644 --- a/src/shared/lib/comparison/AnalysisStore.ts +++ b/src/shared/lib/comparison/AnalysisStore.ts @@ -31,13 +31,19 @@ import { IOncoKbData, } from 'cbioportal-utils'; import { fetchHotspotsData } from '../CancerHotspotsUtils'; -import { GenomeNexusAPIInternal } from 'genome-nexus-ts-api-client'; +import { + GenomeNexusAPI, + GenomeNexusAPIInternal, +} from 'genome-nexus-ts-api-client'; import { countMutations, mutationCountByPositionKey, } from 'pages/resultsView/mutationCountHelpers'; import ComplexKeyCounter from '../complexKeyDataStructures/ComplexKeyCounter'; import GeneCache from 'shared/cache/GeneCache'; +import eventBus from 'shared/events/eventBus'; +import { SiteError } from 'AppStore'; +import { ErrorMessages } from 'shared/errorMessages'; export default abstract class AnalysisStore { @observable driverAnnotationSettings: DriverAnnotationSettings; @@ -58,6 +64,7 @@ export default abstract class AnalysisStore { return Promise.resolve([]); } }, + onError: () => {}, }, [] ); @@ -66,7 +73,10 @@ export default abstract class AnalysisStore { { await: () => [this.oncoKbCancerGenes], invoke: () => { - if (getServerConfig().show_oncokb) { + if ( + getServerConfig().show_oncokb && + !_.isError(this.oncoKbCancerGenes.result) + ) { return Promise.resolve( _.reduce( this.oncoKbCancerGenes.result, @@ -74,7 +84,7 @@ export default abstract class AnalysisStore { map: { [entrezGeneId: number]: boolean }, next: CancerGene ) => { - if (next.oncokbAnnotated) { + if (next?.oncokbAnnotated) { map[next.entrezGeneId] = true; } return map; @@ -86,6 +96,7 @@ export default abstract class AnalysisStore { return Promise.resolve({}); } }, + onError: e => {}, }, {} ); @@ -97,8 +108,38 @@ export default abstract class AnalysisStore { return getGenomeNexusUrl(this.studies.result); } + @computed get genomeNexusClient() { + const client = new GenomeNexusAPI(this.referenceGenomeBuild); + + client.addErrorHandler(err => { + eventBus.emit( + 'error', + null, + new SiteError( + new Error(ErrorMessages.GENOME_NEXUS_LOAD_ERROR), + 'alert' + ) + ); + }); + + return client; + } + @computed get genomeNexusInternalClient() { - return new GenomeNexusAPIInternal(this.referenceGenomeBuild); + const client = new GenomeNexusAPIInternal(this.referenceGenomeBuild); + + client.addErrorHandler(err => { + eventBus.emit( + 'error', + null, + new SiteError( + new Error(ErrorMessages.GENOME_NEXUS_LOAD_ERROR), + 'alert' + ) + ); + }); + + return client; } readonly entrezGeneIdToGene = remoteData<{ [entrezGeneId: number]: Gene }>({ @@ -196,6 +237,7 @@ export default abstract class AnalysisStore { ); }); }, + onError: () => {}, }); // Hotspots @@ -208,17 +250,20 @@ export default abstract class AnalysisStore { this.genomeNexusInternalClient ); }, + onError: () => {}, }); readonly indexedHotspotData = remoteData({ await: () => [this.hotspotData], invoke: () => Promise.resolve(indexHotspotsData(this.hotspotData)), + onError: () => {}, }); public readonly isHotspotForOncoprint = remoteData< ((m: Mutation) => boolean) | Error >({ invoke: () => makeIsHotspotForOncoprint(this.indexedHotspotData), + onError: () => {}, }); //we need seperate oncokb data because oncoprint requires onkb queries across cancertype @@ -226,11 +271,12 @@ export default abstract class AnalysisStore { readonly oncoKbDataForOncoprint = remoteData( { await: () => [this.mutations, this.oncoKbAnnotatedGenes], - invoke: async () => - fetchOncoKbDataForOncoprint( + invoke: async () => { + return await fetchOncoKbDataForOncoprint( this.oncoKbAnnotatedGenes, this.mutations - ), + ); + }, onError: (err: Error) => { // fail silently, leave the error handling responsibility to the data consumer }, @@ -246,6 +292,7 @@ export default abstract class AnalysisStore { makeGetOncoKbMutationAnnotationForOncoprint( this.oncoKbDataForOncoprint ), + onError: () => {}, }); readonly cbioportalMutationCountData = remoteData<{ @@ -350,6 +397,7 @@ export default abstract class AnalysisStore { } }); }, + onError: () => {}, }); readonly geneCache = new GeneCache(); diff --git a/src/shared/lib/errorFormatter.spec.ts b/src/shared/lib/errorFormatter.spec.ts index a980d487727..7ba6c2aca4d 100644 --- a/src/shared/lib/errorFormatter.spec.ts +++ b/src/shared/lib/errorFormatter.spec.ts @@ -20,18 +20,18 @@ const siteErrors: SiteError[] = [ }, }, } as any, - dismissed: false, + displayType: 'alert', }, { errorObj: { message: REQUEST_ERROR_MESSAGE, } as any, - dismissed: false, + displayType: 'alert', title: EXAMPLE_TITLE_1, }, { errorObj: { otherInformation: '' } as any, - dismissed: false, + displayType: 'alert', title: EXAMPLE_TITLE_2, }, ]; diff --git a/src/shared/lib/tracking.ts b/src/shared/lib/tracking.ts index 78cfce4b17a..c637e10e5a1 100644 --- a/src/shared/lib/tracking.ts +++ b/src/shared/lib/tracking.ts @@ -70,9 +70,9 @@ export function serializeEvent(gaEvent: GAEvent) { } catch (ex) {} } -function sendToLoggly(message: string) { +export function sendToLoggly(message: string, tag?: string) { try { - if (window.location.hostname === 'www.cbioportal.org') { + if (/cbioportal\.org$/.test(window.location.hostname)) { const LOGGLY_TOKEN = 'b7a422a1-9878-49a2-8a30-2a8d5d33518f'; $.ajax({ @@ -80,6 +80,7 @@ function sendToLoggly(message: string) { data: { location: window.location.href.replace(/#.*$/, ''), message: message, + tag, e2e: isWebdriver() ? 'true' : 'false', }, }); diff --git a/yarn.lock b/yarn.lock index e8a016e7198..a9cb06cb9c0 100644 --- a/yarn.lock +++ b/yarn.lock @@ -14505,6 +14505,11 @@ js-combinatorics@^0.5.2: resolved "https://registry.yarnpkg.com/js-combinatorics/-/js-combinatorics-0.5.4.tgz#c92916b8f8171b64ecd7c4435b72cfabc803c756" integrity sha512-PCqUIKGqv/Kjao1G4GE/Yni6QkCP2nWW3KnxL+8IGWPlP18vQpT8ufGMf4XUAAY8JHEryUCJbf51zG8329ntMg== +js-event-bus@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/js-event-bus/-/js-event-bus-1.1.1.tgz#7a7a92e2bb4a0bb8cfdbe3853904146e8819d788" + integrity sha512-clZBV/Bzxw3sPB1ugeBANkPWw1noQWwa7MJAtiEXBfw3CH4wptA6Nh7+b9rqi1/z0tjo6t7wILrGGg3IHRAOPw== + js-levenshtein@^1.1.3: version "1.1.6" resolved "https://registry.yarnpkg.com/js-levenshtein/-/js-levenshtein-1.1.6.tgz#c6cee58eb3550372df8deb85fad5ce66ce01d59d"