diff --git a/src/lib/Preview.js b/src/lib/Preview.js index 2f2a84ded..78386a617 100644 --- a/src/lib/Preview.js +++ b/src/lib/Preview.js @@ -55,7 +55,16 @@ import { X_REP_HINT_VIDEO_MP4, FILE_OPTION_FILE_VERSION_ID } from './constants'; -import { VIEWER_EVENT, ERROR_CODE, PREVIEW_ERROR, PREVIEW_METRIC, LOAD_METRIC } from './events'; +import { + VIEWER_EVENT, + ERROR_CODE, + PREVIEW_ERROR, + PREVIEW_METRIC, + LOAD_METRIC, + DURATION_METRIC, + PREVIEW_END_EVENT, + PREVIEW_DOWNLOAD_ATTEMPT_EVENT +} from './events'; import { getClientLogDetails, getISOTime } from './logUtils'; import './Preview.scss'; @@ -186,6 +195,25 @@ class Preview extends EventEmitter { // Destroy viewer if (this.viewer && typeof this.viewer.destroy === 'function') { + // Log a preview end event + if (this.file && this.file.id) { + const previewDurationTag = Timer.createTag(this.file.id, DURATION_METRIC); + const previewDurationTimer = Timer.get(previewDurationTag); + Timer.stop(previewDurationTag); + + const event = { + event_name: PREVIEW_END_EVENT, + value: { + duration: previewDurationTimer ? previewDurationTimer.elapsed : null, + viewer_status: this.viewer.getLoadStatus() + }, + ...this.createLogEvent() + }; + + Timer.reset(previewDurationTag); + this.emit(PREVIEW_METRIC, event); + } + this.viewer.destroy(); } @@ -522,6 +550,14 @@ class Preview extends EventEmitter { DownloadReachability.downloadWithReachabilityCheck(downloadUrl); }); } + + const downloadAttemptEvent = { + event_name: PREVIEW_DOWNLOAD_ATTEMPT_EVENT, + value: this.viewer ? this.viewer.getLoadStatus() : null, + ...this.createLogEvent() + }; + + this.emit(PREVIEW_METRIC, downloadAttemptEvent); } /** @@ -793,6 +829,10 @@ class Preview extends EventEmitter { // Setup loading UI and progress bar this.ui.showLoadingIndicator(); this.ui.startProgressBar(); + + // Start the preview duration timer when the user starts to perceive preview's load + const previewDurationTag = Timer.createTag(this.file.id, DURATION_METRIC); + Timer.start(previewDurationTag); } /** diff --git a/src/lib/__tests__/Preview-test.js b/src/lib/__tests__/Preview-test.js index 0efdd6164..0f36a8edf 100644 --- a/src/lib/__tests__/Preview-test.js +++ b/src/lib/__tests__/Preview-test.js @@ -75,13 +75,21 @@ describe('lib/Preview', () => { }; stubs.viewer = { - destroy: sandbox.stub() + destroy: sandbox.stub(), + getLoadStatus: sandbox.stub() }; }); + it('should invoke emitLoadMetrics()', () => { + stubs.emitLoadMetrics = sandbox.stub(preview, 'emitLoadMetrics'); + preview.destroy(); + expect(stubs.emitLoadMetrics).to.be.called; + }); + it('should destroy the viewer if it exists', () => { preview.viewer = { - destroy: undefined + destroy: undefined, + getLoadStatus: sandbox.stub() }; preview.destroy(); @@ -93,15 +101,41 @@ describe('lib/Preview', () => { expect(stubs.viewer.destroy).to.be.called; }); - it('should clear the viewer', () => { + it('should stop the duration timer, reset it, and log a preview end event', () => { + preview.file = { + id: 1 + }; + stubs.viewer.getLoadStatus.returns('loaded'); + sandbox.stub(preview, 'createLogEvent'); + const durationTimer = { + elapsed: 7 + }; + + const mockEventObject = { + event_name: 'preview_end', + value: { + duration: durationTimer.elapsed, + viewer_status: 'loaded' + } + }; + + sandbox.stub(Timer, 'createTag').returns('duration_tag'); + sandbox.stub(Timer, 'get').returns(durationTimer); + sandbox.stub(Timer, 'stop'); + sandbox.stub(Timer, 'reset'); + sandbox.stub(preview, 'emit'); + preview.viewer = stubs.viewer; + preview.destroy(); - expect(preview.viewer).to.equal(undefined); + expect(Timer.createTag).to.be.called; + expect(Timer.stop).to.be.calledWith('duration_tag'); + expect(stubs.viewer.getLoadStatus).to.be.called; + expect(preview.emit).to.be.calledWith(PREVIEW_METRIC, mockEventObject); }); - it('should invoke emitLoadMetrics()', () => { - stubs.emitLoadMetrics = sandbox.stub(preview, 'emitLoadMetrics'); + it('should clear the viewer', () => { preview.destroy(); - expect(stubs.emitLoadMetrics).to.be.called; + expect(preview.viewer).to.equal(undefined); }); }); @@ -767,6 +801,7 @@ describe('lib/Preview', () => { preview.viewer = { getRepresentation: sandbox.stub(), getAssetPath: sandbox.stub(), + getLoadStatus: sandbox.stub(), createContentUrlWithAuthParams: sandbox.stub(), options: { viewer: { @@ -774,6 +809,7 @@ describe('lib/Preview', () => { } } }; + sandbox.stub(preview, 'emit'); sandbox.stub(file, 'canDownload'); sandbox.stub(file, 'shouldDownloadWM'); sandbox.stub(util, 'openUrlInsideIframe'); @@ -845,6 +881,22 @@ describe('lib/Preview', () => { expect(DownloadReachability.downloadWithReachabilityCheck).to.be.calledWith(url); }); }); + + it('should emit the download attempted metric', () => { + file.canDownload.returns(true); + file.shouldDownloadWM.returns(false); + + const url = 'someurl'; + util.appendQueryParams.returns(url); + + const promise = Promise.resolve({ + download_url: url + }); + + util.get.returns(promise); + preview.download(); + expect(preview.emit).to.be.calledWith('preview_metric'); + }); }); describe('updateToken()', () => { diff --git a/src/lib/events.js b/src/lib/events.js index e0a61dd47..fd4f4aa3a 100644 --- a/src/lib/events.js +++ b/src/lib/events.js @@ -58,6 +58,11 @@ export const LOAD_METRIC = { fullDocumentLoadTime: 'full_document_load_time' // How long it took to load the document so it could be previewed. }; +export const DURATION_METRIC = 'preview_duration_metric'; +// Event fired from preview with preview duration metrics +export const PREVIEW_END_EVENT = 'preview_end'; +// Event fired when the user attempts to download the file +export const PREVIEW_DOWNLOAD_ATTEMPT_EVENT = 'preview_download_attempt'; // Events around download reachability export const DOWNLOAD_REACHABILITY_METRICS = { NOTIFICATION_SHOWN: 'dl_reachability_notification_shown', diff --git a/src/lib/viewers/BaseViewer.js b/src/lib/viewers/BaseViewer.js index 49bbcd570..43492dd29 100644 --- a/src/lib/viewers/BaseViewer.js +++ b/src/lib/viewers/BaseViewer.js @@ -36,6 +36,12 @@ import { VIEWER_EVENT, ERROR_CODE, LOAD_METRIC, DOWNLOAD_REACHABILITY_METRICS } import PreviewError from '../PreviewError'; import Timer from '../Timer'; +const VIEWER_STATUSES = { + error: 'error', + loaded: 'loaded', + loading: 'loading' +}; + const ANNOTATIONS_JS = 'annotations.js'; const ANNOTATIONS_CSS = 'annotations.css'; @@ -750,6 +756,20 @@ class BaseViewer extends EventEmitter { return repStatus; } + /** + * Returns a string representing the viewer's loading status. Either loading, loaded, or error + * + * @public + * @return {string} A string representing the viewer's load status + */ + getLoadStatus() { + if (this.loaded) { + return this.options.viewer.NAME === 'Error' ? VIEWER_STATUSES.error : VIEWER_STATUSES.loaded; + } + + return VIEWER_STATUSES.loading; + } + /** * Returns if representation status is considered success * diff --git a/src/lib/viewers/__tests__/BaseViewer-test.js b/src/lib/viewers/__tests__/BaseViewer-test.js index 031f4b1a0..dc9d2a232 100644 --- a/src/lib/viewers/__tests__/BaseViewer-test.js +++ b/src/lib/viewers/__tests__/BaseViewer-test.js @@ -917,6 +917,23 @@ describe('lib/viewers/BaseViewer', () => { }); }); + describe('getLoadStatus()', () => { + it('should return the correct string based on load status and viewer type', () => { + base.loaded = false; + expect(base.getLoadStatus()).to.equal('loading'); + + base.loaded = true; + base.options.viewer = { + NAME: 'Error' + }; + + expect(base.getLoadStatus()).to.equal('error'); + + base.options.viewer.NAME = 'Dash'; + expect(base.getLoadStatus()).to.equal('loaded'); + }); + }); + describe('isRepresentationReady()', () => { it('should return whether the representation has a successful status', () => { const representation = {