diff --git a/app/actions/views/root.js b/app/actions/views/root.js index 73d838492..43fbb0d24 100644 --- a/app/actions/views/root.js +++ b/app/actions/views/root.js @@ -9,6 +9,8 @@ import {getClientConfig, getDataRetentionPolicy, getLicenseConfig} from 'matterm import {getPosts} from 'mattermost-redux/actions/posts'; import {getMyTeams, getMyTeamMembers, selectTeam} from 'mattermost-redux/actions/teams'; +import {recordTime} from 'app/utils/segment'; + import { handleSelectChannel, setChannelDisplayName, @@ -113,6 +115,14 @@ export function createPost(post) { }; } +export function recordLoadTime(screenName, category) { + return async (dispatch, getState) => { + const {currentUserId} = getState().entities.users; + + recordTime(screenName, category, currentUserId); + }; +} + export default { loadConfigAndLicense, loadFromPushNotification, diff --git a/app/components/channel_drawer/channel_drawer.js b/app/components/channel_drawer/channel_drawer.js index 1eb2ddfef..fe479dd48 100644 --- a/app/components/channel_drawer/channel_drawer.js +++ b/app/components/channel_drawer/channel_drawer.js @@ -15,6 +15,7 @@ import SafeAreaView from 'app/components/safe_area_view'; import Drawer from 'app/components/drawer'; import {alertErrorWithFallback} from 'app/utils/general'; +import tracker from 'app/utils/time_tracker'; import ChannelsList from './channels_list'; import DrawerSwiper from './drawer_swipper'; @@ -207,6 +208,7 @@ export default class ChannelDrawer extends Component { markChannelAsViewed } = actions; + tracker.channelSwitch = Date.now(); setChannelLoading(channel.id !== currentChannelId); setChannelDisplayName(channel.display_name); diff --git a/app/components/channel_drawer/teams_list/teams_list.js b/app/components/channel_drawer/teams_list/teams_list.js index 12c5936e9..290b93eb0 100644 --- a/app/components/channel_drawer/teams_list/teams_list.js +++ b/app/components/channel_drawer/teams_list/teams_list.js @@ -16,6 +16,7 @@ import MaterialIcon from 'react-native-vector-icons/MaterialIcons'; import FormattedText from 'app/components/formatted_text'; import {wrapWithPreventDoubleTap} from 'app/utils/tap'; import {changeOpacity, makeStyleSheetFromTheme} from 'app/utils/theme'; +import tracker from 'app/utils/time_tracker'; import TeamsListItem from './teams_list_item'; @@ -46,6 +47,7 @@ class TeamsList extends PureComponent { requestAnimationFrame(() => { const {actions, closeChannelDrawer, currentTeamId} = this.props; if (teamId !== currentTeamId) { + tracker.teamSwitch = Date.now(); actions.handleTeamChange(teamId); } diff --git a/app/mattermost.js b/app/mattermost.js index 6a763fe2c..076e7d270 100644 --- a/app/mattermost.js +++ b/app/mattermost.js @@ -2,7 +2,6 @@ // See License.txt for license information. import 'babel-polyfill'; -import Analytics from 'analytics-react-native'; import Orientation from 'react-native-orientation'; import {Provider} from 'react-redux'; import {Navigation, NativeEventsReceiver} from 'react-native-navigation'; @@ -10,7 +9,6 @@ import {IntlProvider} from 'react-intl'; import { Alert, AppState, - Dimensions, InteractionManager, NativeModules, Platform @@ -52,6 +50,7 @@ import {registerScreens} from 'app/screens'; import configureStore from 'app/store'; import mattermostManaged from 'app/mattermost_managed'; import {deleteFileCache} from 'app/utils/file'; +import {init as initAnalytics} from 'app/utils/segment'; import { captureException, initializeSentry, @@ -151,38 +150,7 @@ export default class Mattermost { configureAnalytics = (config) => { if (config && config.DiagnosticsEnabled === 'true' && config.DiagnosticId && Config.SegmentApiKey) { - if (!global.analytics) { - const {height, width} = Dimensions.get('window'); - global.analytics = new Analytics(Config.SegmentApiKey); - global.analytics_context = { - app: { - version: DeviceInfo.getVersion(), - build: DeviceInfo.getBuildNumber() - }, - device: { - dimensions: { - height, - width - }, - isTablet: DeviceInfo.isTablet(), - os: DeviceInfo.getSystemVersion() - }, - server: config.Version - }; - - global.analytics.identify({ - userId: config.DiagnosticId, - context: global.analytics_context, - page: { - path: '', - referrer: '', - search: '', - title: '', - url: '' - }, - anonymousId: '00000000000000000000000000' - }); - } + initAnalytics(config); } else { global.analytics = null; } @@ -418,6 +386,16 @@ export default class Mattermost { this.orientationDidChange(orientation); } + const {config} = state.entities.general; + if (config) { + this.configureAnalytics(config); + } + + const {currentUserId} = state.entities.users; + if (currentUserId) { + Client4.setUserId(currentUserId); + } + const isNotActive = AppState.currentState !== 'active'; const notification = PushNotifications.getNotification(); if (notification) { diff --git a/app/screens/channel/channel.js b/app/screens/channel/channel.js index 9d2df5a9d..6c242854c 100644 --- a/app/screens/channel/channel.js +++ b/app/screens/channel/channel.js @@ -23,7 +23,7 @@ import {wrapWithPreventDoubleTap} from 'app/utils/tap'; import {makeStyleSheetFromTheme} from 'app/utils/theme'; import PostTextbox from 'app/components/post_textbox'; import networkConnectionListener from 'app/utils/network'; - +import tracker from 'app/utils/time_tracker'; import LocalConfig from 'assets/config'; import ChannelNavBar from './channel_nav_bar'; @@ -39,6 +39,7 @@ class Channel extends PureComponent { selectInitialChannel: PropTypes.func.isRequired, initWebSocket: PropTypes.func.isRequired, closeWebSocket: PropTypes.func.isRequired, + recordLoadTime: PropTypes.func.isRequired, startPeriodicStatusUpdates: PropTypes.func.isRequired, stopPeriodicStatusUpdates: PropTypes.func.isRequired }).isRequired, @@ -60,12 +61,24 @@ class Channel extends PureComponent { } } + componentDidMount() { + if (tracker.initialLoad) { + this.props.actions.recordLoadTime('Start time', 'initialLoad'); + } + } + componentWillReceiveProps(nextProps) { if (nextProps.currentTeamId && this.props.currentTeamId !== nextProps.currentTeamId) { this.loadChannels(nextProps.currentTeamId); } } + componentDidUpdate() { + if (tracker.teamSwitch) { + this.props.actions.recordLoadTime('Switch Team', 'teamSwitch'); + } + } + componentWillUnmount() { const {closeWebSocket, stopPeriodicStatusUpdates} = this.props.actions; diff --git a/app/screens/channel/channel_post_list/channel_post_list.js b/app/screens/channel/channel_post_list/channel_post_list.js index 54a396d61..4e38d5346 100644 --- a/app/screens/channel/channel_post_list/channel_post_list.js +++ b/app/screens/channel/channel_post_list/channel_post_list.js @@ -13,6 +13,7 @@ import { import PostList from 'app/components/post_list'; import PostListRetry from 'app/components/post_list_retry'; import RetryBarIndicator from 'app/components/retry_bar_indicator'; +import tracker from 'app/utils/time_tracker'; class ChannelPostList extends PureComponent { static propTypes = { @@ -21,6 +22,7 @@ class ChannelPostList extends PureComponent { loadThreadIfNecessary: PropTypes.func.isRequired, increasePostVisibility: PropTypes.func.isRequired, selectPost: PropTypes.func.isRequired, + recordLoadTime: PropTypes.func.isRequired, refreshChannelWithRetry: PropTypes.func.isRequired }).isRequired, channelId: PropTypes.string.isRequired, @@ -64,6 +66,12 @@ class ChannelPostList extends PureComponent { }); } + componentDidUpdate(prevProps) { + if (prevProps.channelId !== this.props.channelId && tracker.channelSwitch) { + this.props.actions.recordLoadTime('Switch Channel', 'channelSwitch'); + } + } + getVisiblePostIds = (props) => { return props.postIds.slice(0, props.postVisibility); }; diff --git a/app/screens/channel/channel_post_list/index.js b/app/screens/channel/channel_post_list/index.js index 2cb9bcfe7..b4c2c7e94 100644 --- a/app/screens/channel/channel_post_list/index.js +++ b/app/screens/channel/channel_post_list/index.js @@ -8,9 +8,11 @@ import {selectPost} from 'mattermost-redux/actions/posts'; import {getPostIdsInCurrentChannel} from 'mattermost-redux/selectors/entities/posts'; import {getCurrentChannelId, getMyCurrentChannelMembership, makeGetChannel} from 'mattermost-redux/selectors/entities/channels'; import {getCurrentUserId} from 'mattermost-redux/selectors/entities/users'; -import {loadPostsIfNecessaryWithRetry, loadThreadIfNecessary, increasePostVisibility, refreshChannelWithRetry} from 'app/actions/views/channel'; import {getTheme} from 'mattermost-redux/selectors/entities/preferences'; +import {loadPostsIfNecessaryWithRetry, loadThreadIfNecessary, increasePostVisibility, refreshChannelWithRetry} from 'app/actions/views/channel'; +import {recordLoadTime} from 'app/actions/views/root'; + import ChannelPostList from './channel_post_list'; function makeMapStateToProps() { @@ -41,6 +43,7 @@ function mapDispatchToProps(dispatch) { loadThreadIfNecessary, increasePostVisibility, selectPost, + recordLoadTime, refreshChannelWithRetry }, dispatch) }; diff --git a/app/screens/channel/index.js b/app/screens/channel/index.js index ec8b1ff90..50fa3ff70 100644 --- a/app/screens/channel/index.js +++ b/app/screens/channel/index.js @@ -16,6 +16,7 @@ import { selectInitialChannel } from 'app/actions/views/channel'; import {connection} from 'app/actions/device'; +import {recordLoadTime} from 'app/actions/views/root'; import {selectFirstAvailableTeam} from 'app/actions/views/select_team'; import {getTheme} from 'mattermost-redux/selectors/entities/preferences'; @@ -42,6 +43,7 @@ function mapDispatchToProps(dispatch) { selectInitialChannel, initWebSocket, closeWebSocket, + recordLoadTime, startPeriodicStatusUpdates, stopPeriodicStatusUpdates }, dispatch) diff --git a/app/screens/login/login.js b/app/screens/login/login.js index 2b1c0daa9..63e904ad4 100644 --- a/app/screens/login/login.js +++ b/app/screens/login/login.js @@ -25,6 +25,7 @@ import StatusBar from 'app/components/status_bar'; import PushNotifications from 'app/push_notifications'; import {GlobalStyles} from 'app/styles'; import {wrapWithPreventDoubleTap} from 'app/utils/tap'; +import tracker from 'app/utils/time_tracker'; import logo from 'assets/images/logo.png'; @@ -77,6 +78,7 @@ class Login extends PureComponent { goToLoadTeam = (expiresAt) => { const {intl, navigator, theme} = this.props; + tracker.initialLoad = Date.now(); if (expiresAt) { PushNotifications.localNotificationSchedule({ diff --git a/app/screens/root/root.js b/app/screens/root/root.js index 39d8bb14f..395e50666 100644 --- a/app/screens/root/root.js +++ b/app/screens/root/root.js @@ -8,6 +8,7 @@ import {Client, Client4} from 'mattermost-redux/client'; import Loading from 'app/components/loading'; import {stripTrailingSlashes} from 'app/utils/url'; +import tracker from 'app/utils/time_tracker'; export default class Root extends Component { static propTypes = { @@ -37,6 +38,7 @@ export default class Root extends Component { goToLoadTeam = () => { const {navigator, theme} = this.props; + tracker.initialLoad = Date.now(); navigator.resetTo({ screen: 'LoadTeam', title: '', diff --git a/app/screens/sso/sso.js b/app/screens/sso/sso.js index d7e2ebea3..39b22c0d5 100644 --- a/app/screens/sso/sso.js +++ b/app/screens/sso/sso.js @@ -20,6 +20,7 @@ import Loading from 'app/components/loading'; import StatusBar from 'app/components/status_bar'; import PushNotifications from 'app/push_notifications'; import {changeOpacity, makeStyleSheetFromTheme} from 'app/utils/theme'; +import tracker from 'app/utils/time_tracker'; const postMessageJS = "setTimeout(function() { postMessage(document.body.innerText, '*')});"; @@ -81,6 +82,7 @@ class SSO extends PureComponent { goToLoadTeam = (expiresAt) => { const {intl, navigator, theme} = this.props; + tracker.initialLoad = Date.now(); if (expiresAt) { PushNotifications.localNotificationSchedule({ diff --git a/app/utils/segment.js b/app/utils/segment.js new file mode 100644 index 000000000..6cbeb2ccc --- /dev/null +++ b/app/utils/segment.js @@ -0,0 +1,65 @@ +// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +import Analytics from 'analytics-react-native'; +import DeviceInfo from 'react-native-device-info'; +import {Dimensions} from 'react-native'; + +import Config from 'assets/config'; + +import tracker from './time_tracker'; + +let diagnosticId; + +export function init(config) { + if (!global.analytics) { + diagnosticId = config.DiagnosticId; + const {height, width} = Dimensions.get('window'); + global.analytics = new Analytics(Config.SegmentApiKey, {flushAt: 1}); + global.analytics_context = { + app: { + version: DeviceInfo.getVersion(), + build: DeviceInfo.getBuildNumber() + }, + device: { + dimensions: { + height, + width + }, + isTablet: DeviceInfo.isTablet(), + os: DeviceInfo.getSystemVersion() + }, + ip: '0.0.0.0', + server: config.Version + }; + + global.analytics.identify({ + userId: diagnosticId, + context: global.analytics_context, + page: { + path: '', + referrer: '', + search: '', + title: '', + url: '' + }, + anonymousId: '00000000000000000000000000' + }); + } +} + +export function recordTime(screenName, category, userId) { + if (global.analytics) { + const startTime = tracker[category]; + tracker[category] = 0; + global.analytics.screen({ + userId: diagnosticId, + name: screenName, + context: global.analytics_context, + properties: { + actual_user_id: userId, + time: Date.now() - startTime + } + }); + } +} diff --git a/app/utils/time_tracker.js b/app/utils/time_tracker.js new file mode 100644 index 000000000..389a8b060 --- /dev/null +++ b/app/utils/time_tracker.js @@ -0,0 +1,8 @@ +// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +export default { + initialLoad: 0, + channelSwitch: 0, + teamSwitch: 0 +};