Skip to content

Commit

Permalink
feat(perf): Add new event to report modern performance metrics
Browse files Browse the repository at this point in the history
  • Loading branch information
jstoffan committed Jan 3, 2020
1 parent 0a4ed88 commit f704338
Show file tree
Hide file tree
Showing 5 changed files with 189 additions and 97 deletions.
112 changes: 63 additions & 49 deletions src/lib/Preview.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import loaderList from './loaders';
import Cache from './Cache';
import PreviewError from './PreviewError';
import PreviewErrorViewer from './viewers/error/PreviewErrorViewer';
import PreviewPerf from './PreviewPerf';
import PreviewUI from './PreviewUI';
import getTokens from './tokens';
import Timer from './Timer';
Expand Down Expand Up @@ -164,6 +165,7 @@ class Preview extends EventEmitter {
this.location = PREVIEW_LOCATION;

this.cache = new Cache();
this.perf = new PreviewPerf();
this.ui = new PreviewUI();
this.browserInfo = Browser.getBrowserInfo();

Expand Down Expand Up @@ -198,18 +200,16 @@ class Preview extends EventEmitter {
const previewDurationTag = Timer.createTag(this.file.id, DURATION_METRIC);
const previewDurationTimer = Timer.get(previewDurationTag);
Timer.stop(previewDurationTag);
const previewDuration = previewDurationTimer ? previewDurationTimer.elapsed : null;
Timer.reset(previewDurationTag);

const event = {
this.emitLogEvent(PREVIEW_METRIC, {
event_name: PREVIEW_END_EVENT,
value: {
duration: previewDurationTimer ? previewDurationTimer.elapsed : null,
duration: previewDuration,
viewer_status: this.viewer.getLoadStatus(),
},
...this.createLogEvent(),
};

Timer.reset(previewDurationTag);
this.emit(PREVIEW_METRIC, event);
});
}

// Eject http interceptors
Expand Down Expand Up @@ -560,13 +560,10 @@ class Preview extends EventEmitter {
});
}

const downloadAttemptEvent = {
this.emit(PREVIEW_METRIC, {
event_name: PREVIEW_DOWNLOAD_ATTEMPT_EVENT,
value: this.viewer ? this.viewer.getLoadStatus() : null,
...this.createLogEvent(),
};

this.emit(PREVIEW_METRIC, downloadAttemptEvent);
});
}

/**
Expand Down Expand Up @@ -1266,13 +1263,10 @@ class Preview extends EventEmitter {
* @return {void}
*/
handleViewerMetrics(data) {
const formattedEvent = {
this.emitLogEvent(PREVIEW_METRIC, {
event_name: data.event,
value: data.data,
...this.createLogEvent(),
};

this.emit(PREVIEW_METRIC, formattedEvent);
});
}

/**
Expand All @@ -1293,6 +1287,7 @@ class Preview extends EventEmitter {

// Log now that loading is finished
this.emitLoadMetrics(data);
this.emitPerfMetrics();

// Show download and print buttons if user can download
if (canDownload(this.file, this.options)) {
Expand Down Expand Up @@ -1518,28 +1513,6 @@ class Preview extends EventEmitter {
this.viewer.load(err);
}

/**
* Create a generic log Object.
*
* @private
* @return {Object} Log details for viewer session and current file.
*/
createLogEvent() {
const file = this.file || {};
const log = {
timestamp: getISOTime(),
file_id: getProp(file, 'id', ''),
file_version_id: getProp(file, 'file_version.id', ''),
content_type: getProp(this.viewer, 'options.viewer.NAME', ''),
extension: file.extension || '',
locale: getProp(this.location, 'locale', ''),
rep_type: getProp(this.viewer, 'options.representation.representation', '').toLowerCase(),
...getClientLogDetails(),
};

return log;
}

