diff --git a/src/app/features/files/components/files-tree-explorer/files-tree-explorer.component.spec.ts b/src/app/features/files/components/files-tree-explorer/files-tree-explorer.component.spec.ts index b4d45a2c8..e4f14adbb 100644 --- a/src/app/features/files/components/files-tree-explorer/files-tree-explorer.component.spec.ts +++ b/src/app/features/files/components/files-tree-explorer/files-tree-explorer.component.spec.ts @@ -250,7 +250,7 @@ describe('FilesTreeExplorerComponent', () => { component.downloadFolder('/upload-folder'); - expect(filesService.getFolderDownloadLink).toHaveBeenCalledWith('/upload-folder'); + expect(filesService.getFolderDownloadLink).toHaveBeenCalledWith('/upload-folder', 'files'); expect(openSpy).toHaveBeenCalledWith('/upload-folder?zip=', '_blank'); }); diff --git a/src/app/features/files/components/files-tree-explorer/files-tree-explorer.component.ts b/src/app/features/files/components/files-tree-explorer/files-tree-explorer.component.ts index 622e6ffa0..5b17a9571 100644 --- a/src/app/features/files/components/files-tree-explorer/files-tree-explorer.component.ts +++ b/src/app/features/files/components/files-tree-explorer/files-tree-explorer.component.ts @@ -27,6 +27,7 @@ import { LoadingSpinnerComponent } from '@osf/shared/components/loading-spinner/ import { FileKind } from '@osf/shared/enums/file-kind.enum'; import { FileMenuType } from '@osf/shared/enums/file-menu-type.enum'; import { CurrentResourceType } from '@osf/shared/enums/resource-type.enum'; +import { appendDownloadTrackingParams } from '@osf/shared/helpers/download-link.helper'; import { FileTreeMapper } from '@osf/shared/mappers/files/file-tree.mapper'; import { FilesMapper } from '@osf/shared/mappers/files/files.mapper'; import { FileModel } from '@osf/shared/models/files/file.model'; @@ -203,13 +204,13 @@ export class FilesTreeExplorerComponent { downloadFile(link: string): void { if (this.isBrowser) { - window.open(link)?.focus(); + window.open(appendDownloadTrackingParams(link, 'files'))?.focus(); } } downloadFolder(downloadLink: string): void { if (downloadLink) { - const link = this.filesService.getFolderDownloadLink(downloadLink); + const link = this.filesService.getFolderDownloadLink(downloadLink, 'files'); window.open(link, '_blank')?.focus(); } } diff --git a/src/app/features/files/pages/file-detail/file-detail.component.spec.ts b/src/app/features/files/pages/file-detail/file-detail.component.spec.ts index 6cb497816..42e09b7d9 100644 --- a/src/app/features/files/pages/file-detail/file-detail.component.spec.ts +++ b/src/app/features/files/pages/file-detail/file-detail.component.spec.ts @@ -169,11 +169,14 @@ describe('FileDetailComponent', () => { setup(); (store.dispatch as Mock).mockClear(); const openSpy = vi.spyOn(window, 'open').mockReturnValue({ focus: vi.fn() } as unknown as Window); + const timeZone = Intl.DateTimeFormat().resolvedOptions().timeZone; component.downloadRevision('3'); expect(dataciteService.logIdentifiableDownload).toHaveBeenCalledWith(component.fileMetadata$); - expect(openSpy).toHaveBeenCalledWith('https://osf.test/download/?revision=3'); + expect(openSpy).toHaveBeenCalledWith( + `https://osf.test/download/?revision=3&source=file-detail&tz=${encodeURIComponent(timeZone)}` + ); expect(store.dispatch).toHaveBeenCalledWith(new GetFileRevisions('https://osf.test/upload')); }); @@ -223,11 +226,14 @@ describe('FileDetailComponent', () => { setup(); (store.dispatch as Mock).mockClear(); const openSpy = vi.spyOn(window, 'open').mockReturnValue({ focus: vi.fn() } as unknown as Window); + const timeZone = Intl.DateTimeFormat().resolvedOptions().timeZone; component.downloadFile(); expect(dataciteService.logIdentifiableDownload).toHaveBeenCalledWith(component.fileMetadata$); - expect(openSpy).toHaveBeenCalledWith('https://osf.test/download'); + expect(openSpy).toHaveBeenCalledWith( + `https://osf.test/download?source=file-detail&tz=${encodeURIComponent(timeZone)}` + ); openSpy.mockRestore(); }); diff --git a/src/app/features/files/pages/file-detail/file-detail.component.ts b/src/app/features/files/pages/file-detail/file-detail.component.ts index 4e1591d22..5030355ae 100644 --- a/src/app/features/files/pages/file-detail/file-detail.component.ts +++ b/src/app/features/files/pages/file-detail/file-detail.component.ts @@ -40,6 +40,7 @@ import { MetadataTabsComponent } from '@osf/shared/components/metadata-tabs/meta import { SubHeaderComponent } from '@osf/shared/components/sub-header/sub-header.component'; import { MetadataResourceEnum } from '@osf/shared/enums/metadata-resource.enum'; import { ResourceType } from '@osf/shared/enums/resource-type.enum'; +import { appendDownloadTrackingParams } from '@osf/shared/helpers/download-link.helper'; import { getMfrUrlWithVersion } from '@osf/shared/helpers/mfr-url.helper'; import { CustomConfirmationService } from '@osf/shared/services/custom-confirmation.service'; import { DataciteService } from '@osf/shared/services/datacite/datacite.service'; @@ -250,7 +251,8 @@ export class FileDetailComponent implements OnDestroy { const storageLink = this.file()?.links.upload || ''; if (downloadUrl) { - window.open(`${downloadUrl}/?revision=${version}`)?.focus(); + const link = appendDownloadTrackingParams(`${downloadUrl}/?revision=${version}`, 'file-detail'); + window.open(link)?.focus(); this.actions.getFileRevisions(storageLink); } } @@ -266,7 +268,7 @@ export class FileDetailComponent implements OnDestroy { .logIdentifiableDownload(this.fileMetadata$) .pipe(takeUntilDestroyed(this.destroyRef)) .subscribe(); - window.open(link)?.focus(); + window.open(appendDownloadTrackingParams(link, 'file-detail'))?.focus(); } deleteEntry(link: string): void { diff --git a/src/app/features/files/pages/files/files.component.spec.ts b/src/app/features/files/pages/files/files.component.spec.ts index cc0982983..958356777 100644 --- a/src/app/features/files/pages/files/files.component.spec.ts +++ b/src/app/features/files/pages/files/files.component.spec.ts @@ -476,7 +476,7 @@ describe('FilesComponent', () => { component.downloadFolder(); expect(dataciteService.logFileDownload).toHaveBeenCalledWith('node-1', 'nodes'); - expect(filesService.getFolderDownloadLink).toHaveBeenCalledWith('/v2/files/file-123/download/'); + expect(filesService.getFolderDownloadLink).toHaveBeenCalledWith('/v2/files/file-123/download/', 'files'); expect(openSpy).toHaveBeenCalledWith('/v2/files/file-123/download/?zip=', '_blank'); openSpy.mockRestore(); }); diff --git a/src/app/features/files/pages/files/files.component.ts b/src/app/features/files/pages/files/files.component.ts index 5f55e4633..c34cf795e 100644 --- a/src/app/features/files/pages/files/files.component.ts +++ b/src/app/features/files/pages/files/files.component.ts @@ -514,7 +514,7 @@ export class FilesComponent { .logFileDownload(resourceId, resourcePath) .pipe(takeUntilDestroyed(this.destroyRef)) .subscribe(); - const link = this.filesService.getFolderDownloadLink(downloadLink); + const link = this.filesService.getFolderDownloadLink(downloadLink, 'files'); window.open(link, '_blank')?.focus(); } } diff --git a/src/app/features/preprints/components/preprint-details/share-and-download/share-and-download.component.spec.ts b/src/app/features/preprints/components/preprint-details/share-and-download/share-and-download.component.spec.ts index 13f353715..011056ab2 100644 --- a/src/app/features/preprints/components/preprint-details/share-and-download/share-and-download.component.spec.ts +++ b/src/app/features/preprints/components/preprint-details/share-and-download/share-and-download.component.spec.ts @@ -85,7 +85,7 @@ describe('ShareAndDownloadComponent', () => { component.download(); - expect(socialShareService.createDownloadUrl).toHaveBeenCalledWith(mockPreprint.id); + expect(socialShareService.createDownloadUrl).toHaveBeenCalledWith(mockPreprint.id, 'preprint'); expect(openSpy).toHaveBeenCalledWith('https://example.com/download'); expect(focus).toHaveBeenCalled(); expect(dataciteService.logIdentifiableDownload).toHaveBeenCalledWith(component.preprint$); @@ -124,7 +124,7 @@ describe('ShareAndDownloadComponent', () => { component.download(); - expect(socialShareService.createDownloadUrl).toHaveBeenCalledWith(mockPreprint.id); + expect(socialShareService.createDownloadUrl).toHaveBeenCalledWith(mockPreprint.id, 'preprint'); expect(dataciteService.logIdentifiableDownload).not.toHaveBeenCalled(); openSpy.mockRestore(); }); diff --git a/src/app/features/preprints/components/preprint-details/share-and-download/share-and-download.component.ts b/src/app/features/preprints/components/preprint-details/share-and-download/share-and-download.component.ts index 896c48f40..bc504b085 100644 --- a/src/app/features/preprints/components/preprint-details/share-and-download/share-and-download.component.ts +++ b/src/app/features/preprints/components/preprint-details/share-and-download/share-and-download.component.ts @@ -43,7 +43,7 @@ export class ShareAndDownloadComponent { return; } - const downloadLink = this.socialShareService.createDownloadUrl(preprint.id); + const downloadLink = this.socialShareService.createDownloadUrl(preprint.id, 'preprint'); const downloadWindow = window.open(downloadLink); if (!downloadWindow) { diff --git a/src/app/features/preprints/pages/preprint-download-redirect/preprint-download-redirect.component.spec.ts b/src/app/features/preprints/pages/preprint-download-redirect/preprint-download-redirect.component.spec.ts index 8bbf62371..a1f9454cd 100644 --- a/src/app/features/preprints/pages/preprint-download-redirect/preprint-download-redirect.component.spec.ts +++ b/src/app/features/preprints/pages/preprint-download-redirect/preprint-download-redirect.component.spec.ts @@ -58,7 +58,7 @@ describe('PreprintDownloadRedirectComponent', () => { it('should redirect to download URL when id is present in browser', () => { const redirectSpy = vi.spyOn(PreprintDownloadRedirectComponent.prototype, 'redirect').mockImplementation(vi.fn()); const { mockSocialShareService } = setup({ id: MOCK_ID }); - expect(mockSocialShareService.createDownloadUrl).toHaveBeenCalledWith(MOCK_ID); + expect(mockSocialShareService.createDownloadUrl).toHaveBeenCalledWith(MOCK_ID, 'preprint'); expect(redirectSpy).toHaveBeenCalledWith(MOCK_DOWNLOAD_URL); redirectSpy.mockRestore(); }); diff --git a/src/app/features/preprints/pages/preprint-download-redirect/preprint-download-redirect.component.ts b/src/app/features/preprints/pages/preprint-download-redirect/preprint-download-redirect.component.ts index 95dae4b89..81d1a9823 100644 --- a/src/app/features/preprints/pages/preprint-download-redirect/preprint-download-redirect.component.ts +++ b/src/app/features/preprints/pages/preprint-download-redirect/preprint-download-redirect.component.ts @@ -24,7 +24,7 @@ export class PreprintDownloadRedirectComponent { return; } - const url = this.socialShareService.createDownloadUrl(id); + const url = this.socialShareService.createDownloadUrl(id, 'preprint'); this.redirect(url); } diff --git a/src/app/shared/helpers/download-link.helper.ts b/src/app/shared/helpers/download-link.helper.ts new file mode 100644 index 000000000..7de4b7176 --- /dev/null +++ b/src/app/shared/helpers/download-link.helper.ts @@ -0,0 +1,5 @@ +export function appendDownloadTrackingParams(link: string, source: string): string { + const separator = link.includes('?') ? '&' : '?'; + const tz = Intl.DateTimeFormat().resolvedOptions().timeZone; + return `${link}${separator}source=${encodeURIComponent(source)}&tz=${encodeURIComponent(tz)}`; +} diff --git a/src/app/shared/services/files.service.spec.ts b/src/app/shared/services/files.service.spec.ts index 6f8a355ab..5168591cb 100644 --- a/src/app/shared/services/files.service.spec.ts +++ b/src/app/shared/services/files.service.spec.ts @@ -118,8 +118,11 @@ describe('FilesService', () => { it('should build folder download link with correct separator', () => { setup(); - expect(service.getFolderDownloadLink('/files/1')).toBe('/files/1?zip='); - expect(service.getFolderDownloadLink('/files/1?foo=bar')).toBe('/files/1?foo=bar&zip='); + const timeZone = Intl.DateTimeFormat().resolvedOptions().timeZone; + expect(service.getFolderDownloadLink('/files/1')).toBe(`/files/1?zip=&source=&tz=${encodeURIComponent(timeZone)}`); + expect(service.getFolderDownloadLink('/files/1?foo=bar')).toBe( + `/files/1?foo=bar&zip=&source=&tz=${encodeURIComponent(timeZone)}` + ); }); it('should return empty reference when addons api response has no data', async () => { diff --git a/src/app/shared/services/files.service.ts b/src/app/shared/services/files.service.ts index 5d5becd79..0401126db 100644 --- a/src/app/shared/services/files.service.ts +++ b/src/app/shared/services/files.service.ts @@ -21,6 +21,7 @@ import { PaginatedData } from '@osf/shared/models/paginated-data.model'; import { FileKind } from '../enums/file-kind.enum'; import { ResourceType } from '../enums/resource-type.enum'; +import { appendDownloadTrackingParams } from '../helpers/download-link.helper'; import { AddonMapper } from '../mappers/addon.mapper'; import { ContributorsMapper } from '../mappers/contributors'; import { FilesMapper } from '../mappers/files/files.mapper'; @@ -173,9 +174,9 @@ export class FilesService { return this.jsonApiService.post(link, body); } - getFolderDownloadLink(link: string): string { + getFolderDownloadLink(link: string, source = ''): string { const separator = link.includes('?') ? '&' : '?'; - return `${link}${separator}zip=`; + return appendDownloadTrackingParams(`${link}${separator}zip=`, source); } getFileTarget(fileGuid: string): Observable { diff --git a/src/app/shared/services/social-share.service.spec.ts b/src/app/shared/services/social-share.service.spec.ts index 04060b9f7..5e1c51ec6 100644 --- a/src/app/shared/services/social-share.service.spec.ts +++ b/src/app/shared/services/social-share.service.spec.ts @@ -79,9 +79,13 @@ describe('SocialShareService', () => { it('should create web urls', () => { setup(); + const timeZone = Intl.DateTimeFormat().resolvedOptions().timeZone; + expect(service.createPreprintUrl('pp-1', 'osf')).toBe('https://osf.test/preprints/osf/pp-1'); expect(service.createGuidUrl('abc12')).toBe('https://osf.test/abc12'); - expect(service.createDownloadUrl('res-1')).toBe('https://osf.test/download/res-1'); + expect(service.createDownloadUrl('res-1')).toBe( + `https://osf.test/download/res-1?source=&tz=${encodeURIComponent(timeZone)}` + ); }); it('should generate social action items from platform config', () => { diff --git a/src/app/shared/services/social-share.service.ts b/src/app/shared/services/social-share.service.ts index d8c9832f2..faa5db904 100644 --- a/src/app/shared/services/social-share.service.ts +++ b/src/app/shared/services/social-share.service.ts @@ -4,6 +4,7 @@ import { ENVIRONMENT } from '@core/provider/environment.provider'; import { SOCIAL_PLATFORMS } from '../constants/social-platforms.const'; import { SOCIAL_SHARE_URLS } from '../constants/social-share.config'; +import { appendDownloadTrackingParams } from '../helpers/download-link.helper'; import { SocialShareContentModel } from '../models/socials/social-share-content.model'; import { SocialShareLinksModel } from '../models/socials/social-share-links.model'; import { SocialsShareActionItem } from '../models/socials/socials-share-action-item.model'; @@ -56,8 +57,8 @@ export class SocialShareService { return `${this.webUrl}/${guid}`; } - createDownloadUrl(resourceId: string): string { - return `${this.webUrl}/download/${resourceId}`; + createDownloadUrl(resourceId: string, source = ''): string { + return appendDownloadTrackingParams(`${this.webUrl}/download/${resourceId}`, source); } generateSocialActionItems(content: SocialShareContentModel): SocialsShareActionItem[] {