From 2dc857b986fea4ae2e6e3161fa3c143ba6d13b00 Mon Sep 17 00:00:00 2001 From: milanmajchrak Date: Tue, 30 Jun 2026 10:11:00 +0200 Subject: [PATCH 1/2] UoE/Remove debug console output and avoidable failed requests Make the running app look professional by removing development console logging and eliminating avoidable failed network requests. Each issue was reproduced against the live dev and production instances and traced to its source. Console noise removed (fired during normal browser use): - "Bitstream: " logged once per file on every item page - "Environment extended with app config" on every page load - debug logs across the submission, upload, access-control and datashare services, the submission footer and the section container Avoidable failed requests eliminated (the request is now avoided, since a browser always logs a network 404/401 that JS cannot suppress): - 404 google.analytics.key on every page -> gated behind a new info.enableGoogleAnalytics flag (default false; DataShare does not use GA) - 404 bulkedit.export.max.items on /search -> only requested once the admin export button is shown - 401 qualityassurancesources on item pages for anonymous users -> gated on the CanSeeQA authorization inside the QA component - 404 /favicon.ico -> the DataShare favicon is served from the web root Specs updated for the new GA flag and CanSeeQA gate (with added coverage). Co-Authored-By: Claude Opus 4.8 --- angular.json | 7 ++- config/config.example.yml | 4 ++ ...bmission-form-section-container.service.ts | 1 - .../datashare/datashare-submission.service.ts | 2 - .../file-section/file-section.component.ts | 8 ---- .../qa-event-notification.component.spec.ts | 32 ++++++++++++- .../qa-event-notification.component.ts | 48 +++++++++++++------ ...access-control-form-container.component.ts | 9 +--- .../bulk-access-control.service.ts | 2 - .../cookies/browser-klaro.service.spec.ts | 22 +++++++++ .../shared/cookies/browser-klaro.service.ts | 13 +++-- .../search-export-csv.component.ts | 25 ++++++++-- .../google-analytics.service.spec.ts | 27 +++++++++++ .../statistics/google-analytics.service.ts | 8 ++++ .../submission-form-footer.component.ts | 6 --- .../container/section-container.component.ts | 2 - .../upload/section-upload.component.ts | 4 -- src/config/config.util.ts | 1 - src/config/default-app-config.ts | 4 ++ src/config/info-config.interface.ts | 8 ++++ src/environments/environment.test.ts | 1 + 21 files changed, 176 insertions(+), 58 deletions(-) diff --git a/angular.json b/angular.json index 26b42f19709..a032f797530 100644 --- a/angular.json +++ b/angular.json @@ -40,7 +40,12 @@ "aot": true, "assets": [ "src/assets", - "src/robots.txt" + "src/robots.txt", + { + "glob": "favicon.ico", + "input": "src/themes/datashare/assets/images", + "output": "/" + } ], "styles": [ "src/styles/startup.scss", diff --git a/config/config.example.yml b/config/config.example.yml index 8ea58b96e40..154b0dea47a 100644 --- a/config/config.example.yml +++ b/config/config.example.yml @@ -465,6 +465,10 @@ info: enableEndUserAgreement: true enablePrivacyStatement: true enableCOARNotifySupport: true + # Only enable Google Analytics on installations that configure a `google.analytics.key` + # property on the backend. When false (default) the UI does not probe for that property, + # avoiding a 404 request on every page. + enableGoogleAnalytics: false # Whether to enable Markdown (https://commonmark.org/) and MathJax (https://www.mathjax.org/) # display in supported metadata fields. By default, only dc.description.abstract is supported. diff --git a/src/app/datashare/datashare-submission-form-section-container.service.ts b/src/app/datashare/datashare-submission-form-section-container.service.ts index 90c6a9e8720..4963dd6f3e5 100644 --- a/src/app/datashare/datashare-submission-form-section-container.service.ts +++ b/src/app/datashare/datashare-submission-form-section-container.service.ts @@ -19,7 +19,6 @@ export class DatashareSubmissionFormSectionContainerService { * @param id The ID of the panel to open, or null to close all panels */ setOpenPanelId(id: string | null): void { - console.log('Setting open panel ID to:', id); this.openPanelId.set(id); } } diff --git a/src/app/datashare/datashare-submission.service.ts b/src/app/datashare/datashare-submission.service.ts index e6e7de66b4b..63a4d10484d 100644 --- a/src/app/datashare/datashare-submission.service.ts +++ b/src/app/datashare/datashare-submission.service.ts @@ -23,14 +23,12 @@ export class DatashareSubmissionService { constructor(private notificationsService: NotificationsService, private translate: TranslateService, ) { - console.log('DatashareSubmissionService created'); } /** * Update the deposit button visibility state */ updatehasUploadFilesErrors(show: boolean): void { - console.log('Updating hasUploadFilesErrors to:', show); this._hasUploadFilesErrorsSignal.set(show); } diff --git a/src/app/item-page/simple/field-components/file-section/file-section.component.ts b/src/app/item-page/simple/field-components/file-section/file-section.component.ts index 6d630ff70f5..09131f6d997 100644 --- a/src/app/item-page/simple/field-components/file-section/file-section.component.ts +++ b/src/app/item-page/simple/field-components/file-section/file-section.component.ts @@ -154,10 +154,6 @@ export class FileSectionComponent implements OnInit { this.notificationsService.error(this.translateService.get('file-section.error.header'), `${bitstreamsRD.statusCode} ${bitstreamsRD.errorMessage}`); } else if (hasValue(bitstreamsRD.payload)) { const current: Bitstream[] = this.bitstreams$.getValue(); - // For debugging. - bitstreamsRD.payload.page.forEach(bitstream => { - console.log('Bitstream:', bitstream); - }); this.bitstreams$.next([...current, ...bitstreamsRD.payload.page]); this.isLoading = false; this.isLastPage = this.currentPage === bitstreamsRD.payload.totalPages; @@ -169,10 +165,6 @@ export class FileSectionComponent implements OnInit { this.notificationsService.error(this.translateService.get('file-section.error.header'), `${licenseRD.statusCode} ${licenseRD.errorMessage}`); } else if (hasValue(licenseRD.payload)) { const updated: Bitstream[] = this.bitstreams$.getValue(); - // For debugging. - licenseRD.payload.page.forEach(bitstream => { - console.log('Bitstream:', bitstream); - }); this.bitstreams$.next([...updated, ...licenseRD.payload.page]); } this.isLoading = false; diff --git a/src/app/item-page/simple/qa-event-notification/qa-event-notification.component.spec.ts b/src/app/item-page/simple/qa-event-notification/qa-event-notification.component.spec.ts index 16141b0359b..d007ffd5907 100644 --- a/src/app/item-page/simple/qa-event-notification/qa-event-notification.component.spec.ts +++ b/src/app/item-page/simple/qa-event-notification/qa-event-notification.component.spec.ts @@ -13,6 +13,7 @@ import { SplitPipe } from 'src/app/shared/utils/split.pipe'; import { APP_DATA_SERVICES_MAP } from '../../../../config/app-config.interface'; import { RemoteDataBuildService } from '../../../core/cache/builders/remote-data-build.service'; import { ObjectCacheService } from '../../../core/cache/object-cache.service'; +import { AuthorizationDataService } from '../../../core/data/feature-authorization/authorization-data.service'; import { RequestService } from '../../../core/data/request.service'; import { QualityAssuranceSourceObject } from '../../../core/notifications/qa/models/quality-assurance-source.model'; import { QualityAssuranceSourceDataService } from '../../../core/notifications/qa/source/quality-assurance-source-data.service'; @@ -28,6 +29,7 @@ describe('QaEventNotificationComponent', () => { let component: QaEventNotificationComponent; let fixture: ComponentFixture; let qualityAssuranceSourceDataServiceStub: any; + let authorizationServiceStub: any; const obj = Object.assign(new QualityAssuranceSourceObject(), { id: 'sourceName:target', @@ -41,7 +43,10 @@ describe('QaEventNotificationComponent', () => { beforeEach(async () => { qualityAssuranceSourceDataServiceStub = { - getSourcesByTarget: () => objPL, + getSourcesByTarget: jasmine.createSpy('getSourcesByTarget').and.returnValue(objPL), + }; + authorizationServiceStub = { + isAuthorized: jasmine.createSpy('isAuthorized').and.returnValue(of(true)), }; await TestBed.configureTestingModule({ imports: [CommonModule, TranslateModule.forRoot(), QaEventNotificationComponent, SplitPipe], @@ -49,6 +54,7 @@ describe('QaEventNotificationComponent', () => { { provide: APP_DATA_SERVICES_MAP, useValue: {} }, { provide: ActivatedRoute, useValue: new ActivatedRouteStub() }, { provide: QualityAssuranceSourceDataService, useValue: qualityAssuranceSourceDataServiceStub }, + { provide: AuthorizationDataService, useValue: authorizationServiceStub }, { provide: RequestService, useValue: {} }, { provide: NotificationsService, useValue: {} }, { provide: HALEndpointService, useValue: new HALEndpointServiceStub('test') }, @@ -57,6 +63,12 @@ describe('QaEventNotificationComponent', () => { provideMockStore({}), ], }) + // The component declares QualityAssuranceSourceDataService in its own `providers`, + // which would otherwise shadow the stub above with a real instance. Override it so + // the component uses the stub we can assert against. + .overrideComponent(QaEventNotificationComponent, { + set: { providers: [{ provide: QualityAssuranceSourceDataService, useValue: qualityAssuranceSourceDataServiceStub }] }, + }) .compileComponents(); fixture = TestBed.createComponent(QaEventNotificationComponent); component = fixture.componentInstance; @@ -78,4 +90,22 @@ describe('QaEventNotificationComponent', () => { const route = component.getQualityAssuranceRoute(); expect(route).toBe('/notifications/quality-assurance'); }); + + it('should request QA sources when the user is authorized to see QA', () => { + authorizationServiceStub.isAuthorized.and.returnValue(of(true)); + qualityAssuranceSourceDataServiceStub.getSourcesByTarget.calls.reset(); + let result: QualityAssuranceSourceObject[]; + component.getQualityAssuranceSources$().subscribe((sources) => result = sources); + expect(qualityAssuranceSourceDataServiceStub.getSourcesByTarget).toHaveBeenCalled(); + expect(result).toEqual([obj]); + }); + + it('should NOT request QA sources when the user is not authorized', () => { + authorizationServiceStub.isAuthorized.and.returnValue(of(false)); + qualityAssuranceSourceDataServiceStub.getSourcesByTarget.calls.reset(); + let result: QualityAssuranceSourceObject[]; + component.getQualityAssuranceSources$().subscribe((sources) => result = sources); + expect(qualityAssuranceSourceDataServiceStub.getSourcesByTarget).not.toHaveBeenCalled(); + expect(result).toEqual([]); + }); }); diff --git a/src/app/item-page/simple/qa-event-notification/qa-event-notification.component.ts b/src/app/item-page/simple/qa-event-notification/qa-event-notification.component.ts index a428f7feb4f..bef2d7adb99 100644 --- a/src/app/item-page/simple/qa-event-notification/qa-event-notification.component.ts +++ b/src/app/item-page/simple/qa-event-notification/qa-event-notification.component.ts @@ -12,14 +12,20 @@ import { } from '@angular/core'; import { RouterLink } from '@angular/router'; import { TranslateModule } from '@ngx-translate/core'; -import { Observable } from 'rxjs'; +import { + Observable, + of as observableOf, +} from 'rxjs'; import { catchError, map, + switchMap, } from 'rxjs/operators'; import { getNotificatioQualityAssuranceRoute } from '../../../admin/admin-routing-paths'; import { RequestParam } from '../../../core/cache/models/request-param.model'; +import { AuthorizationDataService } from '../../../core/data/feature-authorization/authorization-data.service'; +import { FeatureID } from '../../../core/data/feature-authorization/feature-id'; import { FindListOptions } from '../../../core/data/find-list-options.model'; import { PaginatedList } from '../../../core/data/paginated-list.model'; import { RemoteData } from '../../../core/data/remote-data'; @@ -61,6 +67,7 @@ export class QaEventNotificationComponent implements OnChanges { constructor( private qualityAssuranceSourceDataService: QualityAssuranceSourceDataService, + private authorizationService: AuthorizationDataService, ) {} /** @@ -77,20 +84,31 @@ export class QaEventNotificationComponent implements OnChanges { * Note: sourceId is composed as: id: "sourceName:" */ getQualityAssuranceSources$(): Observable { - const findListTopicOptions: FindListOptions = { - searchParams: [new RequestParam('target', this.item.uuid)], - }; - return this.qualityAssuranceSourceDataService.getSourcesByTarget(findListTopicOptions, false) - .pipe( - getFirstCompletedRemoteData(), - map((data: RemoteData>) => { - if (data.hasSucceeded) { - return data.payload.page; - } - return []; - }), - catchError(() => []), - ); + // Quality Assurance sources are only available to authorized users. Checking the + // authorization first avoids issuing the `qualityassurancesources/search/byTarget` + // request for anonymous/unauthorized users, which would otherwise return a 401 that + // shows up as a failed request in the browser. Authorized users keep the same behavior. + return this.authorizationService.isAuthorized(FeatureID.CanSeeQA).pipe( + switchMap((canSeeQA: boolean) => { + if (!canSeeQA) { + return observableOf([] as QualityAssuranceSourceObject[]); + } + const findListTopicOptions: FindListOptions = { + searchParams: [new RequestParam('target', this.item.uuid)], + }; + return this.qualityAssuranceSourceDataService.getSourcesByTarget(findListTopicOptions, false) + .pipe( + getFirstCompletedRemoteData(), + map((data: RemoteData>) => { + if (data.hasSucceeded) { + return data.payload.page; + } + return []; + }), + ); + }), + catchError(() => observableOf([] as QualityAssuranceSourceObject[])), + ); } /** diff --git a/src/app/shared/access-control-form-container/access-control-form-container.component.ts b/src/app/shared/access-control-form-container/access-control-form-container.component.ts index bb84aee35d9..d3fde59bcac 100644 --- a/src/app/shared/access-control-form-container/access-control-form-container.component.ts +++ b/src/app/shared/access-control-form-container/access-control-form-container.component.ts @@ -98,11 +98,6 @@ export class AccessControlFormContainerComponent impleme * Will be used from a parent component to read the value of the form */ getFormValue() { - console.log({ - bitstream: this.bitstreamAccessCmp.getValue(), - item: this.itemAccessCmp.getValue(), - state: this.state, - }); return { bitstream: this.bitstreamAccessCmp.getValue(), item: this.itemAccessCmp.getValue(), @@ -137,9 +132,7 @@ export class AccessControlFormContainerComponent impleme this.bulkAccessControlService.executeScript( [ this.itemRD.payload.uuid ], file, - ).pipe(take(1)).subscribe((res) => { - console.log('success', res); - }); + ).pipe(take(1)).subscribe(); } /** diff --git a/src/app/shared/access-control-form-container/bulk-access-control.service.ts b/src/app/shared/access-control-form-container/bulk-access-control.service.ts index f3c284e3580..b4a4a91dc70 100644 --- a/src/app/shared/access-control-form-container/bulk-access-control.service.ts +++ b/src/app/shared/access-control-form-container/bulk-access-control.service.ts @@ -59,8 +59,6 @@ export class BulkAccessControlService { * @param file */ executeScript(uuids: string[], file: File): Observable { - console.log('execute', { uuids, file }); - const params: ProcessParameter[] = [ { name: '-f', value: file.name }, ]; diff --git a/src/app/shared/cookies/browser-klaro.service.spec.ts b/src/app/shared/cookies/browser-klaro.service.spec.ts index 5cdd5275cd3..9b31e853333 100644 --- a/src/app/shared/cookies/browser-klaro.service.spec.ts +++ b/src/app/shared/cookies/browser-klaro.service.spec.ts @@ -6,6 +6,7 @@ import cloneDeep from 'lodash/cloneDeep'; import { of as observableOf } from 'rxjs'; import { TestScheduler } from 'rxjs/testing'; +import { environment } from '../../../environments/environment'; import { AuthService } from '../../core/auth/auth.service'; import { RestResponse } from '../../core/cache/response.models'; import { ConfigurationDataService } from '../../core/data/configuration-data.service'; @@ -310,6 +311,9 @@ describe('BrowserKlaroService', () => { let GOOGLE_ANALYTICS_KEY; let REGISTRATION_VERIFICATION_ENABLED_KEY; beforeEach(() => { + // The Google Analytics key is only probed when GA is enabled for the installation; + // these tests exercise that probe-and-filter path. + environment.info.enableGoogleAnalytics = true; GOOGLE_ANALYTICS_KEY = clone((service as any).GOOGLE_ANALYTICS_KEY); REGISTRATION_VERIFICATION_ENABLED_KEY = clone((service as any).REGISTRATION_VERIFICATION_ENABLED_KEY); spyOn((service as any), 'getUser$').and.returnValue(observableOf(user)); @@ -320,6 +324,10 @@ describe('BrowserKlaroService', () => { configurationDataService.findByPropertyName = findByPropertyName; }); + afterEach(() => { + environment.info.enableGoogleAnalytics = false; + }); + it('should not filter googleAnalytics when servicesToHide are empty', () => { const filteredConfig = (service as any).filterConfigServices([]); expect(filteredConfig).toContain(jasmine.objectContaining({ name: googleAnalytics })); @@ -401,5 +409,19 @@ describe('BrowserKlaroService', () => { service.initialize(); expect(service.klaroConfig.services).not.toContain(jasmine.objectContaining({ name: googleAnalytics })); }); + it('should hide googleAnalytics without requesting the key when GA is disabled', () => { + environment.info.enableGoogleAnalytics = false; + const findByPropertyNameSpy = jasmine.createSpy('findByPropertyName').and.returnValue( + createSuccessfulRemoteDataObject$({ + ...new ConfigurationProperty(), + name: trackingIdTestValue, + values: ['false'], + }), + ); + configurationDataService.findByPropertyName = findByPropertyNameSpy; + service.initialize(); + expect(findByPropertyNameSpy).not.toHaveBeenCalledWith(GOOGLE_ANALYTICS_KEY); + expect(service.klaroConfig.services).not.toContain(jasmine.objectContaining({ name: googleAnalytics })); + }); }); }); diff --git a/src/app/shared/cookies/browser-klaro.service.ts b/src/app/shared/cookies/browser-klaro.service.ts index ea660d9dd6d..d9b0000b32e 100644 --- a/src/app/shared/cookies/browser-klaro.service.ts +++ b/src/app/shared/cookies/browser-klaro.service.ts @@ -114,10 +114,15 @@ export class BrowserKlaroService extends KlaroService { this.klaroConfig.translations.zy.consentNotice.description = 'cookies.consent.content-notice.description.no-privacy'; } - const hideGoogleAnalytics$ = this.configService.findByPropertyName(this.GOOGLE_ANALYTICS_KEY).pipe( - getFirstCompletedRemoteData(), - map(remoteData => !remoteData.hasSucceeded || !remoteData.payload || isEmpty(remoteData.payload.values)), - ); + // Only probe the backend for the Google Analytics key when GA is enabled for this + // installation. When it is disabled we hide the GA service from the consent UI without + // making a request that would otherwise 404 (the `google.analytics.key` property is unset). + const hideGoogleAnalytics$ = environment.info?.enableGoogleAnalytics + ? this.configService.findByPropertyName(this.GOOGLE_ANALYTICS_KEY).pipe( + getFirstCompletedRemoteData(), + map(remoteData => !remoteData.hasSucceeded || !remoteData.payload || isEmpty(remoteData.payload.values)), + ) + : observableOf(true); const hideRegistrationVerification$ = this.configService.findByPropertyName(this.REGISTRATION_VERIFICATION_ENABLED_KEY).pipe( getFirstCompletedRemoteData(), diff --git a/src/app/shared/search/search-export-csv/search-export-csv.component.ts b/src/app/shared/search/search-export-csv/search-export-csv.component.ts index 2727c6cce7c..a4baf28839d 100644 --- a/src/app/shared/search/search-export-csv/search-export-csv.component.ts +++ b/src/app/shared/search/search-export-csv/search-export-csv.component.ts @@ -19,6 +19,7 @@ import { Observable } from 'rxjs'; import { filter, map, + shareReplay, startWith, switchMap, } from 'rxjs/operators'; @@ -92,16 +93,34 @@ export class SearchExportCsvComponent implements OnInit, OnChanges { switchMap(() => this.scriptDataService.scriptWithNameExistsAndCanExecute('metadata-export-search')), map((canExecute: boolean) => canExecute), startWith(false), + shareReplay({ bufferSize: 1, refCount: true }), ); - this.shouldShowWarning$ = this.itemExceeds(); + this.shouldShowWarning$ = this.buildShouldShowWarning$(); } ngOnChanges(changes: SimpleChanges): void { - if (changes.total) { - this.shouldShowWarning$ = this.itemExceeds(); + // ngOnChanges runs before ngOnInit, so shouldShowButton$ may not exist on the first + // change; in that case ngOnInit builds the warning observable with the current total. + if (changes.total && hasValue(this.shouldShowButton$)) { + this.shouldShowWarning$ = this.buildShouldShowWarning$(); } } + /** + * Build the observable that decides whether to show the export-limit warning. + * The backend `bulkedit.export.max.items` property is only requested once the export + * button is shown (i.e. the user is an administrator who can run the export script), so + * anonymous and non-admin users never trigger that request (which would otherwise 404 + * when the property is unset). + */ + private buildShouldShowWarning$(): Observable { + return this.shouldShowButton$.pipe( + filter((canShow: boolean) => canShow), + switchMap(() => this.itemExceeds()), + startWith(false), + ); + } + /** * Checks if the export limit has been exceeded and updates the tooltip accordingly */ diff --git a/src/app/statistics/google-analytics.service.spec.ts b/src/app/statistics/google-analytics.service.spec.ts index eb35750c4ca..63c4b969f14 100644 --- a/src/app/statistics/google-analytics.service.spec.ts +++ b/src/app/statistics/google-analytics.service.spec.ts @@ -4,6 +4,7 @@ import { } from 'angulartics2'; import { of } from 'rxjs'; +import { environment } from '../../environments/environment'; import { ConfigurationDataService } from '../core/data/configuration-data.service'; import { ConfigurationProperty } from '../core/shared/configuration-property.model'; import { KlaroService } from '../shared/cookies/klaro.service'; @@ -40,6 +41,10 @@ describe('GoogleAnalyticsService', () => { }); beforeEach(() => { + // These tests exercise the active Google Analytics tracking path, which is only + // reached when GA is enabled for the installation. + environment.info.enableGoogleAnalytics = true; + googleAnalyticsSpy = jasmine.createSpyObj('Angulartics2GoogleAnalytics', [ 'startTracking', ]); @@ -80,11 +85,33 @@ describe('GoogleAnalyticsService', () => { service = new GoogleAnalyticsService(googleAnalyticsSpy, googleTagManagerSpy, klaroServiceSpy, configSpy, documentSpy ); }); + afterEach(() => { + environment.info.enableGoogleAnalytics = false; + }); + it('should be created', () => { expect(service).toBeTruthy(); }); describe('addTrackingIdToPage()', () => { + describe('when Google Analytics is disabled by config', () => { + beforeEach(() => { + environment.info.enableGoogleAnalytics = false; + }); + + it(`should NOT request the ${trackingIdProp} property`, () => { + service.addTrackingIdToPage(); + expect(configSpy.findByPropertyName).not.toHaveBeenCalled(); + }); + + it('should NOT add a script or start tracking', () => { + service.addTrackingIdToPage(); + expect(bodyElementSpy.appendChild).toHaveBeenCalledTimes(0); + expect(googleAnalyticsSpy.startTracking).toHaveBeenCalledTimes(0); + expect(googleTagManagerSpy.startTracking).toHaveBeenCalledTimes(0); + }); + }); + it(`should request the ${trackingIdProp} property`, () => { service.addTrackingIdToPage(); expect(configSpy.findByPropertyName).toHaveBeenCalledTimes(1); diff --git a/src/app/statistics/google-analytics.service.ts b/src/app/statistics/google-analytics.service.ts index 508297c3b9a..d1c3499c3a2 100644 --- a/src/app/statistics/google-analytics.service.ts +++ b/src/app/statistics/google-analytics.service.ts @@ -9,6 +9,7 @@ import { } from 'angulartics2'; import { combineLatest } from 'rxjs'; +import { environment } from '../../environments/environment'; import { ConfigurationDataService } from '../core/data/configuration-data.service'; import { getFirstCompletedRemoteData } from '../core/shared/operators'; import { KlaroService } from '../shared/cookies/klaro.service'; @@ -38,6 +39,13 @@ export class GoogleAnalyticsService { * page and starts tracking. */ addTrackingIdToPage(): void { + // Skip Google Analytics entirely when it is not enabled for this installation. + // This avoids a 404 request to the `google.analytics.key` configuration property + // (and any tracking setup) on installations that do not use Google Analytics. + if (!environment.info?.enableGoogleAnalytics) { + return; + } + const googleKey$ = this.configService.findByPropertyName('google.analytics.key').pipe( getFirstCompletedRemoteData(), ); diff --git a/src/app/submission/form/footer/submission-form-footer.component.ts b/src/app/submission/form/footer/submission-form-footer.component.ts index 375f9bfd1a3..ffbc8d5ca97 100644 --- a/src/app/submission/form/footer/submission-form-footer.component.ts +++ b/src/app/submission/form/footer/submission-form-footer.component.ts @@ -92,8 +92,6 @@ export class SubmissionFormFooterComponent implements OnChanges { private restService: SubmissionRestService, private submissionService: SubmissionService, private datashareSubmissionService: DatashareSubmissionService) { - // Debug: Log the signal value changes - console.log('Footer component created, initial signal value:', this.hasUploadFileErrorsSignal()); } // DATASHARE - End @@ -110,10 +108,6 @@ export class SubmissionFormFooterComponent implements OnChanges { this.processingDepositStatus = this.submissionService.getSubmissionDepositProcessingStatus(this.submissionId); this.showDepositAndDiscard = observableOf(this.submissionService.getSubmissionScope() === SubmissionScopeType.WorkspaceItem); this.hasUnsavedModification = this.submissionService.hasUnsavedModification(); - // DATASHARE - Start - // Debug: Log signal value on changes - console.log('Footer ngOnChanges, signal value:', this.hasUploadFileErrorsSignal()); - // DATASHARE - End } } diff --git a/src/app/submission/sections/container/section-container.component.ts b/src/app/submission/sections/container/section-container.component.ts index eb4e49885be..cd7b7327f56 100644 --- a/src/app/submission/sections/container/section-container.component.ts +++ b/src/app/submission/sections/container/section-container.component.ts @@ -133,7 +133,6 @@ export class SubmissionSectionContainerComponent implements OnInit { * @returns {string | null} The ID of the currently open panel, or null if no panel is open */ get openPanelId() { - console.log('Open panel ID:', this.datashareSubmissionFormSectionContainerService.openPanelId()); return this.datashareSubmissionFormSectionContainerService.openPanelId(); } @@ -155,7 +154,6 @@ export class SubmissionSectionContainerComponent implements OnInit { */ isSectionOpen(): boolean { const openPanelId = this.datashareSubmissionFormSectionContainerService.openPanelId(); - console.log('Checking if section is open:', this.sectionData.id === openPanelId); return this.sectionData.id === openPanelId; } // DATASHARE - end diff --git a/src/app/submission/sections/upload/section-upload.component.ts b/src/app/submission/sections/upload/section-upload.component.ts index efbf83280cd..dc7cfd724df 100644 --- a/src/app/submission/sections/upload/section-upload.component.ts +++ b/src/app/submission/sections/upload/section-upload.component.ts @@ -268,10 +268,6 @@ export class SubmissionSectionUploadComponent extends SectionModelComponent { // IMPORTANT: Update the shared service signal const hasDuplicates = this.datashareSubmissionService.getDuplicateFileNames(this.fileNames).length > 0; - console.log('File names:', this.fileNames); - console.log('Has duplicates:', hasDuplicates); - console.log('Should show deposit button:', !hasDuplicates); - console.log('this.isTotalUploadedFilesSizeExceeded: ', this.isTotalUploadedFilesSizeExceeded); // Update the service signal this.datashareSubmissionService.updatehasUploadFilesErrors(!hasDuplicates || this.isTotalUploadedFilesSizeExceeded); diff --git a/src/config/config.util.ts b/src/config/config.util.ts index 005606e70ff..08a63db82b9 100644 --- a/src/config/config.util.ts +++ b/src/config/config.util.ts @@ -17,7 +17,6 @@ import { */ const extendEnvironmentWithAppConfig = (env: any, appConfig: AppConfig): void => { mergeConfig(env, appConfig); - console.log(`Environment extended with app config`); }; /** diff --git a/src/config/default-app-config.ts b/src/config/default-app-config.ts index fb4f5a427a1..fcc8eb1b86c 100644 --- a/src/config/default-app-config.ts +++ b/src/config/default-app-config.ts @@ -483,6 +483,10 @@ export class DefaultAppConfig implements AppConfig { enablePrivacyStatement: true, enableCOARNotifySupport: true, enableCookieConsentPopup: true, + // Disabled by default: when no Google Analytics key is configured on the backend, + // probing for it only produces a 404 on every page. Enable this on installations + // that actually configure a Google Analytics tracking id. + enableGoogleAnalytics: false, }; // Whether to enable Markdown (https://commonmark.org/) and MathJax (https://www.mathjax.org/) diff --git a/src/config/info-config.interface.ts b/src/config/info-config.interface.ts index 0adf559a481..bcf7b5d7ab9 100644 --- a/src/config/info-config.interface.ts +++ b/src/config/info-config.interface.ts @@ -5,4 +5,12 @@ export interface InfoConfig extends Config { enablePrivacyStatement: boolean; enableCOARNotifySupport: boolean; enableCookieConsentPopup: boolean; + /** + * When false, the UI will not probe the backend for the `google.analytics.key` + * property and will not attempt to load Google Analytics. Set this to true only + * on installations that actually configure a Google Analytics tracking id on the + * backend. Keeping it false avoids a 404 request to + * `/server/api/config/properties/google.analytics.key` on every page. + */ + enableGoogleAnalytics: boolean; } diff --git a/src/environments/environment.test.ts b/src/environments/environment.test.ts index 89be0daf6ae..6c09521da74 100644 --- a/src/environments/environment.test.ts +++ b/src/environments/environment.test.ts @@ -342,6 +342,7 @@ export const environment: BuildConfig = { enablePrivacyStatement: true, enableCOARNotifySupport: true, enableCookieConsentPopup: true, + enableGoogleAnalytics: false, }, markdown: { enabled: false, From 3df5656cf89428dd8f341542ee0b0a62b30f7340 Mon Sep 17 00:00:00 2001 From: milanmajchrak Date: Tue, 30 Jun 2026 11:04:32 +0200 Subject: [PATCH 2/2] test(search-export-csv): assert bulkedit.export.max.items is only probed when the export button is shown Locks in the fix from this PR so a regression that reintroduces the per-page 404 (probing the config property for non-admin/anonymous users) is caught. Addresses Copilot review feedback. Co-Authored-By: Claude Opus 4.8 --- .../search-export-csv.component.spec.ts | 21 +++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/src/app/shared/search/search-export-csv/search-export-csv.component.spec.ts b/src/app/shared/search/search-export-csv/search-export-csv.component.spec.ts index f0672637123..7fd98a15a56 100644 --- a/src/app/shared/search/search-export-csv/search-export-csv.component.spec.ts +++ b/src/app/shared/search/search-export-csv/search-export-csv.component.spec.ts @@ -140,6 +140,27 @@ describe('SearchExportCsvComponent', () => { }); }); }); + describe('bulkedit.export.max.items probe', () => { + beforeEach(waitForAsync(() => { + initBeforeEachAsync(); + })); + + it('should NOT probe bulkedit.export.max.items when the export button is not shown', () => { + // Non-admin: the button (and therefore the export-limit warning) is never rendered, + // so the config property must not be requested - this is what avoids the per-page 404. + (authorizationDataService.isAuthorized as jasmine.Spy).and.returnValue(observableOf(false)); + (configurationDataService.findByPropertyName as jasmine.Spy).calls.reset(); + initBeforeEach(); + expect(configurationDataService.findByPropertyName).not.toHaveBeenCalledWith('bulkedit.export.max.items'); + }); + + it('should probe bulkedit.export.max.items once the export button is shown', () => { + // Admin with the export script available: the warning is evaluated, so the limit is fetched. + (configurationDataService.findByPropertyName as jasmine.Spy).calls.reset(); + initBeforeEach(); + expect(configurationDataService.findByPropertyName).toHaveBeenCalledWith('bulkedit.export.max.items'); + }); + }); describe('export', () => { beforeEach(waitForAsync(() => { initBeforeEachAsync();