/**
* Message, to any listeners of Preview, that an error has occurred.
*
Expand All @@ -1558,12 +1531,9 @@ class Preview extends EventEmitter {
sanitizedError.displayMessage = stripAuthFromString(displayMessage);
sanitizedError.message = stripAuthFromString(message);

const errorLog = {
this.emitLogEvent(PREVIEW_ERROR, {
error: sanitizedError,
...this.createLogEvent(),
};

this.emit(PREVIEW_ERROR, errorLog);
});
}

/**
Expand Down Expand Up @@ -1594,22 +1564,66 @@ class Preview extends EventEmitter {
const contentLoadTime = Timer.get(contentLoadTag) || {};
const previewLoadTime = Timer.get(previewLoadTag) || {};

const event = {
this.emitLogEvent(PREVIEW_METRIC, {
encoding,
event_name: LOAD_METRIC.previewLoadEvent,
value: previewLoadTime.elapsed || 0,
[LOAD_METRIC.fileInfoTime]: infoTime.elapsed || 0,
[LOAD_METRIC.convertTime]: convertTime.elapsed || 0,
[LOAD_METRIC.downloadResponseTime]: downloadTime.elapsed || 0,
[LOAD_METRIC.contentLoadTime]: contentLoadTime.elapsed || 0,
...this.createLogEvent(),
};

this.emit(PREVIEW_METRIC, event);
});

Timer.reset([infoTag, convertTag, downloadTag, contentLoadTag, previewLoadTag]);
}

/**
* Emit events to log preview-specific performance metrics if they exist and are non-zero
*
* @private
* @return {void}
*/
emitPerfMetrics() {
const { fcp, lcp } = this.perf.report();

if (fcp) {
this.emitLogEvent(PREVIEW_METRIC, {
event_name: 'preview_perf_fcp',
value: fcp,
});
}

if (lcp) {
this.emitLogEvent(PREVIEW_METRIC, {
event_name: 'preview_perf_lcp',
value: lcp,
});
}
}

/**
* Emit an event that includes a standard set of preview-specific properties for logging
*
* @private
* @param {string} name - event name
* @param {Object} payload - event payload object
*/
emitLogEvent(name, payload = {}) {
const file = this.file || {};

this.emit(name, {
...payload,
content_type: getProp(this.viewer, 'options.viewer.NAME', ''),
extension: file.extension || '',
file_id: getProp(file, 'id', ''),
file_version_id: getProp(file, 'file_version.id', ''),
locale: getProp(this.location, 'locale', ''),
rep_type: getProp(this.viewer, 'options.representation.representation', '').toLowerCase(),
timestamp: getISOTime(),
...getClientLogDetails(),
});
}

/**
* Builds a list of required XHR headers.
*
Expand Down
58 changes: 58 additions & 0 deletions src/lib/PreviewPerf.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
interface PerformancePaintTiming {
loadTime?: DOMHighResTimeStamp;
renderTime?: DOMHighResTimeStamp;
}

interface PerformanceReport {
fcp?: number;
lcp?: number;
}

export default class PreviewPerf {
private fcpObserver: PerformanceObserver;

private lcpObserver: PerformanceObserver;

private performance: Performance = performance;

private performanceReport: PerformanceReport = {};

/**
* Performance metrics are recorded in a global context. We use only unbuffered metrics to avoid skewed data,
* as buffered values can be set based on whatever page the user *first* landed on, which may not be preview.
*
* Glossary:
* - FCP - First Contentful Paint (usually loading screen)
* - LCP - Largest Contentful Paint (usually full content preview)
*/
constructor() {
this.fcpObserver = new window.PerformanceObserver(this.handleFcp.bind(this));
this.fcpObserver.observe({ entryTypes: ['paint'] });

this.lcpObserver = new window.PerformanceObserver(this.handleLcp.bind(this));
this.lcpObserver.observe({ entryTypes: ['largest-contentful-paint'] });
}

/**
* Returns defined metrics if the following conditions are satisfied:
* 1) it's recorded by the browser at all (some are Chrome-only, for now)
* 2) if it was logged *after* the Preview SDK was loaded (not buffered)
*/
public report(): PerformanceReport {
return this.performanceReport;
}

protected handleFcp(list: PerformanceObserverEntryList): void {
const fcpEntries = list.getEntriesByName('first-contentful-paint') || [];
const fcpEntry = fcpEntries[0] || {};

this.performanceReport.fcp = Math.round(fcpEntry.startTime || 0);
}

protected handleLcp(list: PerformanceObserverEntryList): void {
const lcpEntries = (list.getEntries() as PerformancePaintTiming[]) || [];
const lcpEntry = lcpEntries[lcpEntries.length - 1] || {};

this.performanceReport.lcp = Math.round(lcpEntry.renderTime || lcpEntry.loadTime || 0);
}
}
Loading

0 comments on commit f704338

Please sign in to comment.