diff --git a/src/Analytics.js b/src/Analytics.js index 5f4a0d0c778..2ef058b11b4 100644 --- a/src/Analytics.js +++ b/src/Analytics.js @@ -147,7 +147,10 @@ class Analytics { return true; } - trackPageChange() { + trackPageChange(generationTimeMs) { + if (typeof generationTimeMs !== 'number') { + throw new Error('Analytics.trackPageChange: expected generationTimeMs to be a number'); + } if (this.disabled) return; if (this.firstPage) { // De-duplicate first page @@ -156,6 +159,7 @@ class Analytics { return; } this._paq.push(['setCustomUrl', getRedactedUrl()]); + this._paq.push(['setGenerationTimeMs', generationTimeMs]); this._paq.push(['trackPageView']); } diff --git a/src/components/structures/MatrixChat.js b/src/components/structures/MatrixChat.js index 1312abda091..92baecb7877 100644 --- a/src/components/structures/MatrixChat.js +++ b/src/components/structures/MatrixChat.js @@ -291,6 +291,8 @@ export default React.createClass({ this.handleResize(); window.addEventListener('resize', this.handleResize); + this._pageChanging = false; + // check we have the right tint applied for this theme. // N.B. we don't call the whole of setTheme() here as we may be // racing with the theme CSS download finishing from index.js @@ -368,13 +370,58 @@ export default React.createClass({ window.removeEventListener('resize', this.handleResize); }, - componentDidUpdate: function() { + componentWillUpdate: function(props, state) { + if (this.shouldTrackPageChange(this.state, state)) { + this.startPageChangeTimer(); + } + }, + + componentDidUpdate: function(prevProps, prevState) { + if (this.shouldTrackPageChange(prevState, this.state)) { + const durationMs = this.stopPageChangeTimer(); + Analytics.trackPageChange(durationMs); + } if (this.focusComposer) { dis.dispatch({action: 'focus_composer'}); this.focusComposer = false; } }, + startPageChangeTimer() { + // This shouldn't happen because componentWillUpdate and componentDidUpdate + // are used. + if (this._pageChanging) { + console.warn('MatrixChat.startPageChangeTimer: timer already started'); + return; + } + this._pageChanging = true; + performance.mark('riot_MatrixChat_page_change_start'); + }, + + stopPageChangeTimer() { + if (!this._pageChanging) { + console.warn('MatrixChat.stopPageChangeTimer: timer not started'); + return; + } + this._pageChanging = false; + performance.mark('riot_MatrixChat_page_change_stop'); + performance.measure( + 'riot_MatrixChat_page_change_delta', + 'riot_MatrixChat_page_change_start', + 'riot_MatrixChat_page_change_stop', + ); + performance.clearMarks('riot_MatrixChat_page_change_start'); + performance.clearMarks('riot_MatrixChat_page_change_stop'); + const measurement = performance.getEntriesByName('riot_MatrixChat_page_change_delta').pop(); + return measurement.duration; + }, + + shouldTrackPageChange(prevState, state) { + return prevState.currentRoomId !== state.currentRoomId || + prevState.view !== state.view || + prevState.page_type !== state.page_type; + }, + setStateForNewView: function(state) { if (state.view === undefined) { throw new Error("setStateForNewView with no view!"); @@ -1341,7 +1388,6 @@ export default React.createClass({ if (this.props.onNewScreen) { this.props.onNewScreen(screen); } - Analytics.trackPageChange(); }, onAliasClick: function(event, alias) {