diff --git a/android/app/src/main/java/com/mattermost/rnbeta/MainApplication.java b/android/app/src/main/java/com/mattermost/rnbeta/MainApplication.java
index 33972e24b..1110ff008 100644
--- a/android/app/src/main/java/com/mattermost/rnbeta/MainApplication.java
+++ b/android/app/src/main/java/com/mattermost/rnbeta/MainApplication.java
@@ -44,6 +44,15 @@ import com.wix.reactnativenotifications.core.AppLaunchHelper;
import com.wix.reactnativenotifications.core.AppLifecycleFacade;
import com.wix.reactnativenotifications.core.JsIOHelper;
+import com.facebook.react.ReactPackage;
+import com.facebook.react.bridge.Arguments;
+import com.facebook.react.bridge.ReactContext;
+import com.facebook.react.bridge.ReactMarker;
+import com.facebook.react.bridge.ReactMarkerConstants;
+import com.facebook.react.bridge.WritableMap;
+import com.facebook.react.modules.core.DeviceEventManagerModule;
+import android.support.annotation.Nullable;
+
import android.util.Log;
public class MainApplication extends NavigationApplication implements INotificationsApplication, INotificationsDrawerApplication {
@@ -51,6 +60,14 @@ public class MainApplication extends NavigationApplication implements INotificat
public Boolean sharedExtensionIsOpened = false;
public Boolean replyFromPushNotification = false;
+ public long APP_START_TIME;
+
+ public long RELOAD;
+ public long CONTENT_APPEARED;
+
+ public long PROCESS_PACKAGES_START;
+ public long PROCESS_PACKAGES_END;
+
@Override
public boolean isDebug() {
return BuildConfig.DEBUG;
@@ -109,6 +126,9 @@ public class MainApplication extends NavigationApplication implements INotificat
setActivityCallbacks(notificationsLifecycleFacade);
SoLoader.init(this, /* native exopackage */ false);
+
+ // Uncomment to listen to react markers for build that has telemetry enabled
+ // addReactMarkerListener();
}
@Override
@@ -133,4 +153,36 @@ public class MainApplication extends NavigationApplication implements INotificat
public IPushNotificationsDrawer getPushNotificationsDrawer(Context context, AppLaunchHelper defaultAppLaunchHelper) {
return new CustomPushNotificationDrawer(context, defaultAppLaunchHelper);
}
+
+ private void addReactMarkerListener() {
+ ReactMarker.addListener(new ReactMarker.MarkerListener() {
+ @Override
+ public void logMarker(ReactMarkerConstants name, @Nullable String tag, int instanceKey) {
+ if (name.toString() == ReactMarkerConstants.RELOAD.toString()) {
+ APP_START_TIME = System.currentTimeMillis();
+ RELOAD = System.currentTimeMillis();
+ } else if (name.toString() == ReactMarkerConstants.PROCESS_PACKAGES_START.toString()) {
+ PROCESS_PACKAGES_START = System.currentTimeMillis();
+ } else if (name.toString() == ReactMarkerConstants.PROCESS_PACKAGES_END.toString()) {
+ PROCESS_PACKAGES_END = System.currentTimeMillis();
+ } else if (name.toString() == ReactMarkerConstants.CONTENT_APPEARED.toString()) {
+ CONTENT_APPEARED = System.currentTimeMillis();
+ ReactContext ctx = getReactGateway().getReactContext();
+
+ if (ctx != null) {
+ WritableMap map = Arguments.createMap();
+
+ map.putDouble("appReload", RELOAD);
+ map.putDouble("appContentAppeared", CONTENT_APPEARED);
+
+ map.putDouble("processPackagesStart", PROCESS_PACKAGES_START);
+ map.putDouble("processPackagesEnd", PROCESS_PACKAGES_END);
+
+ ctx.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class).
+ emit("nativeMetrics", map);
+ }
+ }
+ }
+ });
+ }
}
diff --git a/app/actions/views/channel.js b/app/actions/views/channel.js
index 08a8e064e..c6f68032a 100644
--- a/app/actions/views/channel.js
+++ b/app/actions/views/channel.js
@@ -34,6 +34,8 @@ import {
} from 'mattermost-redux/selectors/entities/channels';
import {getCurrentTeamId, getTeamByName} from 'mattermost-redux/selectors/entities/teams';
+import telemetry from 'app/telemetry';
+
import {
getChannelByName,
getDirectChannelName,
@@ -536,6 +538,12 @@ export function leaveChannel(channel, reset = false) {
}
export function setChannelLoading(loading = true) {
+ if (loading) {
+ telemetry.start(['channel:loading']);
+ } else {
+ telemetry.end(['channel:loading']);
+ }
+
return {
type: ViewTypes.SET_CHANNEL_LOADER,
loading,
@@ -593,6 +601,9 @@ export function increasePostVisibility(channelId, postId) {
return true;
}
+ telemetry.reset();
+ telemetry.start(['posts:loading']);
+
dispatch({
type: ViewTypes.LOADING_POSTS,
data: true,
@@ -630,6 +641,9 @@ export function increasePostVisibility(channelId, postId) {
}
dispatch(batchActions(actions));
+ telemetry.end(['posts:loading']);
+ telemetry.save();
+
return hasMorePost;
};
}
diff --git a/app/components/post/post.js b/app/components/post/post.js
index 703168907..f9a905c3e 100644
--- a/app/components/post/post.js
+++ b/app/components/post/post.js
@@ -42,6 +42,7 @@ export default class Post extends PureComponent {
renderReplies: PropTypes.bool,
isFirstReply: PropTypes.bool,
isLastReply: PropTypes.bool,
+ isLastPost: PropTypes.bool,
consecutivePost: PropTypes.bool,
hasComments: PropTypes.bool,
isSearchResult: PropTypes.bool,
@@ -237,6 +238,7 @@ export default class Post extends PureComponent {
channelIsReadOnly,
commentedOnPost,
highlight,
+ isLastPost,
isLastReply,
isSearchResult,
onHashtagPress,
@@ -336,6 +338,7 @@ export default class Post extends PureComponent {
ref={'postBody'}
highlight={highlight}
channelIsReadOnly={channelIsReadOnly}
+ isLastPost={isLastPost}
isSearchResult={isSearchResult}
navigator={this.props.navigator}
onFailedPostPress={this.handleFailedPostPress}
diff --git a/app/components/post_body/post_body.js b/app/components/post_body/post_body.js
index df6ad9f26..1116aad16 100644
--- a/app/components/post_body/post_body.js
+++ b/app/components/post_body/post_body.js
@@ -24,6 +24,8 @@ import {getMarkdownTextStyles, getMarkdownBlockStyles} from 'app/utils/markdown'
import {preventDoubleTap} from 'app/utils/tap';
import {changeOpacity, makeStyleSheetFromTheme} from 'app/utils/theme';
+import telemetry from 'app/telemetry';
+
let FileAttachmentList;
let PostAddChannelMember;
let PostBodyAdditionalContent;
@@ -43,6 +45,7 @@ export default class PostBody extends PureComponent {
highlight: PropTypes.bool,
isFailed: PropTypes.bool,
isFlagged: PropTypes.bool,
+ isLastPost: PropTypes.bool,
isPending: PropTypes.bool,
isPostAddChannelMember: PropTypes.bool,
isPostEphemeral: PropTypes.bool,
@@ -95,6 +98,17 @@ export default class PostBody extends PureComponent {
isLongPost: false,
};
+ logTelemetry = () => {
+ telemetry.end([
+ 'channel:switch_initial',
+ 'channel:switch_loaded',
+ 'posts:list_update',
+ 'team:switch',
+ 'start:overall',
+ ]);
+ telemetry.save();
+ }
+
measurePost = (event) => {
const {height} = event.nativeEvent.layout;
const {showLongPost} = this.props;
@@ -104,6 +118,10 @@ export default class PostBody extends PureComponent {
isLongPost: true,
});
}
+
+ if (this.props.isLastPost) {
+ this.logTelemetry();
+ }
};
openLongPost = preventDoubleTap(() => {
diff --git a/app/components/post_list/post_list.js b/app/components/post_list/post_list.js
index 9e438050e..49f13e07d 100644
--- a/app/components/post_list/post_list.js
+++ b/app/components/post_list/post_list.js
@@ -15,6 +15,7 @@ import mattermostManaged from 'app/mattermost_managed';
import {makeExtraData} from 'app/utils/list_view';
import {changeOpacity} from 'app/utils/theme';
import {matchDeepLink} from 'app/utils/url';
+import telemetry from 'app/telemetry';
import DateHeader from './date_header';
import NewMessagesDivider from './new_messages_divider';
@@ -48,6 +49,7 @@ export default class PostList extends PureComponent {
highlightPostId: PropTypes.string,
initialIndex: PropTypes.number,
isSearchResult: PropTypes.bool,
+ lastPostIndex: PropTypes.number.isRequired,
lastViewedAt: PropTypes.number, // Used by container // eslint-disable-line no-unused-prop-types
navigator: PropTypes.object,
onLoadMoreUp: PropTypes.func,
@@ -103,6 +105,8 @@ export default class PostList extends PureComponent {
this.handleDeepLink(this.props.deepLinkURL);
this.props.actions.setDeepLinkURL('');
}
+
+ telemetry.start(['posts:list_update']);
}
componentWillUnmount() {
@@ -256,6 +260,7 @@ export default class PostList extends PureComponent {
);
diff --git a/app/components/post_list/post_list.test.js b/app/components/post_list/post_list.test.js
index c0d3e1129..0c6455843 100644
--- a/app/components/post_list/post_list.test.js
+++ b/app/components/post_list/post_list.test.js
@@ -23,6 +23,7 @@ describe('PostList', () => {
navigator: {
showModal: jest.fn(),
},
+ lastPostIndex: -1,
postIds: ['post-id-1', 'post-id-2'],
serverURL,
siteURL: 'https://site-url.fake',
diff --git a/app/components/sidebars/drawer_layout.js b/app/components/sidebars/drawer_layout.js
index bb4d0ee90..6834b9669 100644
--- a/app/components/sidebars/drawer_layout.js
+++ b/app/components/sidebars/drawer_layout.js
@@ -16,6 +16,8 @@ import {
I18nManager,
} from 'react-native';
+import telemetry from 'app/telemetry';
+
const MIN_SWIPE_DISTANCE = 3;
const DEVICE_WIDTH = parseFloat(Dimensions.get('window').width);
const THRESHOLD = DEVICE_WIDTH / 2;
@@ -234,6 +236,9 @@ export default class DrawerLayout extends Component {
...options,
}).start(() => {
if (this.props.onDrawerOpen) {
+ telemetry.end(['channel:open_drawer']);
+ telemetry.save();
+
this.props.onDrawerOpen();
}
this._emitStateChanged(IDLE);
@@ -250,6 +255,7 @@ export default class DrawerLayout extends Component {
...options,
}).start(() => {
if (this.props.onDrawerClose) {
+ telemetry.end(['channel:close_drawer']);
this.props.onDrawerClose();
}
this._emitStateChanged(IDLE);
diff --git a/app/components/sidebars/main/index.js b/app/components/sidebars/main/index.js
index 8d96e5fd5..436e994c1 100644
--- a/app/components/sidebars/main/index.js
+++ b/app/components/sidebars/main/index.js
@@ -12,9 +12,28 @@ import {getCurrentTeamId, getMyTeamsCount} from 'mattermost-redux/selectors/enti
import {setChannelDisplayName, setChannelLoading} from 'app/actions/views/channel';
import {makeDirectChannel} from 'app/actions/views/more_dms';
import {isLandscape, isTablet, getDimensions} from 'app/selectors/device';
+import telemetry from 'app/telemetry';
import MainSidebar from './main_sidebar.js';
+export function logChannelSwitch(channelId, currentChannelId) {
+ return (dispatch, getState) => {
+ if (channelId === currentChannelId) {
+ return;
+ }
+
+ const metrics = [];
+ if (getState().entities.posts.postsInChannel[channelId]) {
+ metrics.push('channel:switch_loaded');
+ } else {
+ metrics.push('channel:switch_initial');
+ }
+
+ telemetry.reset();
+ telemetry.start(metrics);
+ };
+}
+
function mapStateToProps(state) {
const {currentUserId} = state.entities.users;
@@ -34,6 +53,7 @@ function mapDispatchToProps(dispatch) {
actions: bindActionCreators({
getTeams,
joinChannel,
+ logChannelSwitch,
makeDirectChannel,
setChannelDisplayName,
setChannelLoading,
diff --git a/app/components/sidebars/main/main_sidebar.js b/app/components/sidebars/main/main_sidebar.js
index 0b9b246b9..af165e900 100644
--- a/app/components/sidebars/main/main_sidebar.js
+++ b/app/components/sidebars/main/main_sidebar.js
@@ -23,6 +23,8 @@ import ChannelsList from './channels_list';
import DrawerSwiper from './drawer_swipper';
import TeamsList from './teams_list';
+import telemetry from 'app/telemetry';
+
const DRAWER_INITIAL_OFFSET = 40;
const DRAWER_LANDSCAPE_OFFSET = 150;
@@ -30,6 +32,7 @@ export default class ChannelSidebar extends Component {
static propTypes = {
actions: PropTypes.shape({
getTeams: PropTypes.func.isRequired,
+ logChannelSwitch: PropTypes.func.isRequired,
makeDirectChannel: PropTypes.func.isRequired,
setChannelDisplayName: PropTypes.func.isRequired,
setChannelLoading: PropTypes.func.isRequired,
@@ -165,11 +168,14 @@ export default class ChannelSidebar extends Component {
};
selectChannel = (channel, currentChannelId, closeDrawer = true) => {
- const {setChannelLoading} = this.props.actions;
+ const {logChannelSwitch, setChannelLoading} = this.props.actions;
+
+ logChannelSwitch(channel.id, currentChannelId);
tracker.channelSwitch = Date.now();
if (closeDrawer) {
+ telemetry.start(['channel:close_drawer']);
this.closeChannelDrawer();
setChannelLoading(channel.id !== currentChannelId);
}
diff --git a/app/components/sidebars/main/teams_list/teams_list.js b/app/components/sidebars/main/teams_list/teams_list.js
index 1af160164..e6e0bc236 100644
--- a/app/components/sidebars/main/teams_list/teams_list.js
+++ b/app/components/sidebars/main/teams_list/teams_list.js
@@ -19,6 +19,7 @@ import {ListTypes, ViewTypes} from 'app/constants';
import {preventDoubleTap} from 'app/utils/tap';
import {changeOpacity, makeStyleSheetFromTheme} from 'app/utils/theme';
import tracker from 'app/utils/time_tracker';
+import telemetry from 'app/telemetry';
import TeamsListItem from './teams_list_item';
@@ -55,9 +56,15 @@ export default class TeamsList extends PureComponent {
}
selectTeam = (teamId) => {
+ const {actions, closeChannelDrawer, currentTeamId} = this.props;
+
+ if (teamId !== currentTeamId) {
+ telemetry.reset();
+ telemetry.start(['team:switch']);
+ }
+
StatusBar.setHidden(false, 'slide');
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 6d2ec37b8..293ce947a 100644
--- a/app/mattermost.js
+++ b/app/mattermost.js
@@ -45,6 +45,7 @@ import {deleteFileCache} from 'app/utils/file';
import avoidNativeBridge from 'app/utils/avoid_native_bridge';
import {t} from 'app/utils/i18n';
import LocalConfig from 'assets/config';
+import telemetry from 'app/telemetry';
import App from './app';
import './fetch_preconfig';
@@ -435,6 +436,11 @@ const handleAppInActive = () => {
AppState.addEventListener('change', handleAppStateChange);
const launchEntry = () => {
+ telemetry.start([
+ 'start:select_server_screen',
+ 'start:channel_screen',
+ ]);
+
Navigation.startSingleScreenApp({
screen: {
screen: 'Entry',
@@ -452,6 +458,8 @@ const launchEntry = () => {
},
animationType: 'fade',
});
+
+ telemetry.startSinceLaunch(['start:splash_screen']);
};
configurePushNotifications();
diff --git a/app/screens/channel/channel.js b/app/screens/channel/channel.js
index 99748f8a0..d61503860 100644
--- a/app/screens/channel/channel.js
+++ b/app/screens/channel/channel.js
@@ -32,6 +32,8 @@ import PushNotifications from 'app/push_notifications';
import tracker from 'app/utils/time_tracker';
import LocalConfig from 'assets/config';
+import telemetry from 'app/telemetry';
+
import ChannelNavBar from './channel_nav_bar';
import ChannelPostList from './channel_post_list';
@@ -104,6 +106,8 @@ export default class Channel extends PureComponent {
}
EventEmitter.emit('renderDrawer');
+
+ telemetry.end(['start:channel_screen']);
}
componentWillReceiveProps(nextProps) {
diff --git a/app/screens/channel/channel_nav_bar/channel_drawer_button.js b/app/screens/channel/channel_nav_bar/channel_drawer_button.js
index 7c90c4c21..3341b6774 100644
--- a/app/screens/channel/channel_nav_bar/channel_drawer_button.js
+++ b/app/screens/channel/channel_nav_bar/channel_drawer_button.js
@@ -17,6 +17,8 @@ import {getTheme} from 'mattermost-redux/selectors/entities/preferences';
import {preventDoubleTap} from 'app/utils/tap';
import {makeStyleSheetFromTheme} from 'app/utils/theme';
+import telemetry from 'app/telemetry';
+
import {getUnreadsInCurrentTeam} from 'mattermost-redux/selectors/entities/channels';
import {getCurrentTeamId, getTeamMemberships} from 'mattermost-redux/selectors/entities/teams';
import EventEmitter from 'mattermost-redux/utils/event_emitter';
@@ -69,6 +71,7 @@ class ChannelDrawerButton extends PureComponent {
};
handlePress = preventDoubleTap(() => {
+ telemetry.start(['channel:open_drawer']);
this.props.openDrawer();
});
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 7e995bf78..d0cd21aea 100644
--- a/app/screens/channel/channel_post_list/channel_post_list.js
+++ b/app/screens/channel/channel_post_list/channel_post_list.js
@@ -9,6 +9,8 @@ import {
View,
} from 'react-native';
+import {getLastPostIndex} from 'mattermost-redux/utils/post_list';
+
import AnnouncementBanner from 'app/components/announcement_banner';
import PostList from 'app/components/post_list';
import PostListRetry from 'app/components/post_list_retry';
@@ -190,6 +192,7 @@ export default class ChannelPostList extends PureComponent {
component = (
{
+ telemetry.remove(['start:overall']);
+
const {navigator} = this.props;
tracker.initialLoad = Date.now();
diff --git a/app/screens/select_server/select_server.js b/app/screens/select_server/select_server.js
index ba70c7154..e10e1b031 100644
--- a/app/screens/select_server/select_server.js
+++ b/app/screens/select_server/select_server.js
@@ -34,6 +34,8 @@ import {preventDoubleTap} from 'app/utils/tap';
import tracker from 'app/utils/time_tracker';
import {t} from 'app/utils/i18n';
+import telemetry from 'app/telemetry';
+
import LocalConfig from 'assets/config';
export default class SelectServer extends PureComponent {
@@ -92,6 +94,9 @@ export default class SelectServer extends PureComponent {
this.certificateListener = DeviceEventEmitter.addListener('RNFetchBlobCertificate', this.selectCertificate);
this.props.navigator.setOnNavigatorEvent(this.handleNavigatorEvent);
+
+ telemetry.end(['start:select_server_screen']);
+ telemetry.save();
}
componentWillUpdate(nextProps, nextState) {
diff --git a/app/telemetry/index.js b/app/telemetry/index.js
new file mode 100644
index 000000000..d28de41c1
--- /dev/null
+++ b/app/telemetry/index.js
@@ -0,0 +1,6 @@
+// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
+// See LICENSE.txt for license information.
+
+import telemetry from './telemetry';
+
+export default telemetry;
diff --git a/app/telemetry/telemetry.android.js b/app/telemetry/telemetry.android.js
new file mode 100644
index 000000000..40eb56c58
--- /dev/null
+++ b/app/telemetry/telemetry.android.js
@@ -0,0 +1,158 @@
+// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
+// See LICENSE.txt for license information.
+
+import LocalConfig from 'assets/config'; // eslint-disable-line
+
+import {store} from 'app/mattermost';
+
+import {
+ saveToTelemetryServer,
+ getDeviceInfo,
+ setTraceRecord,
+} from './telemetry_utils';
+
+class Telemetry {
+ constructor() {
+ this.appStartTime = 0;
+ this.reactInitializedStartTime = 0;
+ this.reactInitializedEndTime = 0;
+ this.metrics = [];
+ this.currentMetrics = {};
+ this.pendingSinceLaunchMetrics = [];
+ }
+
+ setAppStartTime(startTime) {
+ this.appStartTime = startTime;
+ }
+
+ reset() {
+ this.metrics = [];
+ this.currentMetrics = {};
+ this.pendingSinceLaunchMetrics = [];
+ }
+
+ canSendTelemetry() {
+ const {config} = store.getState().entities.general;
+ return Boolean(!__DEV__ && config.EnableDiagnostics === 'true' && LocalConfig.TelemetryEnabled);
+ }
+
+ start(names = [], time = 0) {
+ if (this.canSendTelemetry()) {
+ const d = new Date();
+ const startTime = d.getTime();
+
+ names.forEach((name) => {
+ this.currentMetrics[name] = {
+ name,
+ startTime: time || startTime,
+ };
+ });
+ }
+ }
+
+ end(names = []) {
+ if (this.canSendTelemetry()) {
+ const d = new Date();
+ const endTime = d.getTime();
+ names.forEach((name) => {
+ const finalMetric = this.currentMetrics[name];
+
+ if (finalMetric && finalMetric.startTime > 0) {
+ this.metrics.push({
+ ...finalMetric,
+ endTime,
+ });
+
+ Reflect.deleteProperty(this.currentMetrics, name);
+ }
+ });
+ }
+ }
+
+ include(metrics = []) {
+ if (this.canSendTelemetry()) {
+ metrics.forEach((metric) => {
+ this.metrics.push({
+ name: metric.name,
+ startTime: metric.startTime,
+ endTime: metric.endTime,
+ });
+ });
+ }
+ }
+
+ startSinceLaunch(names = []) {
+ if (this.canSendTelemetry()) {
+ const d = new Date();
+ const endTime = d.getTime();
+
+ names.forEach((name) => {
+ if (!this.appStartTime) {
+ this.pendingSinceLaunchMetrics.push({
+ name,
+ endTime,
+ });
+ return;
+ }
+
+ this.metrics.push({
+ name,
+ startTime: this.appStartTime,
+ endTime,
+ });
+ });
+ }
+ }
+
+ remove(names = []) {
+ names.forEach((name) => {
+ const currentMetric = this.currentMetrics[name];
+
+ if (currentMetric) {
+ Reflect.deleteProperty(this.currentMetrics, name);
+ }
+ });
+ }
+
+ save() {
+ if (!this.canSendTelemetry()) {
+ return;
+ }
+
+ this.pendingSinceLaunchMetrics.forEach((pendingMetric) => {
+ this.metrics.push({
+ ...pendingMetric,
+ startTime: this.appStartTime,
+ });
+ });
+
+ if (this.metrics.length === 0) {
+ return;
+ }
+
+ const metrics = this.metrics.filter((m) => m.endTime && m.startTime).map((metric) => {
+ const {name, startTime, endTime} = metric;
+ let dur;
+ if (endTime && startTime) {
+ dur = (endTime - startTime) * 1000;
+ }
+
+ return setTraceRecord({
+ name,
+ time: startTime * 1000,
+ dur,
+ });
+ });
+
+ const {config} = store.getState().entities.general;
+ const deviceInfo = getDeviceInfo();
+ deviceInfo.serverVersion = config.Version;
+
+ saveToTelemetryServer({trace_events: metrics, device_info: deviceInfo});
+
+ this.reset();
+ }
+}
+
+const telemetry = new Telemetry();
+export default telemetry;
diff --git a/app/telemetry/telemetry.ios.js b/app/telemetry/telemetry.ios.js
new file mode 100644
index 000000000..4a50b58b5
--- /dev/null
+++ b/app/telemetry/telemetry.ios.js
@@ -0,0 +1,34 @@
+// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
+// See LICENSE.txt for license information.
+
+class Telemetry {
+ constructor() {
+ this.appStartTime = 0;
+ this.reactInitializedStartTime = 0;
+ this.reactInitializedEndTime = 0;
+ this.metrics = [];
+ this.currentMetrics = {};
+ this.pendingSinceLaunchMetrics = [];
+ }
+
+ setAppStartTime = () => true;
+
+ reset = () => true;
+
+ canSendTelemetry = () => false;
+
+ start = () => true;
+
+ end = () => true;
+
+ include = () => true;
+
+ startSinceLaunch = () => true;
+
+ remove = () => true;
+
+ save = () => true;
+}
+
+const telemetry = new Telemetry();
+export default telemetry;
diff --git a/app/telemetry/telemetry_utils.js b/app/telemetry/telemetry_utils.js
new file mode 100644
index 000000000..2dd446f66
--- /dev/null
+++ b/app/telemetry/telemetry_utils.js
@@ -0,0 +1,100 @@
+// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
+// See LICENSE.txt for license information.
+
+import {Dimensions} from 'react-native';
+import DeviceInfo from 'react-native-device-info';
+
+import LocalConfig from 'assets/config';
+
+export function saveToTelemetryServer(data) {
+ const {
+ TelemetryEnabled,
+ TelemetryUrl,
+ TelemetryApiKey,
+ } = LocalConfig;
+
+ if (TelemetryEnabled && TelemetryUrl) {
+ fetch(TelemetryUrl, {
+ method: 'post',
+ body: JSON.stringify({data}),
+ headers: {
+ Accept: 'application/json',
+ 'Content-Type': 'application/json',
+ 'x-api-key': TelemetryApiKey,
+ },
+ });
+ }
+}
+
+const presetTID = {
+ 'start:overall': 0,
+ 'start:process_packages': 1,
+ 'start:content_appeared': 2,
+ 'start:select_server_screen': 3,
+ 'start:channel_screen': 4,
+ 'team:switch': 5,
+ 'channel:loading': 6,
+ 'channel:switch_loaded': 7,
+ 'channel:switch_initial': 8,
+ 'channel:close_drawer': 9,
+ 'channel:open_drawer': 10,
+ 'posts:loading': 11,
+ 'posts:list_update': 12,
+};
+
+export function setTraceRecord({
+ name,
+ time: ts,
+ dur = 0,
+ instanceKey = 0,
+ pid = 0,
+}) {
+ const tid = presetTID[name];
+
+ return {
+ cat: 'react-native',
+ ph: 'X',
+ name,
+ ts,
+ dur,
+ pid,
+ tid,
+ args: {
+ instanceKey,
+ tag: name,
+ },
+ };
+}
+
+export function getDeviceInfo() {
+ const {height, width} = Dimensions.get('window');
+
+ return {
+ api_level: DeviceInfo.getAPILevel(),
+ build_number: DeviceInfo.getBuildNumber(),
+ bundle_id: DeviceInfo.getBundleId(),
+ brand: DeviceInfo.getBrand(),
+ country: DeviceInfo.getDeviceCountry(),
+ device_id: DeviceInfo.getDeviceId(),
+ device_locale: DeviceInfo.getDeviceLocale().split('-')[0],
+ device_type: DeviceInfo.getDeviceType(),
+ device_unique_id: DeviceInfo.getUniqueID(),
+ height: height ? Math.floor(height) : 0,
+ is_emulator: DeviceInfo.isEmulator(),
+ is_tablet: DeviceInfo.isTablet(),
+ manufacturer: DeviceInfo.getManufacturer(),
+ max_memory: DeviceInfo.getMaxMemory(),
+ model: DeviceInfo.getModel(),
+ system_name: DeviceInfo.getSystemName(),
+ system_version: DeviceInfo.getSystemVersion(),
+ timezone: DeviceInfo.getTimezone(),
+ app_version: DeviceInfo.getVersion(),
+ width: width ? Math.floor(width) : 0,
+ };
+}
+
+export default {
+ getDeviceInfo,
+ setTraceRecord,
+};
+
diff --git a/assets/base/config.json b/assets/base/config.json
index 25f321eda..0abd40e17 100644
--- a/assets/base/config.json
+++ b/assets/base/config.json
@@ -42,5 +42,9 @@
"ShowSentryDebugOptions": false,
- "CustomRequestHeaders": {}
+ "CustomRequestHeaders": {},
+
+ "TelemetryEnabled": false,
+ "TelemetryUrl": "",
+ "TelemetryApiKey": ""
}
diff --git a/fastlane/Fastfile b/fastlane/Fastfile
index e9964e903..ccc9eb425 100644
--- a/fastlane/Fastfile
+++ b/fastlane/Fastfile
@@ -157,13 +157,6 @@ lane :configure do
json['SentryDsnAndroid'] = ENV['SENTRY_DSN_ANDROID']
end
- # Configure Telemetry if enabled
- if ENV['BETA_BUILD'] == 'true' && ENV['TELEMETRY_ENABLED'] == 'true' && ENV['TELEMETRY_URL'] != '' && ENV['TELEMETRY_API_KEY'] != ''
- json['TelemetryEnabled'] = true
- json['TelemetryUrl'] = ENV['TELEMETRY_URL']
- json['TelemetryApiKey'] = ENV['TELEMETRY_API_KEY']
- end
-
# Save the config.json file
save_config_json(json)
@@ -361,6 +354,7 @@ platform :android do
lane :build do
unless configured
configure
+ configure_telemetry_android
end
update_identifiers
replace_assets
@@ -383,6 +377,27 @@ platform :android do
)
end
+ desc 'Update config and module/listener for Android telemetry'
+ lane :configure_telemetry_android do
+ if ENV['TELEMETRY_ENABLED'] == 'true' && ENV['TELEMETRY_URL'] != '' && ENV['TELEMETRY_API_KEY'] != ''
+ # Update telemetry config
+ json = load_config_json
+ json['TelemetryEnabled'] = true
+ json['TelemetryUrl'] = ENV['TELEMETRY_URL']
+ json['TelemetryApiKey'] = ENV['TELEMETRY_API_KEY']
+ save_config_json(json)
+
+ beta_dir = './android/app/src/main/java/com/mattermost/rnbeta/'
+
+ # Listen to telemetry react markers
+ find_replace_string(
+ path_to_file: "#{beta_dir}MainApplication.java",
+ old_string: '// addReactMarkerListener();',
+ new_string: 'addReactMarkerListener();'
+ )
+ end
+ end
+
lane :update_identifiers do
# Set the name for the app
app_name = ENV['APP_NAME'] || 'Mattermost Beta'
diff --git a/index.js b/index.js
index 1b5b1e97e..56acb0bfb 100644
--- a/index.js
+++ b/index.js
@@ -2,14 +2,31 @@
// See LICENSE.txt for license information.
import 'react-native/Libraries/Core/InitializeCore';
-import {AppRegistry, Platform} from 'react-native';
+import {AppRegistry, DeviceEventEmitter, Platform} from 'react-native';
import 'react-native-gesture-handler';
+import LocalConfig from 'assets/config';
+
+import telemetry from 'app/telemetry';
+
import 'app/mattermost';
import ShareExtension from 'share_extension/android';
if (Platform.OS === 'android') {
AppRegistry.registerComponent('MattermostShare', () => ShareExtension);
+
+ if (LocalConfig.TelemetryEnabled) {
+ const metricsSubscription = DeviceEventEmitter.addListener('nativeMetrics', (metrics) => {
+ telemetry.setAppStartTime(metrics.appReload);
+ telemetry.include([
+ {name: 'start:process_packages', startTime: metrics.processPackagesStart, endTime: metrics.processPackagesEnd},
+ {name: 'start:content_appeared', startTime: metrics.appReload, endTime: metrics.appContentAppeared},
+ ]);
+ telemetry.start(['start:overall'], metrics.appReload);
+
+ DeviceEventEmitter.removeSubscription(metricsSubscription);
+ });
+ }
}
// Uncomment the snippet below if you want to update the modules
diff --git a/package-lock.json b/package-lock.json
index 0553422de..85b67477e 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -2700,6 +2700,7 @@
"resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz",
"integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=",
"dev": true,
+ "optional": true,
"requires": {
"is-extendable": "^0.1.0"
}
@@ -2744,7 +2745,8 @@
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
"integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=",
- "dev": true
+ "dev": true,
+ "optional": true
},
"is-glob": {
"version": "4.0.0",
@@ -4821,7 +4823,8 @@
},
"ansi-regex": {
"version": "2.1.1",
- "bundled": true
+ "bundled": true,
+ "optional": true
},
"aproba": {
"version": "1.2.0",
@@ -4839,11 +4842,13 @@
},
"balanced-match": {
"version": "1.0.0",
- "bundled": true
+ "bundled": true,
+ "optional": true
},
"brace-expansion": {
"version": "1.1.11",
"bundled": true,
+ "optional": true,
"requires": {
"balanced-match": "^1.0.0",
"concat-map": "0.0.1"
@@ -4856,15 +4861,18 @@
},
"code-point-at": {
"version": "1.1.0",
- "bundled": true
+ "bundled": true,
+ "optional": true
},
"concat-map": {
"version": "0.0.1",
- "bundled": true
+ "bundled": true,
+ "optional": true
},
"console-control-strings": {
"version": "1.1.0",
- "bundled": true
+ "bundled": true,
+ "optional": true
},
"core-util-is": {
"version": "1.0.2",
@@ -4967,7 +4975,8 @@
},
"inherits": {
"version": "2.0.3",
- "bundled": true
+ "bundled": true,
+ "optional": true
},
"ini": {
"version": "1.3.5",
@@ -4977,6 +4986,7 @@
"is-fullwidth-code-point": {
"version": "1.0.0",
"bundled": true,
+ "optional": true,
"requires": {
"number-is-nan": "^1.0.0"
}
@@ -4989,17 +4999,20 @@
"minimatch": {
"version": "3.0.4",
"bundled": true,
+ "optional": true,
"requires": {
"brace-expansion": "^1.1.7"
}
},
"minimist": {
"version": "0.0.8",
- "bundled": true
+ "bundled": true,
+ "optional": true
},
"minipass": {
"version": "2.3.5",
"bundled": true,
+ "optional": true,
"requires": {
"safe-buffer": "^5.1.2",
"yallist": "^3.0.0"
@@ -5016,6 +5029,7 @@
"mkdirp": {
"version": "0.5.1",
"bundled": true,
+ "optional": true,
"requires": {
"minimist": "0.0.8"
}
@@ -5088,7 +5102,8 @@
},
"number-is-nan": {
"version": "1.0.1",
- "bundled": true
+ "bundled": true,
+ "optional": true
},
"object-assign": {
"version": "4.1.1",
@@ -5098,6 +5113,7 @@
"once": {
"version": "1.4.0",
"bundled": true,
+ "optional": true,
"requires": {
"wrappy": "1"
}
@@ -5173,7 +5189,8 @@
},
"safe-buffer": {
"version": "5.1.2",
- "bundled": true
+ "bundled": true,
+ "optional": true
},
"safer-buffer": {
"version": "2.1.2",
@@ -5203,6 +5220,7 @@
"string-width": {
"version": "1.0.2",
"bundled": true,
+ "optional": true,
"requires": {
"code-point-at": "^1.0.0",
"is-fullwidth-code-point": "^1.0.0",
@@ -5220,6 +5238,7 @@
"strip-ansi": {
"version": "3.0.1",
"bundled": true,
+ "optional": true,
"requires": {
"ansi-regex": "^2.0.0"
}
@@ -5258,11 +5277,13 @@
},
"wrappy": {
"version": "1.0.2",
- "bundled": true
+ "bundled": true,
+ "optional": true
},
"yallist": {
"version": "3.0.3",
- "bundled": true
+ "bundled": true,
+ "optional": true
}
}
},
@@ -9839,8 +9860,8 @@
"integrity": "sha512-rUxjysqif/BZQH2yhd5Aaq7vXMSx9NdEsQcyA07uEzIvxgI7zIr33gGsh+RU0/XjmQpCW7RsVof1vlkvQVCK5A=="
},
"mattermost-redux": {
- "version": "github:mattermost/mattermost-redux#b854353ee96b875b6b87857932b83b1143131860",
- "from": "github:mattermost/mattermost-redux#b854353ee96b875b6b87857932b83b1143131860",
+ "version": "github:mattermost/mattermost-redux#0df3ec0182e9d9ec67397157611d887f5fbc1bf1",
+ "from": "github:mattermost/mattermost-redux#0df3ec0182e9d9ec67397157611d887f5fbc1bf1",
"requires": {
"deep-equal": "1.0.1",
"eslint-plugin-header": "3.0.0",
@@ -13122,7 +13143,8 @@
"version": "0.3.2",
"resolved": "https://registry.npmjs.org/array-unique/-/array-unique-0.3.2.tgz",
"integrity": "sha1-qJS3XUvE9s1nnvMkSp/Y9Gri1Cg=",
- "dev": true
+ "dev": true,
+ "optional": true
},
"braces": {
"version": "2.3.2",
@@ -13408,7 +13430,8 @@
"version": "6.0.2",
"resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.2.tgz",
"integrity": "sha512-s5kLOcnH0XqDO+FvuaLX8DDjZ18CGFk7VygH40QoKPUQhW4e2rvM0rwUq0t8IQDOwYSeLK01U90OjzBTme2QqA==",
- "dev": true
+ "dev": true,
+ "optional": true
},
"micromatch": {
"version": "3.1.10",
diff --git a/package.json b/package.json
index e7090a57c..0f04041f6 100644
--- a/package.json
+++ b/package.json
@@ -18,7 +18,7 @@
"intl": "1.2.5",
"jail-monkey": "2.0.0",
"jsc-android": "236355.1.1",
- "mattermost-redux": "github:mattermost/mattermost-redux#b854353ee96b875b6b87857932b83b1143131860",
+ "mattermost-redux": "github:mattermost/mattermost-redux#0df3ec0182e9d9ec67397157611d887f5fbc1bf1",
"mime-db": "1.40.0",
"moment-timezone": "0.5.23",
"prop-types": "15.7.2",
diff --git a/packager/moduleNames.js b/packager/moduleNames.js
index 56a7915a9..1552ce52f 100644
--- a/packager/moduleNames.js
+++ b/packager/moduleNames.js
@@ -207,6 +207,9 @@ module.exports = [
'app/store/index.js',
'app/store/middleware.js',
'app/store/thunk.js',
+ 'app/telemetry/index.js',
+ 'app/telemetry/telemetry.android.js',
+ 'app/telemetry/telemetry_utils.js',
'app/utils/avoid_native_bridge.js',
'app/utils/client_upgrade.js',
'app/utils/general.js',
diff --git a/packager/modulePaths.js b/packager/modulePaths.js
index 55ffbb83b..b94673ba6 100644
--- a/packager/modulePaths.js
+++ b/packager/modulePaths.js
@@ -179,6 +179,9 @@ module.exports = ['./node_modules/app/app.js',
'./node_modules/app/store/index.js',
'./node_modules/app/store/middleware.js',
'./node_modules/app/store/thunk.js',
+ './node_modules/app/telemetry/index.js',
+ './node_modules/app/telemetry/telemetry.android.js',
+ './node_modules/app/telemetry/telemetry_utils.js',
'./node_modules/app/utils/avoid_native_bridge.js',
'./node_modules/app/utils/client_upgrade.js',
'./node_modules/app/utils/general.js',