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',