diff --git a/src/app/features/analytics/analytics.component.ts b/src/app/features/analytics/analytics.component.ts index fff8d7072..f27d9b343 100644 --- a/src/app/features/analytics/analytics.component.ts +++ b/src/app/features/analytics/analytics.component.ts @@ -140,6 +140,7 @@ export class AnalyticsComponent implements OnInit { navigateToLinkedProjects() { this.router.navigate(['linked-projects'], { relativeTo: this.route }); } + private setData() { const analytics = this.analytics(); @@ -171,7 +172,14 @@ export class AnalyticsComponent implements OnInit { }, ]; - this.popularPagesLabels = analytics.popularPages.map((item) => item.title); + this.popularPagesLabels = analytics.popularPages.map((item) => { + const parts = item.path.split('/').filter(Boolean); + const resource = parts[1]?.replace('-', ' ') || 'overview'; + let cleanTitle = item.title === 'OSF' ? item.title : item.title.replace(/^OSF \| /, ''); + cleanTitle = cleanTitle.replace(/&/gi, '&').replace(/</gi, '<').replace(/>/gi, '>'); + return cleanTitle.endsWith(resource) ? cleanTitle : `${cleanTitle} | ${resource}`; + }); + this.popularPagesDataset = [ { label: this.translateService.instant('project.analytics.charts.popularPages'), diff --git a/src/app/features/preprints/pages/preprint-details/preprint-details.component.ts b/src/app/features/preprints/pages/preprint-details/preprint-details.component.ts index e8511d9f4..31d8ecb41 100644 --- a/src/app/features/preprints/pages/preprint-details/preprint-details.component.ts +++ b/src/app/features/preprints/pages/preprint-details/preprint-details.component.ts @@ -175,14 +175,6 @@ export class PreprintDetailsComponent implements OnInit, OnDestroy { this.helpScoutService.setResourceType('preprint'); this.prerenderReady.setNotReady(); - effect(() => { - const currentPreprint = this.preprint(); - - if (currentPreprint && currentPreprint.isPublic) { - this.analyticsService.sendCountedUsage(currentPreprint.id, 'preprint.detail').subscribe(); - } - }); - effect(() => { const preprint = this.preprint(); const contributors = this.contributors(); diff --git a/src/app/features/project/overview/project-overview.component.ts b/src/app/features/project/overview/project-overview.component.ts index 9ceabbae3..4452dd2ec 100644 --- a/src/app/features/project/overview/project-overview.component.ts +++ b/src/app/features/project/overview/project-overview.component.ts @@ -379,12 +379,6 @@ export class ProjectOverviewComponent implements OnInit { this.actions.getHomeWiki(ResourceType.Project, project.id); } }); - effect(() => { - const currentProject = this.currentProject(); - if (currentProject && currentProject.isPublic) { - this.analyticsService.sendCountedUsage(currentProject.id, 'project.detail').subscribe(); - } - }); } private setupRouteChangeListener(): void { diff --git a/src/app/features/project/project.component.spec.ts b/src/app/features/project/project.component.spec.ts index 607a52643..e41b875d8 100644 --- a/src/app/features/project/project.component.spec.ts +++ b/src/app/features/project/project.component.spec.ts @@ -1,10 +1,12 @@ import { ComponentFixture, TestBed } from '@angular/core/testing'; import { HelpScoutService } from '@core/services/help-scout.service'; +import { CurrentResourceSelectors } from '@osf/shared/stores/current-resource'; import { ProjectComponent } from './project.component'; import { OSFTestingModule } from '@testing/osf.testing.module'; +import { provideMockStore } from '@testing/providers/store-provider.mock'; describe('Component: Project', () => { let component: ProjectComponent; @@ -22,6 +24,9 @@ describe('Component: Project', () => { unsetResourceType: jest.fn(), }, }, + provideMockStore({ + signals: [{ selector: CurrentResourceSelectors.getCurrentResource, value: null }], + }), ], }).compileComponents(); diff --git a/src/app/features/project/project.component.ts b/src/app/features/project/project.component.ts index 9fd8a6ad0..bf4fbfd34 100644 --- a/src/app/features/project/project.component.ts +++ b/src/app/features/project/project.component.ts @@ -1,7 +1,14 @@ -import { ChangeDetectionStrategy, Component, HostBinding, inject, OnDestroy } from '@angular/core'; -import { RouterOutlet } from '@angular/router'; +import { select } from '@ngxs/store'; + +import { filter } from 'rxjs'; + +import { ChangeDetectionStrategy, Component, DestroyRef, HostBinding, inject, OnDestroy } from '@angular/core'; +import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; +import { NavigationEnd, Router, RouterOutlet } from '@angular/router'; import { HelpScoutService } from '@core/services/help-scout.service'; +import { AnalyticsService } from '@shared/services/analytics.service'; +import { CurrentResourceSelectors } from '@shared/stores/current-resource'; @Component({ selector: 'osf-project', @@ -14,8 +21,25 @@ export class ProjectComponent implements OnDestroy { private readonly helpScoutService = inject(HelpScoutService); @HostBinding('class') classes = 'flex flex-1 flex-column w-full'; + private readonly router = inject(Router); + private readonly destroyRef = inject(DestroyRef); + private readonly analyticsService = inject(AnalyticsService); + currentResource = select(CurrentResourceSelectors.getCurrentResource); + constructor() { this.helpScoutService.setResourceType('project'); + + this.router.events + .pipe( + filter((event) => event instanceof NavigationEnd), + takeUntilDestroyed(this.destroyRef) + ) + .subscribe((event: NavigationEnd) => { + this.analyticsService.sendCountedUsageForRegistrationAndProjects( + event.urlAfterRedirects, + this.currentResource() + ); + }); } ngOnDestroy(): void { diff --git a/src/app/features/registry/registry.component.ts b/src/app/features/registry/registry.component.ts index ea5cd3e24..d03ea8a4c 100644 --- a/src/app/features/registry/registry.component.ts +++ b/src/app/features/registry/registry.component.ts @@ -1,6 +1,6 @@ import { createDispatchMap, select } from '@ngxs/store'; -import { map } from 'rxjs'; +import { filter, map } from 'rxjs'; import { DatePipe } from '@angular/common'; import { @@ -15,7 +15,7 @@ import { signal, } from '@angular/core'; import { takeUntilDestroyed, toObservable, toSignal } from '@angular/core/rxjs-interop'; -import { ActivatedRoute, RouterOutlet } from '@angular/router'; +import { ActivatedRoute, NavigationEnd, Router, RouterOutlet } from '@angular/router'; import { ENVIRONMENT } from '@core/provider/environment.provider'; import { PrerenderReadyService } from '@core/services/prerender-ready.service'; @@ -26,6 +26,7 @@ import { AnalyticsService } from '@osf/shared/services/analytics.service'; import { MetaTagsService } from '@osf/shared/services/meta-tags.service'; import { ContributorsSelectors, GetBibliographicContributors } from '@osf/shared/stores/contributors'; import { DataciteService } from '@shared/services/datacite/datacite.service'; +import { CurrentResourceSelectors } from '@shared/stores/current-resource'; import { GetRegistryIdentifiers, GetRegistryWithRelatedData, RegistrySelectors } from './store/registry'; @@ -56,7 +57,7 @@ export class RegistryComponent implements OnDestroy { }); private registryId = toSignal(this.route.params.pipe(map((params) => params['id']))); - + readonly currentResource = select(CurrentResourceSelectors.getCurrentResource); readonly registry = select(RegistrySelectors.getRegistry); readonly isRegistryLoading = select(RegistrySelectors.isRegistryLoading); readonly identifiersForDatacite$ = toObservable(select(RegistrySelectors.getIdentifiers)).pipe( @@ -67,7 +68,6 @@ export class RegistryComponent implements OnDestroy { readonly isBibliographicContributorsLoading = select(ContributorsSelectors.isBibliographicContributorsLoading); readonly license = select(RegistrySelectors.getLicense); readonly isLicenseLoading = select(RegistrySelectors.isLicenseLoading); - readonly isIdentifiersLoading = select(RegistrySelectors.isIdentifiersLoading); private readonly allDataLoaded = computed( () => @@ -78,6 +78,7 @@ export class RegistryComponent implements OnDestroy { ); private readonly lastMetaTagsRegistryId = signal(null); + readonly router = inject(Router); constructor() { this.prerenderReady.setNotReady(); @@ -104,17 +105,22 @@ export class RegistryComponent implements OnDestroy { } }); - effect(() => { - const currentRegistry = this.registry(); - if (currentRegistry && currentRegistry.isPublic) { - this.analyticsService.sendCountedUsage(currentRegistry.id, 'registry.detail').subscribe(); - } - }); - this.dataciteService .logIdentifiableView(this.identifiersForDatacite$) .pipe(takeUntilDestroyed(this.destroyRef)) .subscribe(); + + this.router.events + .pipe( + filter((event) => event instanceof NavigationEnd), + takeUntilDestroyed(this.destroyRef) + ) + .subscribe((event: NavigationEnd) => { + this.analyticsService.sendCountedUsageForRegistrationAndProjects( + event.urlAfterRedirects, + this.currentResource() + ); + }); } ngOnDestroy(): void { diff --git a/src/app/shared/models/current-resource.model.ts b/src/app/shared/models/current-resource.model.ts index 0bd36f242..775a03258 100644 --- a/src/app/shared/models/current-resource.model.ts +++ b/src/app/shared/models/current-resource.model.ts @@ -8,4 +8,5 @@ export interface CurrentResource { rootResourceId?: string; wikiEnabled?: boolean; permissions: UserPermissions[]; + title?: string; } diff --git a/src/app/shared/models/guid-response-json-api.model.ts b/src/app/shared/models/guid-response-json-api.model.ts index 708c61e77..451eb9ece 100644 --- a/src/app/shared/models/guid-response-json-api.model.ts +++ b/src/app/shared/models/guid-response-json-api.model.ts @@ -11,6 +11,7 @@ interface GuidDataJsonApi { guid: string; wiki_enabled: boolean; current_user_permissions: UserPermissions[]; + title?: string; }; relationships: { target?: { diff --git a/src/app/shared/services/analytics.service.ts b/src/app/shared/services/analytics.service.ts index 50364a5f1..e5b428fa8 100644 --- a/src/app/shared/services/analytics.service.ts +++ b/src/app/shared/services/analytics.service.ts @@ -3,8 +3,8 @@ import { Observable } from 'rxjs'; import { inject, Injectable } from '@angular/core'; import { ENVIRONMENT } from '@core/provider/environment.provider'; - -import { JsonApiService } from './json-api.service'; +import { CurrentResource } from '@osf/shared/models/current-resource.model'; +import { JsonApiService } from '@osf/shared/services/json-api.service'; @Injectable({ providedIn: 'root' }) export class AnalyticsService { @@ -15,23 +15,41 @@ export class AnalyticsService { return `${this.environment.apiDomainUrl}/_/metrics/events/counted_usage/`; } - sendCountedUsage(guid: string, routeName: string): Observable { - const payload = { + getPageviewPayload(resource: CurrentResource, routeName: string) { + const all_attrs = { item_guid: resource?.id } as const; + const attributes = Object.fromEntries( + Object.entries(all_attrs).filter(([_, value]: [unknown, unknown]) => typeof value !== 'undefined') + ); + const pageTitle = document.title === 'OSF' ? `OSF | ${resource.title}` : document.title; + return { data: { type: 'counted-usage', attributes: { - item_guid: guid, + ...attributes, action_labels: ['web', 'view'], pageview_info: { page_url: document.URL, - page_title: document.title, + page_title: pageTitle, referer_url: document.referrer, route_name: `angular-osf-web.${routeName}`, }, }, }, }; + } + sendCountedUsage(resource: CurrentResource, route: string): Observable { + const payload = this.getPageviewPayload(resource, route); return this.jsonApiService.post(this.apiDomainUrl, payload); } + + sendCountedUsageForRegistrationAndProjects(urlPath: string, resource: CurrentResource | null) { + if (resource) { + let route = urlPath.split('/').filter(Boolean).join('.'); + if (resource?.type) { + route = `${resource?.type}.${route}`; + } + this.sendCountedUsage(resource, route).subscribe(); + } + } } diff --git a/src/app/shared/services/resource.service.ts b/src/app/shared/services/resource.service.ts index ceb161c5a..e0bec6a80 100644 --- a/src/app/shared/services/resource.service.ts +++ b/src/app/shared/services/resource.service.ts @@ -55,6 +55,7 @@ export class ResourceGuidService { wikiEnabled: res.data.attributes.wiki_enabled, permissions: res.data.attributes.current_user_permissions, rootResourceId: res.data.relationships.root?.data?.id, + title: res.data.attributes?.title, }) as CurrentResource ), finalize(() => this.loaderService.hide())