Request metrics improvements (#8420)

* Network metrics

* update network-client ref

* fix unit tests

* missing catch error parsing

* Replace API's to fetch all teams and channels for a user
Update groupLabel to use a type definition
Include effective Latency and Average Speed in the metrics
Include parallel and sequential request counts

* update network library and fix tests

* feat(MM-61865): send network request telemetry data to the server (#8436)

* feat(MM-61865): send network info telemetry data

* unit testing

* fix latency vs effectiveLatency

* cleanup test

* fix failing tests

* fix spelling error

* fix failing test

* more cleanup

* fix: improve parallel requests calculation (#8439)

* fix: improve parallel requests calculation

* fix test issue

* multiple parallelGroups testing

* calculateAverageSpeedWithCategories test

* categorizedRequests in it's own describe

* clean up duplicate tests

* check for when groupLabel is non-existent

* a case when groupLabel is non-existent

* more testing with effective latency.

* resolveAndFlattenModels with a capital F

* add try..catch within resolveAndFlattenModels

* update groupLabel to fix failing lint

* fix linting issue due to unknown group label

* forgot to push changes

* fix the indentation problem again

* add env var option for COLLECT_NETWORK_METRICS

---------

Co-authored-by: Rahim Rahman <rahim.rahman@mattermost.com>
Co-authored-by: Mattermost Build <build@mattermost.com>
This commit is contained in:
Elias Nahum 2025-01-17 04:55:29 +08:00 committed by GitHub
parent a314c6059f
commit d1d3fda83a
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
61 changed files with 1795 additions and 891 deletions

View file

@ -12,6 +12,7 @@ import PushNotifications from '@init/push_notifications';
import {
prepareDeleteChannel, prepareMyChannelsForTeam, queryAllMyChannel,
getMyChannel, getChannelById, queryUsersOnChannel, queryUserChannelsByTypes,
prepareAllMyChannels,
} from '@queries/servers/channel';
import {queryDisplayNamePreferences} from '@queries/servers/preference';
import {prepareCommonSystemValues, type PrepareCommonSystemValuesArgs, getCommonSystemValues, getCurrentTeamId, setCurrentChannelId, getCurrentUserId, getConfig, getLicense} from '@queries/servers/system';
@ -23,6 +24,7 @@ import {isTablet} from '@utils/helpers';
import {logError, logInfo} from '@utils/log';
import {displayGroupMessageName, displayUsername, getUserIdFromChannelName} from '@utils/user';
import type ServerDataOperator from '@database/operator/server_data_operator';
import type {Model} from '@nozbe/watermelondb';
import type ChannelModel from '@typings/database/models/servers/channel';
import type UserModel from '@typings/database/models/servers/user';
@ -237,6 +239,43 @@ export async function resetMessageCount(serverUrl: string, channelId: string) {
}
}
const resolveAndFlattenModels = async (
operator: ServerDataOperator,
modelPromises: Array<Promise<Model[]>>,
description: string,
prepareRecordsOnly: boolean,
) => {
try {
const models = await Promise.all(modelPromises);
const flattenedModels = models.flat();
if (prepareRecordsOnly || !flattenedModels.length) {
return {models: flattenedModels, error: undefined};
}
await operator.batchRecords(flattenedModels, description);
return {models: flattenedModels, error: undefined};
} catch (error) {
logError('Failed resolveAndFlattenModels', {error, description});
return {models: [], error};
}
};
export async function storeAllMyChannels(serverUrl: string, channels: Channel[], memberships: ChannelMembership[], isCRTEnabled: boolean, prepareRecordsOnly = false) {
try {
const {operator} = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
const cs = new Set(channels.map((c) => c.id));
const modelPromises: Array<Promise<Model[]>> = [
...await prepareAllMyChannels(operator, channels, memberships.filter((m) => cs.has(m.channel_id)), isCRTEnabled),
];
return resolveAndFlattenModels(operator, modelPromises, 'storeAllMyChannels', prepareRecordsOnly);
} catch (error) {
logError('Failed storeAllMyChannels', {error, serverUrl, channelsCount: channels.length});
return {error, models: undefined};
}
}
export async function storeMyChannelsForTeam(serverUrl: string, teamId: string, channels: Channel[], memberships: ChannelMembership[], prepareRecordsOnly = false, isCRTEnabled?: boolean) {
try {
const {operator} = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
@ -244,25 +283,10 @@ export async function storeMyChannelsForTeam(serverUrl: string, teamId: string,
...await prepareMyChannelsForTeam(operator, teamId, channels, memberships, isCRTEnabled),
];
const models = await Promise.all(modelPromises);
if (!models.length) {
return {models: []};
}
const flattenedModels = models.flat();
if (prepareRecordsOnly) {
return {models: flattenedModels};
}
if (flattenedModels.length) {
await operator.batchRecords(flattenedModels, 'storeMyChannelsForTeam');
}
return {models: flattenedModels};
return resolveAndFlattenModels(operator, modelPromises, 'storeMyChannelsForTeam', prepareRecordsOnly);
} catch (error) {
logError('Failed storeMyChannelsForTeam', error);
return {error};
return {error, models: undefined};
}
}

View file

@ -5,7 +5,7 @@
import {DeviceEventEmitter} from 'react-native';
import {addChannelToDefaultCategory, handleConvertedGMCategories, storeCategories} from '@actions/local/category';
import {markChannelAsViewed, removeCurrentUserFromChannel, setChannelDeleteAt, storeMyChannelsForTeam, switchToChannel} from '@actions/local/channel';
import {markChannelAsViewed, removeCurrentUserFromChannel, setChannelDeleteAt, storeAllMyChannels, storeMyChannelsForTeam, switchToChannel} from '@actions/local/channel';
import {switchToGlobalThreads} from '@actions/local/thread';
import {loadCallForChannel} from '@calls/actions/calls';
import {DeepLink, Events, General, Preferences, Screens} from '@constants';
@ -376,7 +376,7 @@ export async function fetchChannelCreator(serverUrl: string, channelId: string,
}
}
export async function fetchChannelStats(serverUrl: string, channelId: string, fetchOnly = false, groupLabel?: string) {
export async function fetchChannelStats(serverUrl: string, channelId: string, fetchOnly = false, groupLabel?: RequestGroupLabel) {
try {
const client = NetworkManager.getClient(serverUrl);
const {database, operator} = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
@ -404,10 +404,74 @@ export async function fetchChannelStats(serverUrl: string, channelId: string, fe
}
}
export async function fetchAllMyChannelsForAllTeams(serverUrl: string, since: number, isCRTEnabled: boolean, fetchOnly = false, groupLabel?: RequestGroupLabel) {
try {
if (!fetchOnly) {
setTeamLoading(serverUrl, true);
}
const client = NetworkManager.getClient(serverUrl);
const {operator} = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
const promises: [Promise<Channel[]>, Promise<ChannelMembership[]>] = [
client.getAllChannelsFromAllTeams(since, since > 0, groupLabel),
client.getAllMyChannelMembersFromAllTeams(0, General.CHANNEL_MEMBERS_CHUNK_SIZE, groupLabel),
];
const [channels, memberships] = await Promise.all(promises);
const remaining = Math.floor(channels.length / General.CHANNEL_MEMBERS_CHUNK_SIZE);
const remainingMembershipsPromises = [];
const categoriesPromises = [];
const teamIdsSet = channels.reduce<Set<string>>((set, c) => {
if (c.team_id) {
set.add(c.team_id);
}
return set;
}, new Set());
for (const teamId of teamIdsSet) {
categoriesPromises.push(client.getCategories('me', teamId, groupLabel));
}
if (remaining > 0) {
for (let i = 1; i <= remaining; i++) {
remainingMembershipsPromises.push(client.getAllMyChannelMembersFromAllTeams(i, General.CHANNEL_MEMBERS_CHUNK_SIZE, groupLabel));
}
}
const results = await Promise.all([...remainingMembershipsPromises, ...categoriesPromises]);
const categories: CategoryWithChannels[] = [];
for (const result of results) {
if (Array.isArray(result)) {
memberships.push(...result);
} else if ('categories' in result) {
categories.push(...result.categories);
}
}
if (!fetchOnly) {
const {models: chModels} = await storeAllMyChannels(serverUrl, channels, memberships, isCRTEnabled, true);
const {models: catModels} = await storeCategories(serverUrl, categories, true, true); // Re-sync
const models = (chModels || []).concat(catModels || []);
if (models.length) {
await operator.batchRecords(models, 'fetchAllMyChannelsForAllTeams');
}
setTeamLoading(serverUrl, false);
}
return {channels, memberships, categories};
} catch (error) {
logDebug('error on fetchMyChannelsForTeam', getFullErrorMessage(error));
if (!fetchOnly) {
setTeamLoading(serverUrl, false);
}
forceLogoutIfNecessary(serverUrl, error);
return {error};
}
}
export async function fetchMyChannelsForTeam(
serverUrl: string, teamId: string, includeDeleted = true,
since = 0, fetchOnly = false, excludeDirect = false,
isCRTEnabled?: boolean, groupLabel?: string,
isCRTEnabled?: boolean, groupLabel?: RequestGroupLabel,
): Promise<MyChannelsRequest> {
try {
if (!fetchOnly) {
@ -457,7 +521,7 @@ export async function fetchMyChannelsForTeam(
}
}
export async function fetchMyChannel(serverUrl: string, teamId: string, channelId: string, fetchOnly = false, groupLabel?: string): Promise<MyChannelsRequest> {
export async function fetchMyChannel(serverUrl: string, teamId: string, channelId: string, fetchOnly = false, groupLabel?: RequestGroupLabel): Promise<MyChannelsRequest> {
try {
const client = NetworkManager.getClient(serverUrl);
@ -484,7 +548,7 @@ export async function fetchMyChannel(serverUrl: string, teamId: string, channelI
export async function fetchMissingDirectChannelsInfo(
serverUrl: string, directChannels: Channel[], locale?: string,
teammateDisplayNameSetting?: string, currentUserId?: string,
fetchOnly = false, groupLabel?: string,
fetchOnly = false, groupLabel?: RequestGroupLabel,
) {
try {
const {database, operator} = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
@ -662,7 +726,7 @@ export async function joinChannelIfNeeded(serverUrl: string, channelId: string)
}
}
export async function markChannelAsRead(serverUrl: string, channelId: string, updateLocal = false, groupLabel?: string) {
export async function markChannelAsRead(serverUrl: string, channelId: string, updateLocal = false, groupLabel?: RequestGroupLabel) {
try {
const client = NetworkManager.getClient(serverUrl);
await client.viewMyChannel(channelId, undefined, groupLabel);
@ -1047,7 +1111,7 @@ export async function getChannelTimezones(serverUrl: string, channelId: string)
}
}
export async function switchToChannelById(serverUrl: string, channelId: string, teamId?: string, skipLastUnread = false, groupLabel?: string) {
export async function switchToChannelById(serverUrl: string, channelId: string, teamId?: string, skipLastUnread = false, groupLabel?: RequestGroupLabel) {
if (channelId === Screens.GLOBAL_THREADS) {
return switchToGlobalThreads(serverUrl, teamId);
}
@ -1059,7 +1123,7 @@ export async function switchToChannelById(serverUrl: string, channelId: string,
DeviceEventEmitter.emit(Events.CHANNEL_SWITCH, true);
fetchPostsForChannel(serverUrl, channelId, false, groupLabel);
fetchPostsForChannel(serverUrl, channelId, false, false, groupLabel);
fetchChannelBookmarks(serverUrl, channelId, false, groupLabel);
await switchToChannel(serverUrl, channelId, teamId, skipLastUnread);
openChannelIfNeeded(serverUrl, channelId, groupLabel);
@ -1258,8 +1322,10 @@ export const handleKickFromChannel = async (serverUrl: string, channelId: string
const currentServer = await getActiveServer();
if (currentServer?.url === serverUrl) {
const channel = await getChannelById(database, channelId);
DeviceEventEmitter.emit(event, channel?.displayName);
await dismissAllModalsAndPopToRoot();
if (channel) {
DeviceEventEmitter.emit(event, channel.displayName);
await dismissAllModalsAndPopToRoot();
}
}
const tabletDevice = isTablet();

View file

@ -11,7 +11,7 @@ import {logError} from '@utils/log';
import {forceLogoutIfNecessary} from './session';
export async function fetchChannelBookmarks(serverUrl: string, channelId: string, fetchOnly = false, groupLabel?: string) {
export async function fetchChannelBookmarks(serverUrl: string, channelId: string, fetchOnly = false, groupLabel?: RequestGroupLabel) {
try {
const client = NetworkManager.getClient(serverUrl);
const {database, operator} = DatabaseManager.getServerDatabaseAndOperator(serverUrl);

View file

@ -4,6 +4,7 @@
import {setLastServerVersionCheck} from '@actions/local/systems';
import {fetchConfigAndLicense} from '@actions/remote/systems';
import DatabaseManager from '@database/manager';
import PerformanceMetricsManager from '@managers/performance_metrics_manager';
import WebsocketManager from '@managers/websocket_manager';
import {prepareCommonSystemValues} from '@queries/servers/system';
import {deleteV1Data} from '@utils/file';
@ -16,6 +17,8 @@ export async function appEntry(serverUrl: string, since = 0) {
return {error: `${serverUrl} database not found`};
}
PerformanceMetricsManager.startTimeToInteraction();
if (!since) {
if (Object.keys(DatabaseManager.serverDatabases).length === 1) {
await setLastServerVersionCheck(serverUrl, true);
@ -28,7 +31,7 @@ export async function appEntry(serverUrl: string, since = 0) {
await operator.batchRecords(removeLastUnreadChannelId, 'appEntry - removeLastUnreadChannelId');
}
WebsocketManager.openAll('entry');
WebsocketManager.openAll('Cold Start');
verifyPushProxy(serverUrl);

View file

@ -4,13 +4,13 @@
import {nativeApplicationVersion} from 'expo-application';
import {RESULTS, checkNotifications} from 'react-native-permissions';
import {fetchMissingDirectChannelsInfo, fetchMyChannelsForTeam, handleKickFromChannel, type MyChannelsRequest} from '@actions/remote/channel';
import {fetchAllMyChannelsForAllTeams, fetchMissingDirectChannelsInfo, handleKickFromChannel, type MyChannelsRequest} from '@actions/remote/channel';
import {fetchGroupsForMember} from '@actions/remote/groups';
import {fetchPostsForUnreadChannels} from '@actions/remote/post';
import {type MyPreferencesRequest, fetchMyPreferences} from '@actions/remote/preference';
import {fetchRoles} from '@actions/remote/role';
import {fetchConfigAndLicense, fetchDataRetentionPolicy} from '@actions/remote/systems';
import {fetchMyTeams, fetchTeamsChannelsThreadsAndUnreadPosts, handleKickFromTeam, type MyTeamsRequest, updateCanJoinTeams} from '@actions/remote/team';
import {fetchMyTeams, fetchTeamsThreads, handleKickFromTeam, type MyTeamsRequest, updateCanJoinTeams} from '@actions/remote/team';
import {syncTeamThreads} from '@actions/remote/thread';
import {fetchMe, type MyUserRequest, updateAllUsersSince, autoUpdateTimezone} from '@actions/remote/user';
import {General, Preferences, Screens} from '@constants';
@ -22,16 +22,16 @@ import {selectDefaultTeam} from '@helpers/api/team';
import {DEFAULT_LOCALE} from '@i18n';
import NetworkManager from '@managers/network_manager';
import {getDeviceToken} from '@queries/app/global';
import {getChannelById, queryAllChannelsForTeam, queryChannelsById} from '@queries/servers/channel';
import {prepareModels, truncateCrtRelatedTables} from '@queries/servers/entry';
import {getChannelById} from '@queries/servers/channel';
import {prepareEntryModels, prepareEntryModelsForDeletion, truncateCrtRelatedTables} from '@queries/servers/entry';
import {getHasCRTChanged} from '@queries/servers/preference';
import {getConfig, getCurrentChannelId, getCurrentTeamId, getIsDataRetentionEnabled, getPushVerificationStatus, getLastFullSync, setCurrentTeamAndChannelId, getConfigValue} from '@queries/servers/system';
import {deleteMyTeams, getAvailableTeamIds, getTeamChannelHistory, queryMyTeams, queryMyTeamsByIds, queryTeamsById} from '@queries/servers/team';
import {getCurrentChannelId, getCurrentTeamId, getIsDataRetentionEnabled, getPushVerificationStatus, getLastFullSync, setCurrentTeamAndChannelId, getConfigValue} from '@queries/servers/system';
import {getTeamChannelHistory} from '@queries/servers/team';
import NavigationStore from '@store/navigation_store';
import {isDMorGM, sortChannelsByDisplayName} from '@utils/channel';
import {getFullErrorMessage, isErrorWithStatusCode} from '@utils/errors';
import {getFullErrorMessage} from '@utils/errors';
import {isMinimumServerVersion, isTablet} from '@utils/helpers';
import {logDebug} from '@utils/log';
import {logDebug, logError} from '@utils/log';
import {processIsCRTEnabled} from '@utils/thread';
import type {Database, Model} from '@nozbe/watermelondb';
@ -66,14 +66,16 @@ export type EntryResponse = {
error: unknown;
}
export const entry = async (serverUrl: string, teamId?: string, channelId?: string, since = 0, groupLabel?: string): Promise<EntryResponse> => {
export const entry = async (serverUrl: string, teamId?: string, channelId?: string, since = 0, groupLabel?: RequestGroupLabel): Promise<EntryResponse> => {
const {database} = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
const result = entryRest(serverUrl, teamId, channelId, since, groupLabel);
const result = await entryRest(serverUrl, teamId, channelId, since, groupLabel);
// Fetch data retention policies
const isDataRetentionEnabled = await getIsDataRetentionEnabled(database);
if (isDataRetentionEnabled) {
fetchDataRetentionPolicy(serverUrl, false, groupLabel);
if (!result.error) {
const isDataRetentionEnabled = await getIsDataRetentionEnabled(database);
if (isDataRetentionEnabled) {
fetchDataRetentionPolicy(serverUrl, false, groupLabel);
}
}
return result;
@ -83,7 +85,7 @@ export async function deferredAppEntryActions(
serverUrl: string, since: number, currentUserId: string, currentUserLocale: string, preferences: PreferenceType[] | undefined,
config: ClientConfig, license: ClientLicense | undefined, teamData: MyTeamsRequest, chData: MyChannelsRequest | undefined,
initialTeamId?: string, initialChannelId?: string,
groupLabel?: string,
groupLabel?: BaseRequestGroupLabel,
) {
const result = restDeferredAppEntryActions(
serverUrl, since, currentUserId, currentUserLocale,
@ -97,242 +99,109 @@ export async function deferredAppEntryActions(
return result;
}
const getRemoveTeamIds = async (database: Database, teamData: MyTeamsRequest) => {
const myTeams = await queryMyTeams(database).fetch();
const joinedTeams = new Set(teamData.memberships?.filter((m) => m.delete_at === 0).map((m) => m.team_id));
return myTeams.filter((m) => !joinedTeams.has(m.id)).map((m) => m.id);
};
const entryRest = async (serverUrl: string, teamId?: string, channelId?: string, since = 0, groupLabel?: RequestGroupLabel) => {
try {
const {database, operator} = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
let lastDisconnectedAt = since || await getLastFullSync(database);
const teamsToRemove = async (serverUrl: string, removeTeamIds?: string[]) => {
const operator = DatabaseManager.serverDatabases[serverUrl]?.operator;
if (!operator) {
return [];
}
const {database} = operator;
const [confResp, prefData] = await Promise.all([
fetchConfigAndLicense(serverUrl, true, groupLabel),
fetchMyPreferences(serverUrl, true, groupLabel),
]);
if (removeTeamIds?.length) {
// Immediately delete myTeams so that the UI renders only teams the user is a member of.
const removeMyTeams = await queryMyTeamsByIds(database, removeTeamIds).fetch();
if (removeMyTeams?.length) {
await deleteMyTeams(operator, removeMyTeams);
const ids = removeMyTeams.map((m) => m.id);
const removeTeams = await queryTeamsById(database, ids).fetch();
return removeTeams;
const isCRTEnabled = Boolean(prefData.preferences && processIsCRTEnabled(prefData.preferences, confResp.config?.CollapsedThreads, confResp.config?.FeatureFlagCollapsedThreads, confResp.config?.Version));
if (prefData.preferences) {
const crtToggled = await getHasCRTChanged(database, prefData.preferences);
if (crtToggled) {
const currentServerUrl = await DatabaseManager.getActiveServerUrl();
const isSameServer = currentServerUrl === serverUrl;
if (isSameServer) {
lastDisconnectedAt = 0;
}
const {error} = await truncateCrtRelatedTables(serverUrl);
if (error) {
throw new Error(`Resetting CRT on ${serverUrl} failed`);
}
}
}
}
return [];
};
// let's start fetching in parallel all we can
const promises: [Promise<MyTeamsRequest>, Promise<MyUserRequest>, Promise<MyChannelsRequest>] = [
fetchMyTeams(serverUrl, true, groupLabel),
fetchMe(serverUrl, true, groupLabel),
fetchAllMyChannelsForAllTeams(serverUrl, lastDisconnectedAt, isCRTEnabled, true, groupLabel),
];
const entryRest = async (serverUrl: string, teamId?: string, channelId?: string, since = 0, groupLabel?: string): Promise<EntryResponse> => {
const operator = DatabaseManager.serverDatabases[serverUrl]?.operator;
if (!operator) {
return {error: `${serverUrl} database not found`};
}
const {database} = operator;
const [teamData, meData, chData] = await Promise.all(promises);
const error = confResp.error || prefData.error || teamData.error || meData.error || chData.error;
if (error) {
return {error};
}
const lastDisconnectedAt = since || await getLastFullSync(database);
fetchRoles(serverUrl, teamData.memberships, chData.memberships, meData.user, false, false, groupLabel);
const fetchedData = await fetchAppEntryData(serverUrl, lastDisconnectedAt, teamId, channelId, groupLabel);
if ('error' in fetchedData) {
return {error: fetchedData.error};
}
let initialTeamId = teamId || '';
let initialChannelId = channelId || '';
let gmConverted = false;
const {initialTeamId, initialChannelId: fetchedChannelId, teamData, chData, prefData, meData, removeTeamIds, removeChannelIds, isCRTEnabled, gmConverted} = fetchedData;
const chError = chData?.error;
if (isErrorWithStatusCode(chError) && chError.status_code === 403) {
// if the user does not have appropriate permissions, which means the user those not belong to the team,
// we set it as there is no errors, so that the teams and others can be properly handled
chData!.error = undefined;
}
const error = teamData.error || chData?.error || prefData.error || meData.error;
if (error) {
if (channelId) {
const existingChannel = await getChannelById(database, channelId);
if (existingChannel && existingChannel.type === General.GM_CHANNEL) {
// Okay, so now we know the channel existsin in mobile app's database as a GM.
// We now need to also check if channel on server is actually a private channel,
// and if so, which team does it belong to now. That team will become the
// active team on mobile app after this point.
const serverChannel = chData.channels?.find((c) => c.id === channelId);
// Although yon can convert GM only to a pirvate channel, a private channel can furthur be converted to a public channel.
// So between the mobile app being on the GM and reconnecting,
// it may have become either a public or a private channel. So we need to check for both.
if (serverChannel?.type === General.PRIVATE_CHANNEL || serverChannel?.type === General.OPEN_CHANNEL) {
initialTeamId = serverChannel.team_id;
initialChannelId = channelId;
gmConverted = true;
}
}
}
if (!teamData.error && teamData.teams?.length === 0) {
initialTeamId = '';
}
const inTeam = teamData.teams?.find((t) => t.id === initialTeamId);
if (initialTeamId && !inTeam && !teamData.error) {
initialTeamId = '';
}
if (!initialTeamId && teamData.teams?.length && teamData.memberships?.length) {
// If no initial team was set in the database but got teams in the response
const teamOrderPreference = getPreferenceValue<string>(prefData.preferences || [], Preferences.CATEGORIES.TEAMS_ORDER, '', '');
const teamMembers = new Set(teamData.memberships.filter((m) => m.delete_at === 0).map((m) => m.team_id));
const myTeams = teamData.teams.filter((t) => teamMembers.has(t.id));
const defaultTeam = selectDefaultTeam(myTeams, meData.user?.locale || DEFAULT_LOCALE, teamOrderPreference, confResp.config?.ExperimentalPrimaryTeam);
if (defaultTeam?.id) {
initialTeamId = defaultTeam.id;
}
}
initialChannelId = await entryInitialChannelId(database, initialChannelId, teamId, initialTeamId, meData?.user?.locale || '', chData?.channels, chData?.memberships);
const dt = Date.now();
const modelsToDeletePromises = await prepareEntryModelsForDeletion({operator, teamData, chData});
const modelPromises = await prepareEntryModels({operator, teamData, chData, prefData, meData, isCRTEnabled});
const modelsToDelete = await Promise.all(modelsToDeletePromises);
const models = await Promise.all(modelPromises);
if (modelsToDelete.length) {
models.push(...modelsToDelete);
}
logDebug('Process models on entry', groupLabel, models.flat().length, `${Date.now() - dt}ms`);
return {models: models.flat(), initialChannelId, initialTeamId, prefData, teamData, chData, meData, gmConverted};
} catch (error) {
logError('entryRest', groupLabel, error);
return {error};
}
const rolesData = await fetchRoles(serverUrl, teamData.memberships, chData?.memberships, meData.user, true, false, groupLabel);
const initialChannelId = await entryInitialChannelId(database, fetchedChannelId, teamId, initialTeamId, meData?.user?.locale || '', chData?.channels, chData?.memberships);
const removeTeams = await teamsToRemove(serverUrl, removeTeamIds);
let removeChannels;
if (removeChannelIds?.length) {
removeChannels = await queryChannelsById(database, removeChannelIds).fetch();
}
const modelPromises = await prepareModels({operator, initialTeamId, removeTeams, removeChannels, teamData, chData, prefData, meData, isCRTEnabled});
if (rolesData.roles?.length) {
modelPromises.push(operator.handleRole({roles: rolesData.roles, prepareRecordsOnly: true}));
}
const models = await Promise.all(modelPromises);
return {models: models.flat(), initialChannelId, initialTeamId, prefData, teamData, chData, meData, gmConverted};
};
const fetchAppEntryData = async (serverUrl: string, sinceArg: number, onLoadTeamId = '', channelId?: string, groupLabel?: string): Promise<AppEntryData | AppEntryError> => {
const database = DatabaseManager.serverDatabases[serverUrl]?.database;
if (!database) {
return {error: `${serverUrl} database not found`};
}
let since = sinceArg;
const includeDeletedChannels = true;
const fetchOnly = true;
const confReq = await fetchConfigAndLicense(serverUrl, false, groupLabel);
const prefData = await fetchMyPreferences(serverUrl, fetchOnly, groupLabel);
const isCRTEnabled = Boolean(prefData.preferences && processIsCRTEnabled(prefData.preferences, confReq.config?.CollapsedThreads, confReq.config?.FeatureFlagCollapsedThreads, confReq.config?.Version));
if (prefData.preferences) {
const crtToggled = await getHasCRTChanged(database, prefData.preferences);
if (crtToggled) {
const currentServerUrl = await DatabaseManager.getActiveServerUrl();
const isSameServer = currentServerUrl === serverUrl;
if (isSameServer) {
since = 0;
}
const {error} = await truncateCrtRelatedTables(serverUrl);
if (error) {
return {error: `Resetting CRT on ${serverUrl} failed`};
}
}
}
// Fetch in parallel teams / team membership / user preferences / user
const promises: [Promise<MyTeamsRequest>, Promise<MyUserRequest>] = [
fetchMyTeams(serverUrl, fetchOnly, groupLabel),
fetchMe(serverUrl, fetchOnly, groupLabel),
];
const resolution = await Promise.all(promises);
const [teamData, meData] = resolution;
let chData;
let initialTeamId = onLoadTeamId;
let initialChannelId = channelId;
let gmConverted = false;
if (channelId) {
const existingChannel = await getChannelById(database, channelId);
if (existingChannel && existingChannel.type === General.GM_CHANNEL) {
// Okay, so now we know the channel existsin in mobile app's database as a GM.
// We now need to also check if channel on server is actually a private channel,
// and if so, which team does it belong to now. That team will become the
// active team on mobile app after this point.
const client = NetworkManager.getClient(serverUrl);
const serverChannel = await client.getChannel(channelId, groupLabel);
// Although yon can convert GM only to a pirvate channel, a private channel can furthur be converted to a public channel.
// So between the mobile app being on the GM and reconnecting,
// it may have become either a public or a private channel. So we need to check for both.
if (serverChannel.type === General.PRIVATE_CHANNEL || serverChannel.type === General.OPEN_CHANNEL) {
initialTeamId = serverChannel.team_id;
initialChannelId = channelId;
gmConverted = true;
}
}
}
if (initialTeamId) {
chData = await fetchMyChannelsForTeam(serverUrl, initialTeamId, includeDeletedChannels, since, fetchOnly, false, isCRTEnabled, groupLabel);
}
if (!initialTeamId && teamData.teams?.length && teamData.memberships?.length) {
// If no initial team was set in the database but got teams in the response
const config = await getConfig(database);
const teamOrderPreference = getPreferenceValue<string>(prefData.preferences || [], Preferences.CATEGORIES.TEAMS_ORDER, '', '');
const teamMembers = new Set(teamData.memberships.filter((m) => m.delete_at === 0).map((m) => m.team_id));
const myTeams = teamData.teams!.filter((t) => teamMembers.has(t.id));
const defaultTeam = selectDefaultTeam(myTeams, meData.user?.locale || DEFAULT_LOCALE, teamOrderPreference, config?.ExperimentalPrimaryTeam);
if (defaultTeam?.id) {
chData = await fetchMyChannelsForTeam(serverUrl, defaultTeam.id, includeDeletedChannels, since, fetchOnly, false, isCRTEnabled, groupLabel);
}
}
const removeTeamIds = await getRemoveTeamIds(database, teamData);
let data: AppEntryData = {
initialTeamId,
teamData,
chData,
prefData,
meData,
removeTeamIds,
isCRTEnabled,
initialChannelId,
gmConverted,
};
if (teamData.teams?.length === 0 && !teamData.error) {
return {
...data,
initialTeamId: '',
removeTeamIds,
};
}
const inTeam = teamData.teams?.find((t) => t.id === initialTeamId);
const chError = chData?.error;
if ((!inTeam && !teamData.error) || (isErrorWithStatusCode(chError) && chError.status_code === 403)) {
// User is no longer a member of the current team
if (!removeTeamIds.includes(initialTeamId)) {
removeTeamIds.push(initialTeamId);
}
const availableTeamIds = await getAvailableTeamIds(database, initialTeamId, teamData.teams, prefData.preferences, meData.user?.locale);
const alternateTeamData = await fetchAlternateTeamData(serverUrl, availableTeamIds, removeTeamIds, includeDeletedChannels, since, fetchOnly, isCRTEnabled, groupLabel);
data = {
...data,
...alternateTeamData,
};
}
if (data.chData?.channels) {
const removeChannelIds: string[] = [];
const fetchedChannelIds = new Set(data.chData.channels.map((channel) => channel.id));
const channels = await queryAllChannelsForTeam(database, initialTeamId).fetch();
for (const channel of channels) {
if (!fetchedChannelIds.has(channel.id)) {
removeChannelIds.push(channel.id);
}
}
data = {
...data,
removeChannelIds,
};
}
return data;
};
const fetchAlternateTeamData = async (
serverUrl: string, availableTeamIds: string[], removeTeamIds: string[],
includeDeleted = true, since = 0, fetchOnly = false,
isCRTEnabled?: boolean, groupLabel?: string,
) => {
let initialTeamId = '';
let chData;
for await (const teamId of availableTeamIds) {
chData = await fetchMyChannelsForTeam(serverUrl, teamId, includeDeleted, since, fetchOnly, false, isCRTEnabled, groupLabel);
const chError = chData.error;
if (isErrorWithStatusCode(chError) && chError.status_code === 403) {
removeTeamIds.push(teamId);
} else {
initialTeamId = teamId;
break;
}
}
if (chData) {
return {initialTeamId, chData, removeTeamIds};
}
return {initialTeamId, removeTeamIds};
};
async function entryInitialChannelId(database: Database, requestedChannelId = '', requestedTeamId = '', initialTeamId: string, locale: string, channels?: Channel[], memberships?: ChannelMember[]) {
@ -376,67 +245,69 @@ async function entryInitialChannelId(database: Database, requestedChannelId = ''
async function restDeferredAppEntryActions(
serverUrl: string, since: number, currentUserId: string, currentUserLocale: string, preferences: PreferenceType[] | undefined,
config: ClientConfig, license: ClientLicense | undefined, teamData: MyTeamsRequest, chData: MyChannelsRequest | undefined,
initialTeamId?: string, initialChannelId?: string, groupLabel?: string,
initialTeamId?: string, initialChannelId?: string, groupLabel?: BaseRequestGroupLabel,
) {
const isCRTEnabled = (preferences && processIsCRTEnabled(preferences, config.CollapsedThreads, config.FeatureFlagCollapsedThreads, config.Version)) || false;
const directChannels = chData?.channels?.filter(isDMorGM);
const channelsToFetchProfiles = new Set<Channel>(directChannels);
const requestLabel: RequestGroupLabel|undefined = groupLabel ? `${groupLabel} Deferred` : undefined;
// sidebar DM & GM profiles
if (channelsToFetchProfiles.size) {
const teammateDisplayNameSetting = getTeammateNameDisplaySetting(preferences || [], config.LockTeammateNameDisplay, config.TeammateNameDisplay, license);
fetchMissingDirectChannelsInfo(serverUrl, Array.from(channelsToFetchProfiles), currentUserLocale, teammateDisplayNameSetting, currentUserId, false, groupLabel);
fetchMissingDirectChannelsInfo(serverUrl, Array.from(channelsToFetchProfiles), currentUserLocale, teammateDisplayNameSetting, currentUserId, false, requestLabel);
}
updateAllUsersSince(serverUrl, since, false, groupLabel);
updateAllUsersSince(serverUrl, since, false, requestLabel);
updateCanJoinTeams(serverUrl);
setTimeout(async () => {
if (chData?.channels?.length && chData.memberships?.length && initialTeamId) {
if (isCRTEnabled && initialTeamId) {
await syncTeamThreads(serverUrl, initialTeamId, {groupLabel});
}
fetchPostsForUnreadChannels(serverUrl, chData.channels, chData.memberships, initialChannelId, false, groupLabel);
}
// defer fetch channels and unread posts for other teams
let mySortedTeams: Team[] = [];
if (teamData.teams?.length && teamData.memberships?.length) {
const teamsOrder = preferences?.find((p) => p.category === Preferences.CATEGORIES.TEAMS_ORDER);
const sortedTeamIds = new Set(teamsOrder?.value.split(','));
const membershipSet = new Set(teamData.memberships.map((m) => m.team_id));
const teamMap = new Map(teamData.teams.map((t) => [t.id, t]));
if (initialTeamId) {
sortedTeamIds.delete(initialTeamId);
membershipSet.delete(initialTeamId);
teamMap.delete(initialTeamId);
}
let myTeams: Team[];
if (sortedTeamIds.size) {
const mySortedTeams = [...sortedTeamIds].
const sortedTeams = [...sortedTeamIds].
filter((id) => membershipSet.has(id) && teamMap.has(id)).
map((id) => teamMap.get(id)!);
const extraTeams = [...membershipSet].
filter((id) => !sortedTeamIds.has(id) && teamMap.get(id)).
map((id) => teamMap.get(id)!).
sort((a, b) => a.display_name.toLocaleLowerCase().localeCompare(b.display_name.toLocaleLowerCase()));
myTeams = [...mySortedTeams, ...extraTeams];
mySortedTeams = [...sortedTeams, ...extraTeams];
} else {
myTeams = teamData.teams.
mySortedTeams = teamData.teams.
sort((a, b) => a.display_name.toLocaleLowerCase().localeCompare(b.display_name.toLocaleLowerCase()));
}
if (myTeams.length) {
fetchTeamsChannelsThreadsAndUnreadPosts(serverUrl, since, myTeams, isCRTEnabled, false, groupLabel + '-additional');
const myOtherSortedTeams = mySortedTeams.filter((t) => t.id !== initialTeamId);
if (initialTeamId) {
const initialTeam = teamMap.get(initialTeamId);
if (initialTeam) {
mySortedTeams = [initialTeam, ...myOtherSortedTeams];
}
}
if (chData?.channels?.length && chData.memberships?.length && initialTeamId) {
if (isCRTEnabled && initialTeamId) {
await syncTeamThreads(serverUrl, initialTeamId, {groupLabel: requestLabel});
}
fetchPostsForUnreadChannels(serverUrl, mySortedTeams, chData.channels, chData.memberships, initialChannelId, isCRTEnabled, false, requestLabel);
}
if (myOtherSortedTeams.length) {
fetchTeamsThreads(serverUrl, since, myOtherSortedTeams, isCRTEnabled, false, requestLabel);
}
}
});
// Fetch groups for current user
fetchGroupsForMember(serverUrl, currentUserId, false, groupLabel);
fetchGroupsForMember(serverUrl, currentUserId, false, requestLabel);
}
export const setExtraSessionProps = async (serverUrl: string, groupLabel?: string) => {
export const setExtraSessionProps = async (serverUrl: string, groupLabel?: RequestGroupLabel) => {
try {
const {database} = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
const serverVersion = await getConfigValue(database, 'Version');
@ -459,7 +330,7 @@ export const setExtraSessionProps = async (serverUrl: string, groupLabel?: strin
}
};
export async function verifyPushProxy(serverUrl: string, groupLabel?: string) {
export async function verifyPushProxy(serverUrl: string, groupLabel?: RequestGroupLabel) {
const deviceId = await getDeviceToken();
if (!deviceId) {
return;

View file

@ -22,6 +22,9 @@ export async function loginEntry({serverUrl}: AfterLoginArgs): Promise<{error?:
// sure we don't do this by skipping the load metric here.
PerformanceMetricsManager.skipLoadMetric();
// But still we want to log the TTI
PerformanceMetricsManager.startTimeToInteraction();
try {
const clData = await fetchConfigAndLicense(serverUrl, false);
if (clData.error) {
@ -31,7 +34,7 @@ export async function loginEntry({serverUrl}: AfterLoginArgs): Promise<{error?:
const credentials = await getServerCredentials(serverUrl);
if (credentials?.token) {
WebsocketManager.createClient(serverUrl, credentials.token);
await WebsocketManager.initializeClient(serverUrl, 'login');
await WebsocketManager.initializeClient(serverUrl, 'Login');
}
return {};

View file

@ -24,7 +24,7 @@ import type MyChannelModel from '@typings/database/models/servers/my_channel';
import type MyTeamModel from '@typings/database/models/servers/my_team';
import type PostModel from '@typings/database/models/servers/post';
export async function pushNotificationEntry(serverUrl: string, notification: NotificationData, groupLabel?: string) {
export async function pushNotificationEntry(serverUrl: string, notification: NotificationData, groupLabel?: BaseRequestGroupLabel) {
// We only reach this point if we have a channel Id in the notification payload
const channelId = notification.channel_id!;
const rootId = notification.root_id!;
@ -33,6 +33,9 @@ export async function pushNotificationEntry(serverUrl: string, notification: Not
if (!operator) {
return {error: `${serverUrl} database not found`};
}
PerformanceMetricsManager.startTimeToInteraction();
const {database} = operator;
const currentTeamId = await getCurrentTeamId(database);

View file

@ -66,7 +66,7 @@ export const fetchGroupsByNames = async (serverUrl: string, names: string[], fet
}
};
export const fetchGroupsForChannel = async (serverUrl: string, channelId: string, fetchOnly = false, groupLabel?: string) => {
export const fetchGroupsForChannel = async (serverUrl: string, channelId: string, fetchOnly = false, groupLabel?: RequestGroupLabel) => {
try {
const {operator, database} = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
const license = await getLicense(database);
@ -129,7 +129,7 @@ export const fetchGroupsForTeam = async (serverUrl: string, teamId: string, fetc
}
};
export const fetchGroupsForMember = async (serverUrl: string, userId: string, fetchOnly = false, groupLabel?: string) => {
export const fetchGroupsForMember = async (serverUrl: string, userId: string, fetchOnly = false, groupLabel?: RequestGroupLabel) => {
try {
const {operator, database} = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
const license = await getLicense(database);
@ -191,7 +191,7 @@ export const fetchGroupsForTeamIfConstrained = async (serverUrl: string, teamId:
}
};
export const fetchGroupsForChannelIfConstrained = async (serverUrl: string, channelId: string, fetchOnly = false, groupLabel?: string) => {
export const fetchGroupsForChannelIfConstrained = async (serverUrl: string, channelId: string, fetchOnly = false, groupLabel?: RequestGroupLabel) => {
try {
const {database} = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
const channel = await getChannelById(database, channelId);

View file

@ -581,7 +581,7 @@ describe('get posts', () => {
await operator.handleSystem({systems: [{id: SYSTEM_IDENTIFIERS.CURRENT_USER_ID, value: user1.id}], prepareRecordsOnly: false});
await operator.handleMyChannel({channels: [channel1], myChannels: [channelMember1], prepareRecordsOnly: false});
const result = await fetchPostsForUnreadChannels(serverUrl, [channel1, {...channel1, id: 'channelid2', total_msg_count: 10}], [{...channelMember1, msg_count: 5}, {...channelMember1, channel_id: 'channelid2', msg_count: 10}], 'testid');
const result = await fetchPostsForUnreadChannels(serverUrl, [{id: teamId}] as Team[], [channel1, {...channel1, id: 'channelid2', total_msg_count: 10}], [{...channelMember1, msg_count: 5}, {...channelMember1, channel_id: 'channelid2', msg_count: 10}], 'testid');
expect(result).toBeDefined();
expect(result?.length).toBe(1); // Only returns the response for the channel with unread messages
expect(result?.[0].posts?.[0].channel_id).toBe(channel1.id);

View file

@ -284,7 +284,7 @@ export const retryFailedPost = async (serverUrl: string, post: PostModel) => {
return {};
};
export async function fetchPostsForChannel(serverUrl: string, channelId: string, fetchOnly = false, groupLabel?: string): Promise<PostsForChannel> {
export async function fetchPostsForChannel(serverUrl: string, channelId: string, fetchOnly = false, skipAuthors = false, groupLabel?: RequestGroupLabel): Promise<PostsForChannel> {
try {
if (!fetchOnly) {
EphemeralStore.addLoadingMessagesForChannel(serverUrl, channelId);
@ -309,8 +309,10 @@ export async function fetchPostsForChannel(serverUrl: string, channelId: string,
}
let authors: UserProfile[] = [];
if (data.posts?.length && data.order?.length) {
const {authors: fetchedAuthors} = await fetchPostAuthors(serverUrl, data.posts, true, groupLabel);
authors = fetchedAuthors || [];
if (!skipAuthors) {
const {authors: fetchedAuthors} = await fetchPostAuthors(serverUrl, data.posts, true, groupLabel);
authors = fetchedAuthors || [];
}
if (!fetchOnly) {
await storePostsForChannel(
@ -333,39 +335,84 @@ export async function fetchPostsForChannel(serverUrl: string, channelId: string,
}
export const fetchPostsForUnreadChannels = async (
serverUrl: string, channels: Channel[], memberships: ChannelMembership[],
excludeChannelId?: string, fetchOnly = false, groupLabel?: string,
serverUrl: string, teams: Team[], channels: Channel[], memberships: ChannelMembership[],
excludeChannelId?: string, isCRTEnabled?: boolean, fetchOnly = false, groupLabel?: RequestGroupLabel,
): Promise<PostsForChannel[]> => {
const membersMap = new Map<string, ChannelMembership>();
for (const member of memberships) {
membersMap.set(member.channel_id, member);
const teamIndexMap = new Map<string, number>();
teams.forEach((team, index) => teamIndexMap.set(team.id, index));
const channelsMap = new Map<string, Channel>();
for (const channel of channels) {
channelsMap.set(channel.id, channel);
}
const unreadChannelIds = channels.reduce<string[]>((result, channel) => {
const member = membersMap.get(channel.id);
if (member && !channel.delete_at && (channel.total_msg_count - member.msg_count) > 0 && channel.id !== excludeChannelId) {
result.push(channel.id);
const sortedUnreadchannelIds = memberships.filter((member) => {
const channel = channelsMap.get(member.channel_id);
if (channel && !channel.delete_at && channel.id !== excludeChannelId) {
const unreads = isCRTEnabled ? (channel.total_msg_count_root ?? 0) - (member.msg_count_root ?? 0) : channel.total_msg_count - member.msg_count;
return unreads > 0; // Keep only channels with unreads
}
return result;
}, []);
return false;
}).
sort((a, b) => {
const channelA = channelsMap.get(a.channel_id);
const channelB = channelsMap.get(b.channel_id);
if (!channelA || !channelB) {
return 0; // If either channel is undefined, treat as equal
}
// Priority for channels with an empty team_id
if (channelA.team_id === '' && channelB.team_id !== '') {
return -1;
}
if (channelB.team_id === '' && channelA.team_id !== '') {
return 1;
}
// Get team indices for sorting
const teamIndexA = teamIndexMap.get(channelA.team_id) ?? Number.MAX_SAFE_INTEGER;
const teamIndexB = teamIndexMap.get(channelB.team_id) ?? Number.MAX_SAFE_INTEGER;
// Sort by team index first
if (teamIndexA !== teamIndexB) {
return teamIndexA - teamIndexB;
}
// If team index is the same, sort by last_viewed_at in descending order
return b.last_viewed_at - a.last_viewed_at;
}).
map((member) => member.channel_id);
const postsForChannel: PostsForChannel[] = [];
const posts: Post[] = [];
// process 10 unread channels at a time
const chunks = chunk(unreadChannelIds, 10);
const chunks = chunk(sortedUnreadchannelIds, 10);
for await (const channelIds of chunks) {
const promises = [];
for (const channelId of channelIds) {
promises.push(fetchPostsForChannel(serverUrl, channelId, fetchOnly, groupLabel));
promises.push(fetchPostsForChannel(serverUrl, channelId, fetchOnly, true, groupLabel));
}
const results = await Promise.all(promises);
postsForChannel.push(...results);
for (const r of results) {
if (r.posts?.length) {
posts.push(...r.posts);
}
}
}
if (posts.length) {
fetchPostAuthors(serverUrl, posts, false, groupLabel);
}
return postsForChannel;
};
export async function fetchPosts(serverUrl: string, channelId: string, page = 0, perPage = General.POST_CHUNK_SIZE, fetchOnly = false, groupLabel?: string): Promise<PostsRequest> {
export async function fetchPosts(serverUrl: string, channelId: string, page = 0, perPage = General.POST_CHUNK_SIZE, fetchOnly = false, groupLabel?: RequestGroupLabel): Promise<PostsRequest> {
try {
if (!fetchOnly) {
EphemeralStore.addLoadingMessagesForChannel(serverUrl, channelId);
@ -465,7 +512,7 @@ export async function fetchPostsBefore(serverUrl: string, channelId: string, pos
}
}
export async function fetchPostsSince(serverUrl: string, channelId: string, since: number, fetchOnly = false, groupLabel?: string): Promise<PostsRequest> {
export async function fetchPostsSince(serverUrl: string, channelId: string, since: number, fetchOnly = false, groupLabel?: RequestGroupLabel): Promise<PostsRequest> {
try {
if (!fetchOnly) {
EphemeralStore.addLoadingMessagesForChannel(serverUrl, channelId);
@ -512,7 +559,7 @@ export async function fetchPostsSince(serverUrl: string, channelId: string, sinc
}
}
export const fetchPostAuthors = async (serverUrl: string, posts: Post[], fetchOnly = false, groupLabel?: string): Promise<AuthorsRequest> => {
export const fetchPostAuthors = async (serverUrl: string, posts: Post[], fetchOnly = false, groupLabel?: RequestGroupLabel): Promise<AuthorsRequest> => {
try {
const {database, operator} = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
const client = NetworkManager.getClient(serverUrl);
@ -575,7 +622,7 @@ export const fetchPostAuthors = async (serverUrl: string, posts: Post[], fetchOn
}
};
export async function fetchPostThread(serverUrl: string, postId: string, options?: FetchPaginatedThreadOptions, fetchOnly = false, groupLabel?: string) {
export async function fetchPostThread(serverUrl: string, postId: string, options?: FetchPaginatedThreadOptions, fetchOnly = false, groupLabel?: RequestGroupLabel) {
try {
setFetchingThreadState(postId, true);
const client = NetworkManager.getClient(serverUrl);
@ -746,7 +793,7 @@ export async function fetchMissingChannelsFromPosts(serverUrl: string, posts: Po
}
}
export async function fetchPostById(serverUrl: string, postId: string, fetchOnly = false, groupLabel?: string) {
export async function fetchPostById(serverUrl: string, postId: string, fetchOnly = false, groupLabel?: RequestGroupLabel) {
try {
const client = NetworkManager.getClient(serverUrl);
const {database, operator} = DatabaseManager.getServerDatabaseAndOperator(serverUrl);

View file

@ -28,7 +28,7 @@ export type MyPreferencesRequest = {
error?: unknown;
};
export const fetchMyPreferences = async (serverUrl: string, fetchOnly = false, groupLabel?: string): Promise<MyPreferencesRequest> => {
export const fetchMyPreferences = async (serverUrl: string, fetchOnly = false, groupLabel?: RequestGroupLabel): Promise<MyPreferencesRequest> => {
try {
const client = NetworkManager.getClient(serverUrl);
const {operator} = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
@ -84,7 +84,7 @@ export const savePostPreference = async (serverUrl: string, postId: string) => {
}
};
export const savePreference = async (serverUrl: string, preferences: PreferenceType[], prepareRecordsOnly = false, groupLabel?: string) => {
export const savePreference = async (serverUrl: string, preferences: PreferenceType[], prepareRecordsOnly = false, groupLabel?: RequestGroupLabel) => {
try {
if (!preferences.length) {
return {preferences: []};
@ -141,7 +141,7 @@ export const deleteSavedPost = async (serverUrl: string, postId: string) => {
}
};
export const openChannelIfNeeded = async (serverUrl: string, channelId: string, groupLabel?: string) => {
export const openChannelIfNeeded = async (serverUrl: string, channelId: string, groupLabel?: RequestGroupLabel) => {
try {
const {database} = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
const channel = await getChannelById(database, channelId);
@ -156,7 +156,7 @@ export const openChannelIfNeeded = async (serverUrl: string, channelId: string,
}
};
export const openAllUnreadChannels = async (serverUrl: string, groupLabel?: string) => {
export const openAllUnreadChannels = async (serverUrl: string, groupLabel?: RequestGroupLabel) => {
try {
const {database} = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
const channels = await queryAllUnreadDMsAndGMsIds(database).fetch();
@ -168,7 +168,7 @@ export const openAllUnreadChannels = async (serverUrl: string, groupLabel?: stri
}
};
const openChannels = async (serverUrl: string, channels: ChannelModel[], groupLabel?: string) => {
const openChannels = async (serverUrl: string, channels: ChannelModel[], groupLabel?: RequestGroupLabel) => {
const {database} = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
const userId = await getCurrentUserId(database);

View file

@ -17,7 +17,7 @@ export type RolesRequest = {
export const fetchRolesIfNeeded = async (
serverUrl: string, updatedRoles: string[],
fetchOnly = false, force = false, groupLabel?: string,
fetchOnly = false, force = false, groupLabel?: RequestGroupLabel,
): Promise<RolesRequest> => {
if (!updatedRoles.length) {
return {roles: []};
@ -70,7 +70,7 @@ export const fetchRolesIfNeeded = async (
export const fetchRoles = async (
serverUrl: string, teamMembership?: TeamMembership[], channelMembership?: ChannelMembership[],
user?: UserProfile, fetchOnly = false, force = false, groupLabel?: string,
user?: UserProfile, fetchOnly = false, force = false, groupLabel?: RequestGroupLabel,
) => {
const rolesToFetch = new Set<string>(user?.roles.split(' ') || []);

View file

@ -22,7 +22,7 @@ export type DataRetentionPoliciesRequest = {
error?: unknown;
}
export const fetchDataRetentionPolicy = async (serverUrl: string, fetchOnly = false, groupLabel?: string): Promise<DataRetentionPoliciesRequest> => {
export const fetchDataRetentionPolicy = async (serverUrl: string, fetchOnly = false, groupLabel?: RequestGroupLabel): Promise<DataRetentionPoliciesRequest> => {
const {data: globalPolicy, error: globalPolicyError} = await fetchGlobalDataRetentionPolicy(serverUrl, groupLabel);
const {data: teamPolicies, error: teamPoliciesError} = await fetchAllGranularDataRetentionPolicies(serverUrl, undefined, undefined, undefined, groupLabel);
const {data: channelPolicies, error: channelPoliciesError} = await fetchAllGranularDataRetentionPolicies(serverUrl, true);
@ -45,7 +45,7 @@ export const fetchDataRetentionPolicy = async (serverUrl: string, fetchOnly = fa
return data;
};
export const fetchGlobalDataRetentionPolicy = async (serverUrl: string, groupLabel?: string): Promise<{data?: GlobalDataRetentionPolicy; error?: unknown}> => {
export const fetchGlobalDataRetentionPolicy = async (serverUrl: string, groupLabel?: RequestGroupLabel): Promise<{data?: GlobalDataRetentionPolicy; error?: unknown}> => {
try {
const client = NetworkManager.getClient(serverUrl);
const data = await client.getGlobalDataRetentionPolicy(groupLabel);
@ -62,7 +62,7 @@ export const fetchAllGranularDataRetentionPolicies = async (
isChannel = false,
page = 0,
policies: Array<TeamDataRetentionPolicy | ChannelDataRetentionPolicy> = [],
groupLabel?: string,
groupLabel?: RequestGroupLabel,
): Promise<{data?: Array<TeamDataRetentionPolicy | ChannelDataRetentionPolicy>; error?: unknown}> => {
try {
const client = NetworkManager.getClient(serverUrl);
@ -86,7 +86,7 @@ export const fetchAllGranularDataRetentionPolicies = async (
}
};
export const fetchConfigAndLicense = async (serverUrl: string, fetchOnly = false, groupLabel?: string): Promise<ConfigAndLicenseRequest> => {
export const fetchConfigAndLicense = async (serverUrl: string, fetchOnly = false, groupLabel?: RequestGroupLabel): Promise<ConfigAndLicenseRequest> => {
try {
const client = NetworkManager.getClient(serverUrl);
const [config, license]: [ClientConfig, ClientLicense] = await Promise.all([

View file

@ -25,7 +25,7 @@ import {
handleKickFromTeam,
getTeamMembersByIds,
buildTeamIconUrl,
fetchTeamsChannelsThreadsAndUnreadPosts,
fetchTeamsThreads,
} from './team';
import type ServerDataOperator from '@database/operator/server_data_operator';
@ -271,20 +271,20 @@ describe('teams', () => {
expect(result).toBeDefined();
});
it('fetchTeamsChannelsThreadsAndUnreadPosts - handle not found database', async () => {
const result = await fetchTeamsChannelsThreadsAndUnreadPosts('foo', 0, []) as {error: unknown};
it('fetchTeamsThreads - handle not found database', async () => {
const result = await fetchTeamsThreads('foo', 0, []) as {error: unknown};
expect(result?.error).toBeDefined();
});
it('fetchTeamsChannelsThreadsAndUnreadPosts - base case', async () => {
const result = await fetchTeamsChannelsThreadsAndUnreadPosts(
it('fetchTeamsThreads - base case', async () => {
const result = await fetchTeamsThreads(
serverUrl, 0,
[team], true, false);
expect(result).toBeDefined();
});
it('fetchTeamsChannelsThreadsAndUnreadPosts - fetch only case', async () => {
const result = await fetchTeamsChannelsThreadsAndUnreadPosts(
it('fetchTeamsThreads - fetch only case', async () => {
const result = await fetchTeamsThreads(
serverUrl, 0,
[team], true, true);
expect(result.models).toBeDefined();

View file

@ -4,9 +4,6 @@
import {chunk} from 'lodash';
import {DeviceEventEmitter} from 'react-native';
import {storeCategories} from '@actions/local/category';
import {storeMyChannelsForTeam} from '@actions/local/channel';
import {storePostsForChannel} from '@actions/local/post';
import {removeUserFromTeam as localRemoveUserFromTeam} from '@actions/local/team';
import {PER_PAGE_DEFAULT} from '@client/rest/constants';
import {Events} from '@constants';
@ -27,7 +24,7 @@ import {logDebug} from '@utils/log';
import {fetchMyChannelsForTeam, switchToChannelById} from './channel';
import {fetchGroupsForTeamIfConstrained} from './groups';
import {fetchPostsForChannel, fetchPostsForUnreadChannels, type PostsForChannel} from './post';
import {fetchPostsForChannel} from './post';
import {fetchRolesIfNeeded} from './role';
import {forceLogoutIfNecessary} from './session';
import {syncThreadsIfNeeded} from './thread';
@ -161,7 +158,7 @@ export async function sendEmailInvitesToTeam(serverUrl: string, teamId: string,
}
}
export async function fetchMyTeams(serverUrl: string, fetchOnly = false, groupLabel?: string): Promise<MyTeamsRequest> {
export async function fetchMyTeams(serverUrl: string, fetchOnly = false, groupLabel?: RequestGroupLabel): Promise<MyTeamsRequest> {
try {
const client = NetworkManager.getClient(serverUrl);
const {database, operator} = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
@ -207,7 +204,7 @@ export async function fetchMyTeams(serverUrl: string, fetchOnly = false, groupLa
}
}
export async function fetchMyTeam(serverUrl: string, teamId: string, fetchOnly = false, groupLabel?: string): Promise<MyTeamsRequest> {
export async function fetchMyTeam(serverUrl: string, teamId: string, fetchOnly = false, groupLabel?: RequestGroupLabel): Promise<MyTeamsRequest> {
try {
const client = NetworkManager.getClient(serverUrl);
const {operator} = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
@ -247,7 +244,7 @@ export const fetchAllTeams = async (serverUrl: string, page = 0, perPage = PER_P
}
};
const recCanJoinTeams = async (client: Client, myTeamsIds: Set<string>, page: number, groupLabel?: string): Promise<boolean> => {
const recCanJoinTeams = async (client: Client, myTeamsIds: Set<string>, page: number, groupLabel?: RequestGroupLabel): Promise<boolean> => {
const fetchedTeams = await client.getTeams(page, PER_PAGE_DEFAULT, false, groupLabel);
if (fetchedTeams.find((t) => !myTeamsIds.has(t.id) && t.delete_at === 0)) {
return true;
@ -298,7 +295,7 @@ export async function fetchTeamsForComponent(
return {teams: alreadyLoaded, hasMore: false, page};
}
export const updateCanJoinTeams = async (serverUrl: string, groupLabel?: string) => {
export const updateCanJoinTeams = async (serverUrl: string, groupLabel?: RequestGroupLabel) => {
try {
const client = NetworkManager.getClient(serverUrl);
const {database} = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
@ -318,9 +315,8 @@ export const updateCanJoinTeams = async (serverUrl: string, groupLabel?: string)
}
};
export const fetchTeamsChannelsThreadsAndUnreadPosts = async (
serverUrl: string, since: number, teams: Team[],
isCRTEnabled?: boolean, fetchOnly = false, groupLabel?: string,
export const fetchTeamsThreads = async (
serverUrl: string, since: number, teams: Team[], isCRTEnabled?: boolean, fetchOnly = false, groupLabel?: RequestGroupLabel,
) => {
try {
const {operator} = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
@ -329,62 +325,14 @@ export const fetchTeamsChannelsThreadsAndUnreadPosts = async (
// process up to 15 teams at a time
const chunks = chunk(teams, 15);
for await (const myTeams of chunks) {
const promises = [];
for (const team of myTeams) {
promises.push(fetchMyChannelsForTeam(serverUrl, team.id, true, since, true, true, isCRTEnabled, groupLabel));
}
const results = await Promise.all(promises);
const models: Model[] = [];
const channels: Channel[] = [];
const members: ChannelMembership[] = [];
for await (const r of results) {
if (!r.error && r.teamId && r.categories && r.channels && r.memberships) {
const {models: chModels} = await storeMyChannelsForTeam(serverUrl, r.teamId, r.channels, r.memberships, true, isCRTEnabled);
const {models: catModels} = await storeCategories(serverUrl, r.categories, true, true); // Re-sync
if (chModels?.length) {
models.push(...chModels);
}
if (catModels?.length) {
models.push(...catModels);
}
channels.push(...r.channels);
members.push(...r.memberships);
}
}
const unreadPromise = fetchPostsForUnreadChannels(serverUrl, channels, members, undefined, true, groupLabel);
const threadsPromise = syncThreadsIfNeeded(serverUrl, isCRTEnabled ?? false, myTeams, true, groupLabel);
const postPromises: [Promise<PostsForChannel[]>, Promise<{models?: Model[]; error?: unknown}>] = [
unreadPromise,
threadsPromise,
];
const [unreadPosts, threads] = await Promise.all(postPromises);
for await (const data of unreadPosts) {
if (data.channelId && data.posts?.length && data.order?.length && data.actionType) {
const {models: prepared} = await storePostsForChannel(
serverUrl, data.channelId,
data.posts, data.order, data.previousPostId ?? '',
data.actionType, data.authors || [], true,
);
if (prepared?.length) {
models.push(...prepared);
}
}
}
const threads = await syncThreadsIfNeeded(serverUrl, isCRTEnabled ?? false, myTeams, true, groupLabel);
if (threads.models) {
models.push(...threads.models);
}
if (!fetchOnly && models.length) {
await operator.batchRecords(removeDuplicatesModels(models), 'fetchTeamsChannelsThreadsAndUnreadPosts');
await operator.batchRecords(removeDuplicatesModels(models), 'fetchTeamsThreads');
}
if (models.length) {
@ -394,7 +342,7 @@ export const fetchTeamsChannelsThreadsAndUnreadPosts = async (
return {error: undefined, models: result};
} catch (error) {
logDebug('error on fetchTeamsChannelsThreadsAndUnreadPosts', getFullErrorMessage(error));
logDebug('error on fetchTeamsThreads', getFullErrorMessage(error));
return {error};
}
};

View file

@ -39,7 +39,7 @@ enum Direction {
Down,
}
export const fetchAndSwitchToThread = async (serverUrl: string, rootId: string, isFromNotification = false, groupLabel?: string) => {
export const fetchAndSwitchToThread = async (serverUrl: string, rootId: string, isFromNotification = false, groupLabel?: RequestGroupLabel) => {
const database = DatabaseManager.serverDatabases[serverUrl]?.database;
if (!database) {
return {error: `${serverUrl} database not found`};
@ -214,7 +214,7 @@ export const fetchThreads = async (
options: FetchThreadsOptions,
direction?: Direction,
pages?: number,
groupLabel?: string,
groupLabel?: RequestGroupLabel,
) => {
const operator = DatabaseManager.serverDatabases[serverUrl]?.operator;
if (!operator) {
@ -287,7 +287,7 @@ export const fetchThreads = async (
export const syncThreadsIfNeeded = async (
serverUrl: string, isCRTEnabled: boolean, teams?: Team[],
fetchOnly = false, groupLabel?: string,
fetchOnly = false, groupLabel?: RequestGroupLabel,
) => {
try {
if (!isCRTEnabled) {
@ -329,7 +329,7 @@ type SyncThreadOptions = {
excludeDirect?: boolean;
fetchOnly?: boolean;
refresh?: boolean;
groupLabel?: string;
groupLabel?: RequestGroupLabel;
}
export const syncTeamThreads = async (

View file

@ -43,7 +43,7 @@ export type ProfilesInChannelRequest = {
error?: unknown;
}
export const fetchMe = async (serverUrl: string, fetchOnly = false, groupLabel?: string): Promise<MyUserRequest> => {
export const fetchMe = async (serverUrl: string, fetchOnly = false, groupLabel?: RequestGroupLabel): Promise<MyUserRequest> => {
try {
const client = NetworkManager.getClient(serverUrl);
const {operator} = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
@ -98,7 +98,7 @@ export const refetchCurrentUser = async (serverUrl: string, currentUserId: strin
export async function fetchProfilesInChannel(
serverUrl: string, channelId: string, excludeUserId?: string, options?: GetUsersOptions,
fetchOnly = false, groupLabel?: string,
fetchOnly = false, groupLabel?: RequestGroupLabel,
): Promise<ProfilesInChannelRequest> {
try {
const client = NetworkManager.getClient(serverUrl);
@ -134,7 +134,7 @@ export async function fetchProfilesInChannel(
}
}
export async function fetchProfilesInGroupChannels(serverUrl: string, groupChannelIds: string[], fetchOnly = false, groupLabel?: string): Promise<ProfilesPerChannelRequest> {
export async function fetchProfilesInGroupChannels(serverUrl: string, groupChannelIds: string[], fetchOnly = false, groupLabel?: RequestGroupLabel): Promise<ProfilesPerChannelRequest> {
try {
const client = NetworkManager.getClient(serverUrl);
const {database, operator} = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
@ -199,7 +199,7 @@ export async function fetchProfilesInGroupChannels(serverUrl: string, groupChann
export async function fetchProfilesPerChannels(
serverUrl: string, channelIds: string[], excludeUserId?: string,
fetchOnly = false, groupLabel?: string,
fetchOnly = false, groupLabel?: RequestGroupLabel,
): Promise<ProfilesPerChannelRequest> {
try {
const {operator} = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
@ -250,7 +250,7 @@ export async function fetchProfilesPerChannels(
}
}
export const updateMe = async (serverUrl: string, user: Partial<UserProfile>, groupLabel?: string) => {
export const updateMe = async (serverUrl: string, user: Partial<UserProfile>, groupLabel?: RequestGroupLabel) => {
try {
const client = NetworkManager.getClient(serverUrl);
const {operator} = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
@ -428,7 +428,7 @@ export const fetchUserByIdBatched = async (serverUrl: string, userId: string) =>
usersByIdBatch.timeout = setTimeout(processBatch, TIME_TO_BATCH);
};
export const fetchUsersByIds = async (serverUrl: string, userIds: string[], fetchOnly = false, groupLabel?: string) => {
export const fetchUsersByIds = async (serverUrl: string, userIds: string[], fetchOnly = false, groupLabel?: RequestGroupLabel) => {
if (!userIds.length) {
return {users: [], existingUsers: []};
}
@ -632,7 +632,7 @@ export const fetchMissingProfilesByUsernames = async (serverUrl: string, usernam
return {users};
};
export async function updateAllUsersSince(serverUrl: string, since: number, fetchOnly = false, groupLabel?: string) {
export async function updateAllUsersSince(serverUrl: string, since: number, fetchOnly = false, groupLabel?: RequestGroupLabel) {
if (!since) {
return {users: []};
}
@ -807,7 +807,7 @@ export const buildProfileImageUrlFromUser = (serverUrl: string, user: UserModel
return buildProfileImageUrl(serverUrl, user.id, lastPictureUpdate);
};
export const autoUpdateTimezone = async (serverUrl: string, groupLabel?: string) => {
export const autoUpdateTimezone = async (serverUrl: string, groupLabel?: RequestGroupLabel) => {
let database;
try {
const result = DatabaseManager.getServerDatabaseAndOperator(serverUrl);

View file

@ -36,17 +36,17 @@ import {setTeamLoading} from '@store/team_load_store';
import {isTablet} from '@utils/helpers';
import {logDebug, logInfo} from '@utils/log';
export async function handleFirstConnect(serverUrl: string, groupLabel?: string) {
export async function handleFirstConnect(serverUrl: string, groupLabel?: BaseRequestGroupLabel) {
setExtraSessionProps(serverUrl, groupLabel);
autoUpdateTimezone(serverUrl, groupLabel);
return doReconnect(serverUrl, groupLabel);
}
export async function handleReconnect(serverUrl: string) {
return doReconnect(serverUrl, 'reconnection');
export async function handleReconnect(serverUrl: string, groupLabel: BaseRequestGroupLabel = 'WebSocket Reconnect') {
return doReconnect(serverUrl, groupLabel);
}
async function doReconnect(serverUrl: string, groupLabel?: string) {
async function doReconnect(serverUrl: string, groupLabel?: BaseRequestGroupLabel) {
const operator = DatabaseManager.serverDatabases[serverUrl]?.operator;
if (!operator) {
return new Error('cannot find server database');
@ -82,13 +82,6 @@ async function doReconnect(serverUrl: string, groupLabel?: string) {
await setLastFullSync(operator, now);
const tabletDevice = isTablet();
const isActiveServer = (await getActiveServerUrl()) === serverUrl;
if (isActiveServer && tabletDevice && initialChannelId === currentChannelId) {
await markChannelAsRead(serverUrl, initialChannelId, false, groupLabel);
markChannelAsViewed(serverUrl, initialChannelId);
}
logInfo('WEBSOCKET RECONNECT MODELS BATCHING TOOK', `${Date.now() - dt}ms`);
setTeamLoading(serverUrl, false);
@ -112,7 +105,7 @@ async function doReconnect(serverUrl: string, groupLabel?: string) {
return undefined;
}
async function fetchPostDataIfNeeded(serverUrl: string, groupLabel?: string) {
async function fetchPostDataIfNeeded(serverUrl: string, groupLabel?: RequestGroupLabel) {
try {
const isActiveServer = (await getActiveServerUrl()) === serverUrl;
if (!isActiveServer) {
@ -146,7 +139,7 @@ async function fetchPostDataIfNeeded(serverUrl: string, groupLabel?: string) {
}
if (currentChannelId && (isChannelScreenMounted || tabletDevice)) {
await fetchPostsForChannel(serverUrl, currentChannelId, false, groupLabel);
await fetchPostsForChannel(serverUrl, currentChannelId, false, false, groupLabel);
markChannelAsRead(serverUrl, currentChannelId, false, groupLabel);
if (!EphemeralStore.wasNotificationTapped()) {
markChannelAsViewed(serverUrl, currentChannelId, true);

View file

@ -135,7 +135,7 @@ describe('WebSocket Post Actions', () => {
mockedGetMyChannel.mockResolvedValueOnce(undefined);
mockedFetchMyChannel.mockResolvedValue({teamId: 'team1', memberships: [{user_id: 'user1', channel_id: 'channel1'}]} as MyChannelsRequest);
mockedStoreMyChannelsForTeam.mockResolvedValue({models: [myChannelModel]});
mockedStoreMyChannelsForTeam.mockResolvedValue({models: [myChannelModel], error: undefined});
mockedGetMyChannel.mockResolvedValueOnce(myChannelModel);
mockedGetIsCRTEnabled.mockResolvedValue(false);
mockedShouldIgnorePost.mockReturnValue(true);

View file

@ -7,7 +7,7 @@ import type ClientBase from './base';
export interface ClientAppsMix {
executeAppCall: <Res = unknown>(call: AppCallRequest, trackAsSubmit: boolean) => Promise<AppCallResponse<Res>>;
getAppsBindings: (userID: string, channelID: string, teamID: string, groupLabel?: string) => Promise<AppBinding[]>;
getAppsBindings: (userID: string, channelID: string, teamID: string, groupLabel?: RequestGroupLabel) => Promise<AppBinding[]>;
}
const ClientApps = <TBase extends Constructor<ClientBase>>(superclass: TBase) => class extends superclass {
@ -27,7 +27,7 @@ const ClientApps = <TBase extends Constructor<ClientBase>>(superclass: TBase) =>
);
};
getAppsBindings = async (userID: string, channelID: string, teamID: string, groupLabel?: string) => {
getAppsBindings = async (userID: string, channelID: string, teamID: string, groupLabel?: RequestGroupLabel) => {
const params = {
user_id: userID,
channel_id: channelID,

View file

@ -4,14 +4,14 @@
import type ClientBase from './base';
export interface ClientCategoriesMix {
getCategories: (userId: string, teamId: string, groupLabel?: string) => Promise<CategoriesWithOrder>;
getCategories: (userId: string, teamId: string, groupLabel?: RequestGroupLabel) => Promise<CategoriesWithOrder>;
getCategoriesOrder: (userId: string, teamId: string) => Promise<string[]>;
getCategory: (userId: string, teamId: string, categoryId: string) => Promise<Category>;
updateChannelCategories: (userId: string, teamId: string, categories: CategoryWithChannels[]) => Promise<CategoriesWithOrder>;
}
const ClientCategories = <TBase extends Constructor<ClientBase>>(superclass: TBase) => class extends superclass {
getCategories = async (userId: string, teamId: string, groupLabel?: string) => {
getCategories = async (userId: string, teamId: string, groupLabel?: RequestGroupLabel) => {
return this.doFetch(
`${this.getCategoriesRoute(userId, teamId)}`,
{method: 'get', groupLabel},

View file

@ -81,7 +81,7 @@ describe('ClientChannelBookmarks', () => {
test('getChannelBookmarksForChannel', async () => {
const channelId = 'channel_id';
const since = 123456;
const groupLabel = 'group_label';
const groupLabel = 'Login';
const expectedUrl = `${client.getChannelBookmarksRoute(channelId)}${buildQueryString({bookmarks_since: since})}`;
const expectedOptions = {method: 'get', groupLabel};

View file

@ -10,7 +10,7 @@ export interface ClientChannelBookmarksMix {
updateChannelBookmark(channelId: string, bookmark: ChannelBookmark, connectionId?: string): Promise<UpdateChannelBookmarkResponse>;
updateChannelBookmarkSortOrder(channelId: string, bookmarkId: string, newSortOrder: number, connectionId?: string): Promise<ChannelBookmarkWithFileInfo[]>;
deleteChannelBookmark(channelId: string, bookmarkId: string, connectionId?: string): Promise<ChannelBookmarkWithFileInfo>;
getChannelBookmarksForChannel(channelId: string, since: number, groupLabel?: string): Promise<ChannelBookmarkWithFileInfo[]>;
getChannelBookmarksForChannel(channelId: string, since: number, groupLabel?: RequestGroupLabel): Promise<ChannelBookmarkWithFileInfo[]>;
}
const ClientChannelBookmarks = <TBase extends Constructor<ClientBase>>(superclass: TBase) => class extends superclass {
@ -57,7 +57,7 @@ const ClientChannelBookmarks = <TBase extends Constructor<ClientBase>>(superclas
);
}
getChannelBookmarksForChannel(channelId: string, since: number, groupLabel?: string) {
getChannelBookmarksForChannel(channelId: string, since: number, groupLabel?: RequestGroupLabel) {
return this.doFetch(
`${this.getChannelBookmarksRoute(channelId)}${buildQueryString({bookmarks_since: since})}`,
{method: 'get', groupLabel},

View file

@ -19,24 +19,24 @@ export interface ClientChannelsMix {
updateChannelPrivacy: (channelId: string, privacy: any) => Promise<Channel>;
patchChannel: (channelId: string, channelPatch: Partial<Channel>) => Promise<Channel>;
updateChannelNotifyProps: (props: ChannelNotifyProps & {channel_id: string; user_id: string}) => Promise<any>;
getChannel: (channelId: string, groupLabel?: string) => Promise<Channel>;
getChannel: (channelId: string, groupLabel?: RequestGroupLabel) => Promise<Channel>;
getChannelByName: (teamId: string, channelName: string, includeDeleted?: boolean) => Promise<Channel>;
getChannelByNameAndTeamName: (teamName: string, channelName: string, includeDeleted?: boolean) => Promise<Channel>;
getChannels: (teamId: string, page?: number, perPage?: number) => Promise<Channel[]>;
getArchivedChannels: (teamId: string, page?: number, perPage?: number) => Promise<Channel[]>;
getSharedChannels: (teamId: string, page?: number, perPage?: number) => Promise<Channel[]>;
getMyChannels: (teamId: string, includeDeleted?: boolean, lastDeleteAt?: number, groupLabel?: string) => Promise<Channel[]>;
getMyChannels: (teamId: string, includeDeleted?: boolean, lastDeleteAt?: number, groupLabel?: RequestGroupLabel) => Promise<Channel[]>;
getMyChannelMember: (channelId: string) => Promise<ChannelMembership>;
getMyChannelMembers: (teamId: string, groupLabel?: string) => Promise<ChannelMembership[]>;
getMyChannelMembers: (teamId: string, groupLabel?: RequestGroupLabel) => Promise<ChannelMembership[]>;
getChannelMembers: (channelId: string, page?: number, perPage?: number) => Promise<ChannelMembership[]>;
getChannelTimezones: (channelId: string) => Promise<string[]>;
getChannelMember: (channelId: string, userId: string, groupLabel?: string) => Promise<ChannelMembership>;
getChannelMember: (channelId: string, userId: string, groupLabel?: RequestGroupLabel) => Promise<ChannelMembership>;
getChannelMembersByIds: (channelId: string, userIds: string[]) => Promise<ChannelMembership[]>;
addToChannel: (userId: string, channelId: string, postRootId?: string) => Promise<ChannelMembership>;
removeFromChannel: (userId: string, channelId: string) => Promise<any>;
getChannelStats: (channelId: string, groupLabel?: string) => Promise<ChannelStats>;
getChannelStats: (channelId: string, groupLabel?: RequestGroupLabel) => Promise<ChannelStats>;
getChannelMemberCountsByGroup: (channelId: string, includeTimezones: boolean) => Promise<ChannelMemberCountByGroup[]>;
viewMyChannel: (channelId: string, prevChannelId?: string, groupLabel?: string) => Promise<any>;
viewMyChannel: (channelId: string, prevChannelId?: string, groupLabel?: RequestGroupLabel) => Promise<any>;
autocompleteChannels: (teamId: string, name: string) => Promise<Channel[]>;
autocompleteChannelsForSearch: (teamId: string, name: string) => Promise<Channel[]>;
searchChannels: (teamId: string, term: string) => Promise<Channel[]>;
@ -46,6 +46,8 @@ export interface ClientChannelsMix {
getMemberInChannel: (channelId: string, userId: string) => Promise<ChannelMembership>;
getGroupMessageMembersCommonTeams: (channelId: string) => Promise<Team[]>;
convertGroupMessageToPrivateChannel: (channelId: string, teamId: string, displayName: string, name: string) => Promise<Channel>;
getAllChannelsFromAllTeams: (lastDeleteAt: number, includeDeleted: boolean, groupLabel?: RequestGroupLabel) => Promise<Channel[]>;
getAllMyChannelMembersFromAllTeams: (page: number, perPage: number, groupLabel?: RequestGroupLabel) => Promise<ChannelMembership[]>;
}
const ClientChannels = <TBase extends Constructor<ClientBase>>(superclass: TBase) => class extends superclass {
@ -63,6 +65,30 @@ const ClientChannels = <TBase extends Constructor<ClientBase>>(superclass: TBase
);
};
getAllChannelsFromAllTeams = async (lastDeleteAt: number, includeDeleted: boolean, groupLabel?: RequestGroupLabel) => {
const queryData = {
last_delete_at: lastDeleteAt,
include_deleted: includeDeleted,
};
return this.doFetch(
`${this.getUserRoute('me')}/channels${buildQueryString(queryData)}`,
{method: 'get', groupLabel},
);
};
getAllMyChannelMembersFromAllTeams = async (page: number, perPage: number, groupLabel?: RequestGroupLabel) => {
const queryData = {
page,
per_page: perPage,
};
return this.doFetch(
`${this.getUserRoute('me')}/channel_members${buildQueryString(queryData)}`,
{method: 'get', groupLabel},
);
};
createChannel = async (channel: Channel) => {
return this.doFetch(
`${this.getChannelsRoute()}`,
@ -130,7 +156,7 @@ const ClientChannels = <TBase extends Constructor<ClientBase>>(superclass: TBase
);
};
getChannel = async (channelId: string, groupLabel?: string) => {
getChannel = async (channelId: string, groupLabel?: RequestGroupLabel) => {
return this.doFetch(
this.getChannelRoute(channelId),
{method: 'get', groupLabel},
@ -180,7 +206,7 @@ const ClientChannels = <TBase extends Constructor<ClientBase>>(superclass: TBase
);
};
getMyChannels = async (teamId: string, includeDeleted = false, since = 0, groupLabel?: string) => {
getMyChannels = async (teamId: string, includeDeleted = false, since = 0, groupLabel?: RequestGroupLabel) => {
return this.doFetch(
`${this.getUserRoute('me')}/teams/${teamId}/channels${buildQueryString({
include_deleted: includeDeleted,
@ -197,7 +223,7 @@ const ClientChannels = <TBase extends Constructor<ClientBase>>(superclass: TBase
);
};
getMyChannelMembers = async (teamId: string, groupLabel?: string) => {
getMyChannelMembers = async (teamId: string, groupLabel?: RequestGroupLabel) => {
return this.doFetch(
`${this.getUserRoute('me')}/teams/${teamId}/channels/members`,
{method: 'get', groupLabel},
@ -218,7 +244,7 @@ const ClientChannels = <TBase extends Constructor<ClientBase>>(superclass: TBase
);
};
getChannelMember = async (channelId: string, userId: string, groupLabel?: string) => {
getChannelMember = async (channelId: string, userId: string, groupLabel?: RequestGroupLabel) => {
return this.doFetch(
`${this.getChannelMemberRoute(channelId, userId)}`,
{method: 'get', groupLabel},
@ -247,7 +273,7 @@ const ClientChannels = <TBase extends Constructor<ClientBase>>(superclass: TBase
);
};
getChannelStats = async (channelId: string, groupLabel?: string) => {
getChannelStats = async (channelId: string, groupLabel?: RequestGroupLabel) => {
return this.doFetch(
`${this.getChannelRoute(channelId)}/stats`,
{method: 'get', groupLabel},
@ -261,7 +287,7 @@ const ClientChannels = <TBase extends Constructor<ClientBase>>(superclass: TBase
);
};
viewMyChannel = async (channelId: string, prevChannelId?: string, groupLabel?: string) => {
viewMyChannel = async (channelId: string, prevChannelId?: string, groupLabel?: RequestGroupLabel) => {
// collapsed_threads_supported is not based on user preferences but to know if "CLIENT" supports CRT
const data = {channel_id: channelId, prev_channel_id: prevChannelId, collapsed_threads_supported: true};
return this.doFetch(

View file

@ -21,7 +21,7 @@ describe('ClientGeneral', () => {
test('ping', async () => {
const deviceId = 'device1';
const timeoutInterval = 1000;
const groupLabel = 'group1';
const groupLabel = 'DeepLink';
const expectedUrl = `${client.urlVersion}/system/ping?time=${Date.now()}&device_id=${deviceId}`;
const expectedOptions = {method: 'get', timeoutInterval, groupLabel};
@ -59,7 +59,7 @@ describe('ClientGeneral', () => {
});
test('getClientConfigOld', async () => {
const groupLabel = 'group1';
const groupLabel = 'Notification';
const expectedUrl = `${client.urlVersion}/config/client?format=old`;
const expectedOptions = {method: 'get', groupLabel};
@ -69,7 +69,7 @@ describe('ClientGeneral', () => {
});
test('getClientLicenseOld', async () => {
const groupLabel = 'group1';
const groupLabel = 'Notification';
const expectedUrl = `${client.urlVersion}/license/client?format=old`;
const expectedOptions = {method: 'get', groupLabel};
@ -88,7 +88,7 @@ describe('ClientGeneral', () => {
});
test('getGlobalDataRetentionPolicy', async () => {
const groupLabel = 'group1';
const groupLabel = 'Notification';
const expectedUrl = `${client.getGlobalDataRetentionRoute()}/policy`;
const expectedOptions = {method: 'get', groupLabel};
@ -101,7 +101,7 @@ describe('ClientGeneral', () => {
const userId = 'user1';
const page = 1;
const perPage = 10;
const groupLabel = 'group1';
const groupLabel = 'Notification';
const expectedUrl = `${client.getGranularDataRetentionRoute(userId)}/team_policies${buildQueryString({page, per_page: perPage})}`;
const expectedOptions = {method: 'get', groupLabel};
@ -118,7 +118,7 @@ describe('ClientGeneral', () => {
const userId = 'user1';
const page = 1;
const perPage = 10;
const groupLabel = 'group1';
const groupLabel = 'Notification';
const expectedUrl = `${client.getGranularDataRetentionRoute(userId)}/channel_policies${buildQueryString({page, per_page: perPage})}`;
const expectedOptions = {method: 'get', groupLabel};
@ -133,7 +133,7 @@ describe('ClientGeneral', () => {
test('getRolesByNames', async () => {
const rolesNames = ['role1', 'role2'];
const groupLabel = 'group1';
const groupLabel = 'Notification';
const expectedUrl = `${client.getRolesRoute()}/names`;
const expectedOptions = {method: 'post', body: rolesNames, groupLabel};

View file

@ -14,21 +14,21 @@ type PoliciesResponse<T> = {
}
export interface ClientGeneralMix {
ping: (deviceId?: string, timeoutInterval?: number, groupLabel?: string) => Promise<any>;
ping: (deviceId?: string, timeoutInterval?: number, groupLabel?: RequestGroupLabel) => Promise<any>;
logClientError: (message: string, level?: string) => Promise<any>;
getClientConfigOld: (groupLabel?: string) => Promise<ClientConfig>;
getClientLicenseOld: (groupLabel?: string) => Promise<ClientLicense>;
getClientConfigOld: (groupLabel?: RequestGroupLabel) => Promise<ClientConfig>;
getClientLicenseOld: (groupLabel?: RequestGroupLabel) => Promise<ClientLicense>;
getTimezones: () => Promise<string[]>;
getGlobalDataRetentionPolicy: (groupLabel?: string) => Promise<GlobalDataRetentionPolicy>;
getTeamDataRetentionPolicies: (userId: string, page?: number, perPage?: number, groupLabel?: string) => Promise<PoliciesResponse<TeamDataRetentionPolicy>>;
getChannelDataRetentionPolicies: (userId: string, page?: number, perPage?: number, groupLabel?: string) => Promise<PoliciesResponse<ChannelDataRetentionPolicy>>;
getRolesByNames: (rolesNames: string[], groupLabel?: string) => Promise<Role[]>;
getGlobalDataRetentionPolicy: (groupLabel?: RequestGroupLabel) => Promise<GlobalDataRetentionPolicy>;
getTeamDataRetentionPolicies: (userId: string, page?: number, perPage?: number, groupLabel?: RequestGroupLabel) => Promise<PoliciesResponse<TeamDataRetentionPolicy>>;
getChannelDataRetentionPolicies: (userId: string, page?: number, perPage?: number, groupLabel?: RequestGroupLabel) => Promise<PoliciesResponse<ChannelDataRetentionPolicy>>;
getRolesByNames: (rolesNames: string[], groupLabel?: RequestGroupLabel) => Promise<Role[]>;
getRedirectLocation: (urlParam: string) => Promise<Record<string, string>>;
sendPerformanceReport: (batch: PerformanceReport) => Promise<{}>;
}
const ClientGeneral = <TBase extends Constructor<ClientBase>>(superclass: TBase) => class extends superclass {
ping = async (deviceId?: string, timeoutInterval?: number, groupLabel?: string) => {
ping = async (deviceId?: string, timeoutInterval?: number, groupLabel?: RequestGroupLabel) => {
let url = `${this.urlVersion}/system/ping?time=${Date.now()}`;
if (deviceId) {
url = `${url}&device_id=${deviceId}`;
@ -56,14 +56,14 @@ const ClientGeneral = <TBase extends Constructor<ClientBase>>(superclass: TBase)
);
};
getClientConfigOld = async (groupLabel?: string) => {
getClientConfigOld = async (groupLabel?: RequestGroupLabel) => {
return this.doFetch(
`${this.urlVersion}/config/client?format=old`,
{method: 'get', groupLabel},
);
};
getClientLicenseOld = async (groupLabel?: string) => {
getClientLicenseOld = async (groupLabel?: RequestGroupLabel) => {
return this.doFetch(
`${this.urlVersion}/license/client?format=old`,
{method: 'get', groupLabel},
@ -77,28 +77,28 @@ const ClientGeneral = <TBase extends Constructor<ClientBase>>(superclass: TBase)
);
};
getGlobalDataRetentionPolicy = (groupLabel?: string) => {
getGlobalDataRetentionPolicy = (groupLabel?: RequestGroupLabel) => {
return this.doFetch(
`${this.getGlobalDataRetentionRoute()}/policy`,
{method: 'get', groupLabel},
);
};
getTeamDataRetentionPolicies = (userId: string, page = 0, perPage = PER_PAGE_DEFAULT, groupLabel?: string) => {
getTeamDataRetentionPolicies = (userId: string, page = 0, perPage = PER_PAGE_DEFAULT, groupLabel?: RequestGroupLabel) => {
return this.doFetch(
`${this.getGranularDataRetentionRoute(userId)}/team_policies${buildQueryString({page, per_page: perPage})}`,
{method: 'get', groupLabel},
);
};
getChannelDataRetentionPolicies = (userId: string, page = 0, perPage = PER_PAGE_DEFAULT, groupLabel?: string) => {
getChannelDataRetentionPolicies = (userId: string, page = 0, perPage = PER_PAGE_DEFAULT, groupLabel?: RequestGroupLabel) => {
return this.doFetch(
`${this.getGranularDataRetentionRoute(userId)}/channel_policies${buildQueryString({page, per_page: perPage})}`,
{method: 'get', groupLabel},
);
};
getRolesByNames = async (rolesNames: string[], groupLabel?: string) => {
getRolesByNames = async (rolesNames: string[], groupLabel?: RequestGroupLabel) => {
return this.doFetch(
`${this.getRolesRoute()}/names`,
{method: 'post', body: rolesNames, groupLabel},

View file

@ -58,7 +58,7 @@ describe('ClientGroups', () => {
test('getAllGroupsAssociatedToChannel', async () => {
const channelId = 'channel1';
const groupLabel = 'testGroup';
const groupLabel = 'WebSocket Reconnect';
await client.getAllGroupsAssociatedToChannel(channelId, undefined, groupLabel);
@ -88,7 +88,7 @@ describe('ClientGroups', () => {
test('getAllGroupsAssociatedToMembership', async () => {
const userId = 'user1';
const groupLabel = 'testGroup';
const groupLabel = 'WebSocket Reconnect';
await client.getAllGroupsAssociatedToMembership(userId, undefined, groupLabel);

View file

@ -9,8 +9,8 @@ import type ClientBase from './base';
export interface ClientGroupsMix {
getGroups: (params: {query?: string; filterAllowReference?: boolean; page?: number; perPage?: number; since?: number; includeMemberCount?: boolean}) => Promise<Group[]>;
getAllGroupsAssociatedToChannel: (channelId: string, filterAllowReference?: boolean, groupLabel?: string) => Promise<{groups: Group[]; total_group_count: number}>;
getAllGroupsAssociatedToMembership: (userId: string, filterAllowReference?: boolean, groupLabel?: string) => Promise<Group[]>;
getAllGroupsAssociatedToChannel: (channelId: string, filterAllowReference?: boolean, groupLabel?: RequestGroupLabel) => Promise<{groups: Group[]; total_group_count: number}>;
getAllGroupsAssociatedToMembership: (userId: string, filterAllowReference?: boolean, groupLabel?: RequestGroupLabel) => Promise<Group[]>;
getAllGroupsAssociatedToTeam: (teamId: string, filterAllowReference?: boolean) => Promise<{groups: Group[]; total_group_count: number}>;
}
@ -29,7 +29,7 @@ const ClientGroups = <TBase extends Constructor<ClientBase>>(superclass: TBase)
);
};
getAllGroupsAssociatedToChannel = async (channelId: string, filterAllowReference = false, groupLabel?: string) => {
getAllGroupsAssociatedToChannel = async (channelId: string, filterAllowReference = false, groupLabel?: RequestGroupLabel) => {
return this.doFetch(
`${this.urlVersion}/channels/${channelId}/groups${buildQueryString({
paginate: false,
@ -47,7 +47,7 @@ const ClientGroups = <TBase extends Constructor<ClientBase>>(superclass: TBase)
);
};
getAllGroupsAssociatedToMembership = async (userId: string, filterAllowReference = false, groupLabel?: string) => {
getAllGroupsAssociatedToMembership = async (userId: string, filterAllowReference = false, groupLabel?: RequestGroupLabel) => {
return this.doFetch(
`${this.urlVersion}/users/${userId}/groups${buildQueryString({paginate: false, filter_allow_reference: filterAllowReference})}`,
{method: 'get', groupLabel},

View file

@ -10,12 +10,12 @@ import type ClientBase from './base';
export interface ClientPostsMix {
createPost: (post: Post) => Promise<Post>;
updatePost: (post: Post) => Promise<Post>;
getPost: (postId: string, groupLabel?: string) => Promise<Post>;
getPost: (postId: string, groupLabel?: RequestGroupLabel) => Promise<Post>;
patchPost: (postPatch: Partial<Post> & {id: string}) => Promise<Post>;
deletePost: (postId: string) => Promise<any>;
getPostThread: (postId: string, options: FetchPaginatedThreadOptions, groupLabel?: string) => Promise<PostResponse>;
getPosts: (channelId: string, page?: number, perPage?: number, collapsedThreads?: boolean, collapsedThreadsExtended?: boolean, groupLabel?: string) => Promise<PostResponse>;
getPostsSince: (channelId: string, since: number, collapsedThreads?: boolean, collapsedThreadsExtended?: boolean, groupLabel?: string) => Promise<PostResponse>;
getPostThread: (postId: string, options: FetchPaginatedThreadOptions, groupLabel?: RequestGroupLabel) => Promise<PostResponse>;
getPosts: (channelId: string, page?: number, perPage?: number, collapsedThreads?: boolean, collapsedThreadsExtended?: boolean, groupLabel?: RequestGroupLabel) => Promise<PostResponse>;
getPostsSince: (channelId: string, since: number, collapsedThreads?: boolean, collapsedThreadsExtended?: boolean, groupLabel?: RequestGroupLabel) => Promise<PostResponse>;
getPostsBefore: (channelId: string, postId?: string, page?: number, perPage?: number, collapsedThreads?: boolean, collapsedThreadsExtended?: boolean) => Promise<PostResponse>;
getPostsAfter: (channelId: string, postId: string, page?: number, perPage?: number, collapsedThreads?: boolean, collapsedThreadsExtended?: boolean) => Promise<PostResponse>;
getFileInfosForPost: (postId: string) => Promise<FileInfo[]>;
@ -51,7 +51,7 @@ const ClientPosts = <TBase extends Constructor<ClientBase>>(superclass: TBase) =
);
};
getPost = async (postId: string, groupLabel?: string) => {
getPost = async (postId: string, groupLabel?: RequestGroupLabel) => {
return this.doFetch(
`${this.getPostRoute(postId)}`,
{method: 'get', groupLabel},
@ -72,7 +72,7 @@ const ClientPosts = <TBase extends Constructor<ClientBase>>(superclass: TBase) =
);
};
getPostThread = (postId: string, options: FetchPaginatedThreadOptions, groupLabel?: string) => {
getPostThread = (postId: string, options: FetchPaginatedThreadOptions, groupLabel?: RequestGroupLabel) => {
const {
fetchThreads = true,
collapsedThreads = false,
@ -88,14 +88,14 @@ const ClientPosts = <TBase extends Constructor<ClientBase>>(superclass: TBase) =
);
};
getPosts = async (channelId: string, page = 0, perPage = PER_PAGE_DEFAULT, collapsedThreads = false, collapsedThreadsExtended = false, groupLabel?: string) => {
getPosts = async (channelId: string, page = 0, perPage = PER_PAGE_DEFAULT, collapsedThreads = false, collapsedThreadsExtended = false, groupLabel?: RequestGroupLabel) => {
return this.doFetch(
`${this.getChannelRoute(channelId)}/posts${buildQueryString({page, per_page: perPage, collapsedThreads, collapsedThreadsExtended})}`,
{method: 'get', groupLabel},
);
};
getPostsSince = async (channelId: string, since: number, collapsedThreads = false, collapsedThreadsExtended = false, groupLabel?: string) => {
getPostsSince = async (channelId: string, since: number, collapsedThreads = false, collapsedThreadsExtended = false, groupLabel?: RequestGroupLabel) => {
return this.doFetch(
`${this.getChannelRoute(channelId)}/posts${buildQueryString({since, collapsedThreads, collapsedThreadsExtended})}`,
{method: 'get', groupLabel},

View file

@ -17,7 +17,7 @@ describe('ClientPreferences', () => {
test('savePreferences', async () => {
const userId = 'user_id';
const preferences = [{category: 'category1', name: 'name1', value: 'value1'}] as PreferenceType[];
const groupLabel = 'group_label';
const groupLabel = 'Server Switch';
const expectedUrl = client.getPreferencesRoute(userId);
const expectedOptions = {
method: 'put',
@ -31,7 +31,7 @@ describe('ClientPreferences', () => {
});
test('getMyPreferences', async () => {
const groupLabel = 'group_label';
const groupLabel = 'Server Switch';
const expectedUrl = client.getPreferencesRoute('me');
const expectedOptions = {
method: 'get',

View file

@ -4,20 +4,20 @@
import type ClientBase from './base';
export interface ClientPreferencesMix {
savePreferences: (userId: string, preferences: PreferenceType[], groupLabel?: string) => Promise<any>;
savePreferences: (userId: string, preferences: PreferenceType[], groupLabel?: RequestGroupLabel) => Promise<any>;
deletePreferences: (userId: string, preferences: PreferenceType[]) => Promise<any>;
getMyPreferences: (groupLabel?: string) => Promise<PreferenceType[]>;
getMyPreferences: (groupLabel?: RequestGroupLabel) => Promise<PreferenceType[]>;
}
const ClientPreferences = <TBase extends Constructor<ClientBase>>(superclass: TBase) => class extends superclass {
savePreferences = async (userId: string, preferences: PreferenceType[], groupLabel?: string) => {
savePreferences = async (userId: string, preferences: PreferenceType[], groupLabel?: RequestGroupLabel) => {
return this.doFetch(
`${this.getPreferencesRoute(userId)}`,
{method: 'put', body: preferences, groupLabel},
);
};
getMyPreferences = async (groupLabel?: string) => {
getMyPreferences = async (groupLabel?: RequestGroupLabel) => {
return this.doFetch(
`${this.getPreferencesRoute('me')}`,
{method: 'get', groupLabel},

View file

@ -12,14 +12,14 @@ export interface ClientTeamsMix {
deleteTeam: (teamId: string) => Promise<any>;
updateTeam: (team: Team) => Promise<Team>;
patchTeam: (team: Partial<Team> & {id: string}) => Promise<Team>;
getTeams: (page?: number, perPage?: number, includeTotalCount?: boolean, groupLabel?: string) => Promise<Team[]>;
getTeam: (teamId: string, groupLabel?: string) => Promise<Team>;
getTeams: (page?: number, perPage?: number, includeTotalCount?: boolean, groupLabel?: RequestGroupLabel) => Promise<Team[]>;
getTeam: (teamId: string, groupLabel?: RequestGroupLabel) => Promise<Team>;
getTeamByName: (teamName: string) => Promise<Team>;
getMyTeams: (groupLabel?: string) => Promise<Team[]>;
getMyTeams: (groupLabel?: RequestGroupLabel) => Promise<Team[]>;
getTeamsForUser: (userId: string) => Promise<Team[]>;
getMyTeamMembers: (groupLabel?: string) => Promise<TeamMembership[]>;
getMyTeamMembers: (groupLabel?: RequestGroupLabel) => Promise<TeamMembership[]>;
getTeamMembers: (teamId: string, page?: number, perPage?: number) => Promise<TeamMembership[]>;
getTeamMember: (teamId: string, userId: string, groupLabel?: string) => Promise<TeamMembership>;
getTeamMember: (teamId: string, userId: string, groupLabel?: RequestGroupLabel) => Promise<TeamMembership>;
getTeamMembersByIds: (teamId: string, userIds: string[]) => Promise<TeamMembership[]>;
addToTeam: (teamId: string, userId: string) => Promise<TeamMembership>;
addUsersToTeamGracefully: (teamId: string, userIds: string[]) => Promise<TeamMemberWithError[]>;
@ -59,14 +59,14 @@ const ClientTeams = <TBase extends Constructor<ClientBase>>(superclass: TBase) =
);
};
getTeams = async (page = 0, perPage = PER_PAGE_DEFAULT, includeTotalCount = false, groupLabel?: string) => {
getTeams = async (page = 0, perPage = PER_PAGE_DEFAULT, includeTotalCount = false, groupLabel?: RequestGroupLabel) => {
return this.doFetch(
`${this.getTeamsRoute()}${buildQueryString({page, per_page: perPage, include_total_count: includeTotalCount})}`,
{method: 'get', groupLabel},
);
};
getTeam = async (teamId: string, groupLabel?: string) => {
getTeam = async (teamId: string, groupLabel?: RequestGroupLabel) => {
return this.doFetch(
this.getTeamRoute(teamId),
{method: 'get', groupLabel},
@ -80,7 +80,7 @@ const ClientTeams = <TBase extends Constructor<ClientBase>>(superclass: TBase) =
);
};
getMyTeams = async (groupLabel?: string) => {
getMyTeams = async (groupLabel?: RequestGroupLabel) => {
return this.doFetch(
`${this.getUserRoute('me')}/teams`,
{method: 'get', groupLabel},
@ -94,7 +94,7 @@ const ClientTeams = <TBase extends Constructor<ClientBase>>(superclass: TBase) =
);
};
getMyTeamMembers = async (groupLabel?: string) => {
getMyTeamMembers = async (groupLabel?: RequestGroupLabel) => {
return this.doFetch(
`${this.getUserRoute('me')}/teams/members`,
{method: 'get', groupLabel},
@ -108,7 +108,7 @@ const ClientTeams = <TBase extends Constructor<ClientBase>>(superclass: TBase) =
);
};
getTeamMember = async (teamId: string, userId: string, groupLabel?: string) => {
getTeamMember = async (teamId: string, userId: string, groupLabel?: RequestGroupLabel) => {
return this.doFetch(
`${this.getTeamMemberRoute(teamId, userId)}`,
{method: 'get', groupLabel},

View file

@ -31,7 +31,7 @@ describe('ClientThreads', () => {
const totalsOnly = true;
const serverVersion = '6.0.0';
const excludeDirect = true;
const groupLabel = 'group1';
const groupLabel = 'Cold Start';
const queryStringObj = {
extended: 'true',
before,

View file

@ -12,7 +12,7 @@ export interface ClientThreadsMix {
userId: string, teamId: string,
before?: string, after?: string, pageSize?: number,
deleted?: boolean, unread?: boolean, since?: number, totalsOnly?: boolean, serverVersion?: string,
excludeDirect?: boolean, groupLabel?: string,
excludeDirect?: boolean, groupLabel?: RequestGroupLabel,
) => Promise<GetUserThreadsResponse>;
getThread: (userId: string, teamId: string, threadId: string, extended?: boolean) => Promise<any>;
markThreadAsRead: (userId: string, teamId: string, threadId: string, timestamp: number) => Promise<any>;
@ -26,7 +26,7 @@ const ClientThreads = <TBase extends Constructor<ClientBase>>(superclass: TBase)
userId: string, teamId: string,
before = '', after = '', pageSize = PER_PAGE_DEFAULT,
deleted = false, unread = false, since = 0, totalsOnly = false, serverVersion = '',
excludeDirect = false, groupLabel?: string) => {
excludeDirect = false, groupLabel?: RequestGroupLabel) => {
const queryStringObj: Record<string, any> = {
extended: 'true',
before,

View file

@ -1,13 +1,21 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
/* eslint-disable max-lines */
import {DeviceEventEmitter} from 'react-native';
import LocalConfig from '@assets/config.json';
import {Events} from '@constants';
import test_helper from '@test/test_helper';
import * as ClientConstants from './constants';
import ClientTracking from './tracking';
import ClientTracking, {testExports} from './tracking';
import type ClientError from './error';
import type {APIClientInterface, ClientResponseMetrics} from '@mattermost/react-native-network-client';
type ParallelGroup = typeof testExports.ParallelGroup;
jest.mock('react-native', () => ({
DeviceEventEmitter: {
emit: jest.fn(),
@ -50,7 +58,11 @@ jest.mock('@init/credentials', () => ({
setServerCredentials: jest.fn(),
}));
describe('ClientTraking', () => {
jest.mock('@managers/performance_metrics_manager', () => ({
collectNetworkRequestData: jest.fn(),
}));
describe('ClientTracking', () => {
const apiClientMock = {
baseUrl: 'https://example.com',
get: jest.fn(),
@ -62,11 +74,22 @@ describe('ClientTraking', () => {
let client: ClientTracking;
beforeAll(() => {
LocalConfig.CollectNetworkMetrics = true;
});
beforeEach(() => {
jest.clearAllMocks();
client = new ClientTracking(apiClientMock as unknown as APIClientInterface);
});
afterEach(() => {
jest.clearAllMocks();
});
afterAll(() => {
LocalConfig.CollectNetworkMetrics = false;
});
it('should set bearer token', () => {
const token = 'testToken';
client.setBearerToken(token);
@ -92,60 +115,73 @@ describe('ClientTraking', () => {
});
it('should initialize and track group data', () => {
client.initTrackGroup('testGroup');
expect(client.requestGroups.has('testGroup')).toBe(true);
client.initTrackGroup('Cold Start');
expect(client.requestGroups.has('Cold Start')).toBe(true);
client.trackRequest('testGroup', 'https://example.com/api', {size: 100, compressedSize: 50} as ClientResponseMetrics);
const group = client.requestGroups.get('testGroup')!;
client.trackRequest('Cold Start', 'https://example.com/api', {size: 100, compressedSize: 50} as ClientResponseMetrics);
const group = client.requestGroups.get('Cold Start')!;
expect(group.totalSize).toBe(100);
expect(group.totalCompressedSize).toBe(50);
expect(group.urls['https://example.com/api'].count).toBe(1);
});
it('should increment and decrement request count', () => {
client.initTrackGroup('testGroup');
client.incrementRequestCount('testGroup');
let group = client.requestGroups.get('testGroup')!;
client.initTrackGroup('Cold Start');
client.incrementRequestCount('Cold Start');
let group = client.requestGroups.get('Cold Start')!;
expect(group.activeCount).toBe(1);
client.decrementRequestCount('testGroup');
group = client.requestGroups.get('testGroup')!;
client.decrementRequestCount('Cold Start');
group = client.requestGroups.get('Cold Start')!;
expect(group.activeCount).toBe(0);
});
it('should handle request completion', () => {
client.initTrackGroup('testGroup');
client.handleRequestCompletion('testGroup');
client.initTrackGroup('Cold Start');
client.handleRequestCompletion('Cold Start');
expect(require('@utils/log').logDebug).toHaveBeenCalledWith('Group "testGroup" completed.');
expect(require('@utils/log').logDebug).toHaveBeenCalledWith('Group "Cold Start" completed.');
});
it('should clear completion timer', () => {
client.initTrackGroup('testGroup');
const group = client.requestGroups.get('testGroup')!;
client.initTrackGroup('Cold Start');
const group = client.requestGroups.get('Cold Start')!;
group.completionTimer = setTimeout(() => {}, 100);
client.clearCompletionTimer('testGroup');
client.clearCompletionTimer('Cold Start');
expect(group.completionTimer).toBeUndefined();
});
it('should check if all requests are completed', () => {
client.initTrackGroup('testGroup');
const result = client.allRequestsCompleted('testGroup');
client.initTrackGroup('Cold Start');
const result = client.allRequestsCompleted('Cold Start');
expect(result).toBe(true);
});
it('should send telemetry event', () => {
client.initTrackGroup('testGroup');
const group = client.requestGroups.get('testGroup')!;
client.initTrackGroup('Cold Start');
const group = client.requestGroups.get('Cold Start')!;
group.urls = {
'https://example.com/api': {
count: 2,
metrics: {latency: 200, networkType: 'Wi-Fi', tlsCipherSuite: 'none', tlsVersion: 'none', isCached: false, httpVersion: 'h2', compressedSize: 10 * 1024, size: 6 * 1024 * 1024, connectionTime: 0, startTime: 0, endTime: 100, speedInMbps: 1},
metrics: {
latency: 200,
startTime: Date.now(),
endTime: Date.now() + 300,
speedInMbps: 1,
networkType: 'Wi-Fi',
tlsCipherSuite: 'none',
tlsVersion: 'none',
isCached: false,
httpVersion: 'h2',
compressedSize: 10 * 1024,
size: 6 * 1024 * 1024,
connectionTime: 0,
},
},
};
client.sendTelemetryEvent('testGroup', group, 1000);
client.sendTelemetryEvent('Cold Start', group, 1000);
expect(require('@utils/log').logInfo).toHaveBeenCalled();
});
@ -173,7 +209,7 @@ describe('ClientTraking', () => {
const options = {
method: 'GET',
groupLabel: 'testGroup',
groupLabel: 'Cold Start' as RequestGroupLabel,
};
const result = await client.doFetchWithTracking('https://example.com/api', options);
@ -185,12 +221,98 @@ describe('ClientTraking', () => {
const options = {
method: 'GET',
groupLabel: 'testGroup',
groupLabel: 'Cold Start' as RequestGroupLabel,
};
await expect(client.doFetchWithTracking('https://example.com/api', options)).rejects.toThrow('Received invalid response from the server.');
});
it('should handle non-ok response with error details', async () => {
apiClientMock.get.mockResolvedValue({
ok: false,
data: {
message: 'Custom error message',
id: 'error_id_123',
},
code: 400,
});
const options = {
method: 'GET',
groupLabel: 'Cold Start' as RequestGroupLabel,
};
try {
await client.doFetchWithTracking('https://example.com/api', options);
fail('Expected error to be thrown');
} catch (error: unknown) {
const clientError = error as ClientError;
expect((clientError.details as {message: string}).message).toBe('Custom error message');
expect((clientError.details as {server_error_id: string}).server_error_id).toBe('error_id_123');
expect((clientError.details as {status_code: string}).status_code).toBe(400);
}
});
it('should handle non-ok response without error details', async () => {
apiClientMock.get.mockResolvedValue({
ok: false,
data: {},
code: 500,
});
const options = {
method: 'GET',
groupLabel: 'Cold Start' as RequestGroupLabel,
};
try {
await client.doFetchWithTracking('https://example.com/api', options);
fail('Expected error to be thrown');
} catch (error: unknown) {
const clientError = error as ClientError;
expect((clientError.details as {message: string}).message).toBe('Response with status code 500');
expect((clientError.details as {status_code: string}).status_code).toBe(500);
}
});
it('should handle response with bearer token header', async () => {
apiClientMock.get.mockResolvedValue({
ok: true,
data: {success: true},
headers: {
Token: 'new_bearer_token',
},
});
const options = {
method: 'GET',
groupLabel: 'Cold Start' as RequestGroupLabel,
};
await client.doFetchWithTracking('https://example.com/api', options);
expect(client.requestHeaders[ClientConstants.HEADER_AUTH]).toBe(`${ClientConstants.HEADER_BEARER} new_bearer_token`);
});
it('should handle response with lowercase bearer token header', async () => {
apiClientMock.get.mockResolvedValue({
ok: true,
data: {success: true},
headers: {
token: 'new_lowercase_token',
},
});
const options = {
method: 'GET',
groupLabel: 'Cold Start' as RequestGroupLabel,
};
await client.doFetchWithTracking('https://example.com/api', options);
expect(client.requestHeaders[ClientConstants.HEADER_AUTH]).toBe(`${ClientConstants.HEADER_BEARER} new_lowercase_token`);
});
it('should call increment and decrement the same number of times as doFetchWithTracking, and handleRequestCompletion only once', async () => {
const incrementSpy = jest.spyOn(client, 'incrementRequestCount');
const decrementSpy = jest.spyOn(client, 'decrementRequestCount');
@ -203,67 +325,501 @@ describe('ClientTraking', () => {
ok: true,
data: {success: true},
headers: {},
metrics: {latency: 200, size: 500, compressedSize: 300},
metrics: {latency: 200, size: 500, compressedSize: 300, startTime: Date.now(), endTime: Date.now() + 100, speedInMbps: 1},
});
}, 200); // Simulate a 100ms network delay
});
});
client.initTrackGroup('testGroup');
client.initTrackGroup('Cold Start');
// Simulate multiple fetch calls
const promises = [
client.doFetchWithTracking('https://example.com/api1', {method: 'GET', groupLabel: 'testGroup'}),
client.doFetchWithTracking('https://example.com/api2', {method: 'GET', groupLabel: 'testGroup'}),
client.doFetchWithTracking('https://example.com/api3', {method: 'GET', groupLabel: 'testGroup'}),
client.doFetchWithTracking('https://example.com/api1', {method: 'GET', groupLabel: 'Cold Start'}),
client.doFetchWithTracking('https://example.com/api2', {method: 'GET', groupLabel: 'Cold Start'}),
client.doFetchWithTracking('https://example.com/api3', {method: 'GET', groupLabel: 'Cold Start'}),
];
// Wait for all fetches to resolve
await Promise.all(promises);
const group = client.requestGroups.get('testGroup')!;
const group = client.requestGroups.get('Cold Start')!;
expect(group.activeCount).toBe(0);
await test_helper.wait(100);
await test_helper.wait(300);
expect(handleRequestCompletionSpy).toHaveBeenCalledTimes(1);
expect(incrementSpy).toHaveBeenCalledTimes(3);
expect(decrementSpy).toHaveBeenCalledTimes(3);
});
it('should handle invalid HTTP method', async () => {
const options = {
method: 'INVALID' as string,
groupLabel: 'Cold Start' as RequestGroupLabel,
};
const result = await client.doFetchWithTracking('https://example.com/api', options) as unknown as {error: string};
expect(result.error).toEqual(new Error('Invalid request method'));
});
it('should handle server version changes without cache control', async () => {
apiClientMock.get.mockResolvedValue({
ok: true,
data: {success: true},
headers: {
'x-version-id': '5.30.0',
},
});
await client.doFetchWithTracking('https://example.com/api', {
method: 'GET',
groupLabel: 'Cold Start',
});
expect(client.serverVersion).toBe('5.30.0');
expect(DeviceEventEmitter.emit).toHaveBeenCalledWith(
Events.SERVER_VERSION_CHANGED,
{serverUrl: 'https://example.com', serverVersion: '5.30.0'},
);
});
it('should not update server version when cache control present', async () => {
client.serverVersion = '5.20.0';
apiClientMock.get.mockResolvedValue({
ok: true,
data: {success: true},
headers: {
'x-version-id': '5.31.0',
'Cache-Control': 'no-cache',
},
});
await client.doFetchWithTracking('https://example.com/api', {
method: 'GET',
groupLabel: 'Cold Start',
});
expect(client.serverVersion).toBe('5.20.0');
});
it('should track duplicate requests correctly and log duplicate details', async () => {
const logDebugSpy = jest.spyOn(require('@utils/log'), 'logDebug');
apiClientMock.get.mockResolvedValue({
ok: true,
data: {success: true},
headers: {},
metrics: {latency: 150, size: 200, compressedSize: 100},
metrics: {latency: 150, size: 200, compressedSize: 100, startTime: Date.now(), endTime: Date.now() + 100, speedInMbps: 1},
});
client.initTrackGroup('testGroup');
client.initTrackGroup('Cold Start');
// Simulate multiple fetch calls to the same URL
const promises = [
client.doFetchWithTracking('https://example.com/api', {method: 'GET', groupLabel: 'testGroup'}),
client.doFetchWithTracking('https://example.com/api', {method: 'GET', groupLabel: 'testGroup'}),
client.doFetchWithTracking('https://example.com/api', {method: 'GET', groupLabel: 'testGroup'}),
client.doFetchWithTracking('https://example.com/api', {method: 'GET', groupLabel: 'Cold Start'}),
client.doFetchWithTracking('https://example.com/api', {method: 'GET', groupLabel: 'Cold Start'}),
client.doFetchWithTracking('https://example.com/api', {method: 'GET', groupLabel: 'Cold Start'}),
];
// Wait for all fetches to resolve
await Promise.all(promises);
// Verify that the group tracked duplicates correctly
const group = client.requestGroups.get('testGroup')!;
const group = client.requestGroups.get('Cold Start')!;
expect(group.urls['https://example.com/api'].count).toBe(3); // URL was requested 3 times
expect(group.totalSize).toBe(600); // Total size = 3 * 200
expect(group.totalCompressedSize).toBe(300); // Total compressed size = 3 * 100
await test_helper.wait(100);
await test_helper.wait(300);
// Verify duplicate logging
expect(logDebugSpy).toHaveBeenCalledWith(
'Duplicate URLs:\n',
expect.stringContaining('https://example.com/api'),
expect.stringContaining('Duplicate URLs:\n1 - https://example.com/api'),
);
});
it('should return true when checking allRequestsCompleted for non-existent group', () => {
// Try to check completion status for a non-existent group
const result = client.allRequestsCompleted('Non Existent Group' as RequestGroupLabel);
// Should return true since there are no pending requests
expect(result).toBe(true);
});
describe('Request Categorization and Speed Calculations', () => {
it('should create multiple parallel groups for non-overlapping requests', () => {
client.initTrackGroup('Cold Start');
const group = client.requestGroups.get('Cold Start')!;
const baseTime = Date.now();
// First parallel group (2 concurrent requests)
group.urls = {
'https://example.com/api1': {
count: 1,
metrics: {
latency: 100,
startTime: baseTime,
endTime: baseTime + 200,
size: 1000,
compressedSize: 500,
} as ClientResponseMetrics,
},
'https://example.com/api2': {
count: 1,
metrics: {
latency: 150,
startTime: baseTime + 50,
endTime: baseTime + 250,
size: 1000,
compressedSize: 500,
} as ClientResponseMetrics,
},
// Second parallel group (3 concurrent requests)
'https://example.com/api3': {
count: 1,
metrics: {
latency: 120,
startTime: baseTime + 300,
endTime: baseTime + 500,
size: 1000,
compressedSize: 500,
} as ClientResponseMetrics,
},
'https://example.com/api4': {
count: 1,
metrics: {
latency: 180,
startTime: baseTime + 320,
endTime: baseTime + 520,
size: 1000,
compressedSize: 500,
} as ClientResponseMetrics,
},
'https://example.com/api5': {
count: 1,
metrics: {
latency: 160,
startTime: baseTime + 340,
endTime: baseTime + 540,
size: 1000,
compressedSize: 500,
} as ClientResponseMetrics,
},
};
const result = client.categorizeRequests('Cold Start');
// Verify parallel groups
expect(result.parallelGroups).toHaveLength(2);
expect(result.maxConcurrency).toBe(3);
// First group should have 2 requests
expect(result.parallelGroups[0].requests).toHaveLength(2);
expect(result.parallelGroups[0].latency).toBe(150); // Max latency of first group
// Second group should have 3 requests
expect(result.parallelGroups[1].requests).toHaveLength(3);
expect(result.parallelGroups[1].latency).toBe(180); // Max latency of second group
});
it('should handle overlapping parallel groups correctly', () => {
client.initTrackGroup('Cold Start');
const group = client.requestGroups.get('Cold Start')!;
const baseTime = Date.now();
group.urls = {
// First request starts early and ends late
'https://example.com/api1': {
count: 1,
metrics: {
latency: 500,
startTime: baseTime,
endTime: baseTime + 1000,
size: 1000,
compressedSize: 500,
} as ClientResponseMetrics,
},
// These requests start during the first request
'https://example.com/api2': {
count: 1,
metrics: {
latency: 200,
startTime: baseTime + 100,
endTime: baseTime + 300,
size: 1000,
compressedSize: 500,
} as ClientResponseMetrics,
},
'https://example.com/api3': {
count: 1,
metrics: {
latency: 200,
startTime: baseTime + 200,
endTime: baseTime + 400,
size: 1000,
compressedSize: 500,
} as ClientResponseMetrics,
},
};
const result = client.categorizeRequests('Cold Start');
// Should create a single parallel group since all requests overlap
expect(result.parallelGroups).toHaveLength(1);
expect(result.maxConcurrency).toBe(3);
// The group should contain all 3 requests
expect(result.parallelGroups[0].requests).toHaveLength(3);
expect(result.parallelGroups[0].latency).toBe(500); // Should take the max latency
});
it('should handle requests with latency extending beyond group end time', () => {
client.initTrackGroup('Cold Start');
const group = client.requestGroups.get('Cold Start')!;
const baseTime = Date.now();
const dataTransferTime = 200;
group.urls = {
// First request starts and ends quickly
'https://example.com/api1': {
count: 1,
metrics: {
latency: 100,
startTime: baseTime,
endTime: baseTime + dataTransferTime + 100,
size: 1000,
compressedSize: 500,
} as ClientResponseMetrics,
},
// Second request starts at same time but has much longer latency
'https://example.com/api2': {
count: 1,
metrics: {
latency: 500, // Long latency
startTime: baseTime,
endTime: baseTime + dataTransferTime + 500, // Ends much later
size: 1000,
compressedSize: 500,
} as ClientResponseMetrics,
},
};
const result = client.categorizeRequests('Cold Start');
// Should create a single parallel group
expect(result.parallelGroups).toHaveLength(1);
expect(result.maxConcurrency).toBe(2);
const group1 = result.parallelGroups[0];
// Group end time should be the earliest end time
expect(group1.endTime).toBe(baseTime + dataTransferTime + 100);
// But latency should be from the longest request
expect(group1.latency).toBe(500);
});
it('should handle requests with latency extending beyond group end time and separate parallel groups', () => {
client.initTrackGroup('Cold Start');
const group = client.requestGroups.get('Cold Start')!;
const baseTime = Date.now();
const dataTransferTime = 200;
const firstGroupMinEndTime = baseTime + dataTransferTime + 100;
group.urls = {
// First parallel group
'https://example.com/api1': {
count: 1,
metrics: {
latency: 100,
startTime: baseTime,
endTime: firstGroupMinEndTime,
size: 1000,
compressedSize: 500,
} as ClientResponseMetrics,
},
'https://example.com/api2': {
count: 1,
metrics: {
latency: 500,
startTime: baseTime,
endTime: baseTime + dataTransferTime + 500,
size: 1000,
compressedSize: 500,
} as ClientResponseMetrics,
},
// Second parallel group - starts after first group has ended
'https://example.com/api3': {
count: 1,
metrics: {
latency: 300,
startTime: firstGroupMinEndTime + 1, // Starts after first group
endTime: firstGroupMinEndTime + dataTransferTime + 300,
size: 1000,
compressedSize: 500,
} as ClientResponseMetrics,
},
'https://example.com/api4': {
count: 1,
metrics: {
latency: 600,
startTime: firstGroupMinEndTime + 1, // Starts with api3
endTime: firstGroupMinEndTime + 100 + 600,
size: 1000,
compressedSize: 500,
} as ClientResponseMetrics,
},
};
const result = client.categorizeRequests('Cold Start');
// Should create two parallel groups
expect(result.parallelGroups).toHaveLength(2);
expect(result.maxConcurrency).toBe(2);
const group1 = result.parallelGroups[0];
const group2 = result.parallelGroups[1];
// First group assertions
expect(group1.endTime).toBe(baseTime + dataTransferTime + 100);
// longest latency on the first group is 500, but the first group ends after 300ms, so the effective latency is only 300ms
expect(group1.latency).toBe(300);
expect(group1.requests).toHaveLength(2);
// Second group assertions
expect(group2.endTime).toBe(firstGroupMinEndTime + dataTransferTime + 300);
// second group min endTime after 500ms, but it's the last group to end, so the effective latency for the group
// is 600ms (longest latency)
expect(group2.latency).toBe(600);
expect(group2.requests).toHaveLength(2);
});
it('should calculate average speed and effective latency for multiple parallel groups', () => {
const baseTime = Date.now();
// Create test parallel groups with known data sizes and timings
const parallelGroups: ParallelGroup[] = [
{
startTime: baseTime,
endTime: baseTime + 1000,
latency: 200,
requests: [
{
latency: 200,
startTime: baseTime,
endTime: baseTime + 1000,
compressedSize: 1000000, // 1MB
} as ClientResponseMetrics,
{
latency: 150,
startTime: baseTime + 100,
endTime: baseTime + 900,
compressedSize: 500000, // 0.5MB
} as ClientResponseMetrics,
],
},
{
startTime: baseTime + 1100,
endTime: baseTime + 2000,
latency: 300,
requests: [
{
latency: 300,
startTime: baseTime + 1100,
endTime: baseTime + 2000,
compressedSize: 2000000, // 2MB
} as ClientResponseMetrics,
{
latency: 250,
startTime: baseTime + 1200,
endTime: baseTime + 1900,
compressedSize: 1500000, // 1.5MB
} as ClientResponseMetrics,
],
},
];
// Total compressed size: 5MB (5,000,000 bytes)
// Total elapsed time: 2 seconds
// Total effective latency: 500ms (200ms + 300ms)
const result = client.calculateAverageSpeedWithCategories(parallelGroups, 2);
// Expected average speed:
// Total bits = 5,000,000 * 8 = 40,000,000 bits
// Data transfer time = 2 seconds - (500ms / 1000) = 1.5 seconds
// Speed = 40,000,000 / 1.5 = 26,666,666.67 bps = 26.67 Mbps
expect(result.averageSpeedMbps).toBeCloseTo(26.67, 1);
expect(result.effectiveLatency).toBe(500); // Sum of max latencies from each group
});
it('should handle zero transfer time in speed calculation', () => {
client.initTrackGroup('Cold Start');
const group = client.requestGroups.get('Cold Start')!;
const now = Date.now();
// Set up metrics where transfer time would be zero
group.urls = {
'https://example.com/api': {
count: 1,
metrics: {
latency: 1000,
startTime: now,
endTime: now + 1000,
speedInMbps: 0,
networkType: 'Wi-Fi',
tlsCipherSuite: 'none',
tlsVersion: 'none',
isCached: false,
httpVersion: 'h2',
size: 1000,
compressedSize: 500,
connectionTime: 0,
} as ClientResponseMetrics,
},
};
const parallelGroups: ParallelGroup[] = [{
startTime: now,
endTime: now + 1000,
latency: 1000,
requests: [group.urls['https://example.com/api'].metrics!],
}];
const result = client.calculateAverageSpeedWithCategories(
parallelGroups,
1, // 1 second elapsed
);
expect(result.averageSpeedMbps).toBe(0); // Should be 0 since data transfer time is 0/negative
expect(result.effectiveLatency).toBe(1000);
});
it('should return empty result when groupData is not found', () => {
// Try to categorize requests for a non-existent group
const result = client.categorizeRequests('Non Existent Group' as RequestGroupLabel);
// Verify empty result structure
expect(result.parallelGroups).toEqual([]);
expect(result.maxConcurrency).toBe(0);
});
it('should return default latency when groupData is not found', () => {
// Try to get average latency for a non-existent group
const result = client.getAverageLatency('Non Existent Group' as RequestGroupLabel);
// Should return default latency of 100ms
expect(result).toBe(100);
});
});
});
/* eslint-enable max-lines */

View file

@ -3,9 +3,12 @@
import {DeviceEventEmitter, Platform} from 'react-native';
import {CollectNetworkMetrics} from '@assets/config.json';
import {Events} from '@constants';
import {t} from '@i18n';
import {setServerCredentials} from '@init/credentials';
import PerformanceMetricsManager from '@managers/performance_metrics_manager';
import {NetworkRequestMetrics} from '@managers/performance_metrics_manager/constant';
import {getFormattedFileSize} from '@utils/file';
import {logDebug, logInfo} from '@utils/log';
import {semverFromServerVersion} from '@utils/server';
@ -30,6 +33,30 @@ type GroupData = {
completionTimer?: NodeJS.Timeout;
}
/**
* ParallelGroup
* @typedef {Object} ParallelGroup
* @property {number} startTime is the start time of all requests in the group
* @property {number} endTime is min end time of all requests in the group
* @property {number} latency is the max latency (in ms)
* @property {ClientResponseMetrics[]} requests is the list of requests in the group
*/
type ParallelGroup = {
startTime: number;
endTime: number;
latency: number;
requests: ClientResponseMetrics[];
}
type CategorizedRequestsResult = {
parallelGroups: ParallelGroup[];
maxConcurrency: number;
};
export const testExports = {
ParallelGroup: {} as ParallelGroup,
};
export default class ClientTracking {
apiClient: APIClientInterface;
csrfToken = '';
@ -63,7 +90,7 @@ export default class ClientTracking {
return headers;
}
initTrackGroup(groupLabel: string) {
initTrackGroup(groupLabel: RequestGroupLabel) {
if (!this.requestGroups.has(groupLabel)) {
this.requestGroups.set(groupLabel, {
activeCount: 0,
@ -76,7 +103,7 @@ export default class ClientTracking {
}
}
trackRequest(groupLabel: string, url: string, metrics?: ClientResponseMetrics) {
trackRequest(groupLabel: RequestGroupLabel, url: string, metrics?: ClientResponseMetrics) {
this.initTrackGroup(groupLabel);
const group = this.requestGroups.get(groupLabel)!;
@ -93,32 +120,134 @@ export default class ClientTracking {
group.totalCompressedSize += metrics?.compressedSize ?? 0;
}
incrementRequestCount(groupLabel: string) {
getAverageLatency(groupLabel: RequestGroupLabel): number {
const groupData = this.requestGroups.get(groupLabel);
if (!groupData) {
return 100;
}
const urlData = Object.entries(groupData.urls);
const sumLatency = urlData.reduce((result, url) => (result + (url[1].metrics?.latency ?? 0)), 0);
return sumLatency / urlData.length;
}
categorizeRequests(groupLabel: RequestGroupLabel): CategorizedRequestsResult {
const groupData = this.requestGroups.get(groupLabel);
if (!groupData) {
const emptyResult: CategorizedRequestsResult = {
parallelGroups: [],
maxConcurrency: 0,
};
return emptyResult;
}
const requestsMetrics = Object.entries(groupData.urls).
map((e) => e[1].metrics).
filter((m) => m != null);
requestsMetrics.sort((a, b) => a.startTime - b.startTime);
const parallelGroups: ParallelGroup[] = [];
let maxConcurrency = 0;
// First pass: group requests
for (const metrics of requestsMetrics) {
const groupIndex = parallelGroups.findIndex((g) => metrics.startTime <= g.endTime);
if (groupIndex >= 0) {
const currentGroup = parallelGroups[groupIndex];
currentGroup.requests.push(metrics);
currentGroup.endTime = Math.min(currentGroup.endTime, metrics.endTime);
currentGroup.latency = Math.max(currentGroup.latency, metrics.latency);
maxConcurrency = Math.max(maxConcurrency, currentGroup.requests.length);
} else {
// Create a new parallel group
parallelGroups.push({
startTime: metrics.startTime,
endTime: metrics.endTime,
latency: metrics.latency,
requests: [metrics],
});
}
}
// Second pass: recalculate max latency for all groups (except the last)
// The max latency should only be considered up until the startTime of the next group
for (let i = 0; i < parallelGroups.length - 1; i++) {
const currentGroup = parallelGroups[i];
let maxGroupLatency = 0;
for (const metrics of currentGroup.requests) {
let currentLatency = metrics.latency;
if ((metrics.startTime + metrics.latency) > currentGroup.endTime) {
currentLatency = currentGroup.endTime - metrics.startTime;
}
maxGroupLatency = Math.max(maxGroupLatency, currentLatency);
}
currentGroup.latency = maxGroupLatency;
}
return {parallelGroups, maxConcurrency};
}
calculateAverageSpeedWithCategories = (
parallelGroups: ParallelGroup[],
elapsedTimeInSeconds: number, // Observed total elapsed time in seconds
): { averageSpeedMbps: number; effectiveLatency: number } => {
// Step 1: Calculate total data size in bits
const totalDataBits = parallelGroups.reduce((sum, group) => {
return sum + group.requests.reduce((groupSum, req) => groupSum + (req.compressedSize * 8), 0);
}, 0);
// Step 2: Calculate effective latency (in ms)
const effectiveLatency = parallelGroups.reduce((sum, group) => sum + group.latency, 0);
// Step 3: Calculate data transfer time
const dataTransferTime = elapsedTimeInSeconds - (effectiveLatency / 1000);
// Handle edge case: if data transfer time is zero or negative.
if (dataTransferTime <= 0) {
return {averageSpeedMbps: 0, effectiveLatency};
}
// Step 4: Calculate average speed
const averageSpeedBps = totalDataBits / dataTransferTime; // Speed in bps
const averageSpeedMbps = averageSpeedBps / 1_000_000; // Convert to Mbps
return {
averageSpeedMbps,
effectiveLatency,
};
};
incrementRequestCount(groupLabel: RequestGroupLabel) {
this.initTrackGroup(groupLabel);
const group = this.requestGroups.get(groupLabel)!;
group.activeCount += 1;
}
decrementRequestCount(groupLabel: string) {
decrementRequestCount(groupLabel: RequestGroupLabel) {
const group = this.requestGroups.get(groupLabel);
if (group) {
group.activeCount -= 1;
if (group.activeCount <= 0 && !group.completionFlag) {
this.clearCompletionTimer(groupLabel);
const latency = this.getAverageLatency(groupLabel);
group.completionTimer = setTimeout(() => {
if (this.allRequestsCompleted(groupLabel)) {
group.completionFlag = true;
this.handleRequestCompletion(groupLabel);
this.clearCompletionTimer(groupLabel);
}
}, 100); // Adjust delay as needed (e.g., 100ms) should we set this based on latency or something?
}, latency);
}
}
}
clearCompletionTimer(groupLabel: string) {
clearCompletionTimer(groupLabel: RequestGroupLabel) {
const group = this.requestGroups.get(groupLabel);
if (group?.completionTimer) {
clearTimeout(group.completionTimer);
@ -126,7 +255,7 @@ export default class ClientTracking {
}
}
handleRequestCompletion(groupLabel: string) {
handleRequestCompletion(groupLabel: RequestGroupLabel) {
const group = this.requestGroups.get(groupLabel);
if (group) {
const duration = Date.now() - group.startTime;
@ -138,31 +267,62 @@ export default class ClientTracking {
}
}
allRequestsCompleted(groupLabel: string): boolean {
allRequestsCompleted(groupLabel: RequestGroupLabel): boolean {
const group = this.requestGroups.get(groupLabel);
return group ? group.activeCount <= 0 : true;
}
sendTelemetryEvent(groupLabel: string, groupData: GroupData, duration: number) {
const urls = Object.keys(groupData.urls);
const urlData = Object.entries(groupData.urls);
sendTelemetryEvent(groupLabel: RequestGroupLabel, groupData: GroupData, duration: number) {
const {totalCompressedSize, totalSize, urls: groupedUrls} = groupData;
const urls = Object.keys(groupedUrls);
const urlData = Object.entries(groupedUrls);
const dupe = urlData.filter((u) => u[1].count > 1);
const urlCount = urlData.reduce((result, url) => (result + url[1].count), 0);
const sumLatency = urlData.reduce((result, url) => (result + (url[1].metrics?.latency ?? 0)), 0);
const latency = sumLatency / urlCount;
logInfo(`Telemetry event on ${Platform.OS} for server ${this.apiClient.baseUrl}
Group "${groupLabel}"
requesting ${urls.length} urls
total Compressed size of: ${getFormattedFileSize(groupData.totalCompressedSize)}
total size of: ${getFormattedFileSize(groupData.totalSize)}
elapsed time: ${duration / 1000} seconds
average latency: ${latency} ms`);
const latency = this.getAverageLatency(groupLabel);
const {parallelGroups, maxConcurrency} = this.categorizeRequests(groupLabel);
const elapsedTimeInSeconds = duration / 1000;
const {averageSpeedMbps, effectiveLatency} = this.calculateAverageSpeedWithCategories(parallelGroups, elapsedTimeInSeconds);
logInfo(`Telemetry Event on ${Platform.OS} for Server ${this.apiClient.baseUrl}
Group: "${groupLabel}"
Total Requests: ${urls.length} URLs
Max Concurrency: ${maxConcurrency}
Parallel Groups: ${parallelGroups.length}
Requests in Each Group: ${parallelGroups.map((g) => g.requests.length).join(', ')}
Total Compressed Size: ${getFormattedFileSize(groupData.totalCompressedSize)}
Total Size: ${getFormattedFileSize(groupData.totalSize)}
Elapsed Time: ${elapsedTimeInSeconds} seconds (${duration} ms)
Average Request Latency: ${latency} ms
Effective Latency: ${effectiveLatency} ms
Average Speed: ${averageSpeedMbps.toFixed(4)} Mbps
`,
);
if (dupe.length) {
logDebug('Duplicate URLs:\n', dupe.map((d) => `${d[0]} ${JSON.stringify(d[1])}`).join('\n'));
logDebug(`Duplicate URLs:\n${dupe.map((d, i) => `${i + 1} - ${d[0]} ${JSON.stringify(d[1])}`).join('\n')}`);
}
// Integrate with telemetry framework here
const {collectNetworkRequestData} = PerformanceMetricsManager;
const commonArguments = {serverUrl: this.apiClient.baseUrl, groupLabel};
const metricsData: Array<[NetworkRequestMetrics, number]> = [
[NetworkRequestMetrics.AverageSpeed, averageSpeedMbps],
[NetworkRequestMetrics.EffectiveLatency, effectiveLatency],
[NetworkRequestMetrics.ElapsedTime, elapsedTimeInSeconds],
[NetworkRequestMetrics.Latency, latency],
[NetworkRequestMetrics.TotalCompressedSize, totalCompressedSize],
[NetworkRequestMetrics.TotalRequests, urls.length],
[NetworkRequestMetrics.TotalSequentialRequests, parallelGroups.length],
[NetworkRequestMetrics.TotalSize, totalSize],
];
metricsData.forEach(([metric, value]) => {
collectNetworkRequestData(metric, value, commonArguments);
});
// Send metrics for each parallel group
parallelGroups.forEach((group) => {
collectNetworkRequestData(NetworkRequestMetrics.TotalParallelRequests, group.requests.length, commonArguments);
});
}
buildRequestOptions(options: ClientOptions): RequestOptions {
@ -208,14 +368,14 @@ export default class ClientTracking {
})};
}
if (groupLabel) {
if (groupLabel && CollectNetworkMetrics) {
this.incrementRequestCount(groupLabel);
}
try {
const response = await request!(url, this.buildRequestOptions(options));
const headers: ClientHeaders = response.headers || {};
if (groupLabel) {
if (groupLabel && CollectNetworkMetrics) {
this.trackRequest(groupLabel, url, response.metrics);
}
const serverVersion = semverFromServerVersion(
@ -255,7 +415,7 @@ export default class ClientTracking {
details: error,
});
} finally {
if (groupLabel) {
if (groupLabel && CollectNetworkMetrics) {
this.decrementRequestCount(groupLabel);
}
}

View file

@ -10,7 +10,7 @@ import type ClientBase from './base';
export interface ClientUsersMix {
createUser: (user: UserProfile, token: string, inviteId: string) => Promise<UserProfile>;
patchMe: (userPatch: Partial<UserProfile>, groupLabel?: string) => Promise<UserProfile>;
patchMe: (userPatch: Partial<UserProfile>, groupLabel?: RequestGroupLabel) => Promise<UserProfile>;
patchUser: (userPatch: Partial<UserProfile> & {id: string}) => Promise<UserProfile>;
updateUser: (user: UserProfile) => Promise<UserProfile>;
demoteUserToGuest: (userId: string) => Promise<any>;
@ -21,15 +21,15 @@ export interface ClientUsersMix {
loginById: (id: string, password: string, token?: string, deviceId?: string) => Promise<UserProfile>;
logout: () => Promise<any>;
getProfiles: (page?: number, perPage?: number, options?: Record<string, any>) => Promise<UserProfile[]>;
getProfilesByIds: (userIds: string[], options?: Record<string, any>, groupLabel?: string) => Promise<UserProfile[]>;
getProfilesByUsernames: (usernames: string[], groupLabel?: string) => Promise<UserProfile[]>;
getProfilesByIds: (userIds: string[], options?: Record<string, any>, groupLabel?: RequestGroupLabel) => Promise<UserProfile[]>;
getProfilesByUsernames: (usernames: string[], groupLabel?: RequestGroupLabel) => Promise<UserProfile[]>;
getProfilesInTeam: (teamId: string, page?: number, perPage?: number, sort?: string, options?: Record<string, any>) => Promise<UserProfile[]>;
getProfilesNotInTeam: (teamId: string, groupConstrained: boolean, page?: number, perPage?: number) => Promise<UserProfile[]>;
getProfilesWithoutTeam: (page?: number, perPage?: number, options?: Record<string, any>) => Promise<UserProfile[]>;
getProfilesInChannel: (channelId: string, options?: GetUsersOptions, groupLabel?: string) => Promise<UserProfile[]>;
getProfilesInGroupChannels: (channelsIds: string[], groupLabel?: string) => Promise<{[x: string]: UserProfile[]}>;
getProfilesInChannel: (channelId: string, options?: GetUsersOptions, groupLabel?: RequestGroupLabel) => Promise<UserProfile[]>;
getProfilesInGroupChannels: (channelsIds: string[], groupLabel?: RequestGroupLabel) => Promise<{[x: string]: UserProfile[]}>;
getProfilesNotInChannel: (teamId: string, channelId: string, groupConstrained: boolean, page?: number, perPage?: number) => Promise<UserProfile[]>;
getMe: (groupLabel?: string) => Promise<UserProfile>;
getMe: (groupLabel?: RequestGroupLabel) => Promise<UserProfile>;
getUser: (userId: string) => Promise<UserProfile>;
getUserByUsername: (username: string) => Promise<UserProfile>;
getUserByEmail: (email: string) => Promise<UserProfile>;
@ -38,10 +38,10 @@ export interface ClientUsersMix {
autocompleteUsers: (name: string, teamId: string, channelId?: string, options?: Record<string, any>) => Promise<{users: UserProfile[]; out_of_channel?: UserProfile[]}>;
getSessions: (userId: string) => Promise<Session[]>;
checkUserMfa: (loginId: string) => Promise<{mfa_required: boolean}>;
setExtraSessionProps: (deviceId: string, notificationsEnabled: boolean, version: string | null, groupLabel?: string) => Promise<{}>;
setExtraSessionProps: (deviceId: string, notificationsEnabled: boolean, version: string | null, groupLabel?: RequestGroupLabel) => Promise<{}>;
searchUsers: (term: string, options: SearchUserOptions) => Promise<UserProfile[]>;
getStatusesByIds: (userIds: string[]) => Promise<UserStatus[]>;
getStatus: (userId: string, groupLabel?: string) => Promise<UserStatus>;
getStatus: (userId: string, groupLabel?: RequestGroupLabel) => Promise<UserStatus>;
updateStatus: (status: UserStatus) => Promise<UserStatus>;
updateCustomStatus: (customStatus: UserCustomStatus) => Promise<{status: string}>;
unsetCustomStatus: () => Promise<{status: string}>;
@ -66,7 +66,7 @@ const ClientUsers = <TBase extends Constructor<ClientBase>>(superclass: TBase) =
);
};
patchMe = async (userPatch: Partial<UserProfile>, groupLabel?: string) => {
patchMe = async (userPatch: Partial<UserProfile>, groupLabel?: RequestGroupLabel) => {
return this.doFetch(
`${this.getUserRoute('me')}/patch`,
{method: 'put', body: userPatch, groupLabel},
@ -177,14 +177,14 @@ const ClientUsers = <TBase extends Constructor<ClientBase>>(superclass: TBase) =
);
};
getProfilesByIds = async (userIds: string[], options = {}, groupLabel?: string) => {
getProfilesByIds = async (userIds: string[], options = {}, groupLabel?: RequestGroupLabel) => {
return this.doFetch(
`${this.getUsersRoute()}/ids${buildQueryString(options)}`,
{method: 'post', body: userIds, groupLabel},
);
};
getProfilesByUsernames = async (usernames: string[], groupLabel?: string) => {
getProfilesByUsernames = async (usernames: string[], groupLabel?: RequestGroupLabel) => {
return this.doFetch(
`${this.getUsersRoute()}/usernames`,
{method: 'post', body: usernames, groupLabel},
@ -217,7 +217,7 @@ const ClientUsers = <TBase extends Constructor<ClientBase>>(superclass: TBase) =
);
};
getProfilesInChannel = async (channelId: string, options: GetUsersOptions, groupLabel?: string) => {
getProfilesInChannel = async (channelId: string, options: GetUsersOptions, groupLabel?: RequestGroupLabel) => {
const queryStringObj = {in_channel: channelId, ...options};
return this.doFetch(
`${this.getUsersRoute()}${buildQueryString(queryStringObj)}`,
@ -225,7 +225,7 @@ const ClientUsers = <TBase extends Constructor<ClientBase>>(superclass: TBase) =
);
};
getProfilesInGroupChannels = async (channelsIds: string[], groupLabel?: string) => {
getProfilesInGroupChannels = async (channelsIds: string[], groupLabel?: RequestGroupLabel) => {
return this.doFetch(
`${this.getUsersRoute()}/group_channels`,
{method: 'post', body: channelsIds, groupLabel},
@ -244,7 +244,7 @@ const ClientUsers = <TBase extends Constructor<ClientBase>>(superclass: TBase) =
);
};
getMe = async (groupLabel?: string) => {
getMe = async (groupLabel?: RequestGroupLabel) => {
return this.doFetch(
`${this.getUserRoute('me')}`,
{method: 'get', groupLabel},
@ -332,7 +332,7 @@ const ClientUsers = <TBase extends Constructor<ClientBase>>(superclass: TBase) =
);
};
setExtraSessionProps = async (deviceId: string, deviceNotificationDisabled: boolean, version: string | null, groupLabel?: string) => {
setExtraSessionProps = async (deviceId: string, deviceNotificationDisabled: boolean, version: string | null, groupLabel?: RequestGroupLabel) => {
return this.doFetch(
`${this.getUsersRoute()}/sessions/device`,
{
@ -361,7 +361,7 @@ const ClientUsers = <TBase extends Constructor<ClientBase>>(superclass: TBase) =
);
};
getStatus = async (userId: string, groupLabel?: string) => {
getStatus = async (userId: string, groupLabel?: RequestGroupLabel) => {
return this.doFetch(
`${this.getUserRoute(userId)}/status`,
{method: 'get', groupLabel},

View file

@ -8,6 +8,7 @@ export default {
POST_CHUNK_SIZE: 60,
POST_AROUND_CHUNK_SIZE: 10,
CHANNELS_CHUNK_SIZE: 50,
CHANNEL_MEMBERS_CHUNK_SIZE: 200,
CRT_CHUNK_SIZE: 60,
STATUS_INTERVAL: 60000,
AUTOCOMPLETE_LIMIT_DEFAULT: 25,

View file

@ -182,7 +182,7 @@ const launchToHome = async (props: LaunchProps) => {
openPushNotification = Boolean(props.serverUrl && !props.launchError && extra.userInteraction && extra.payload?.channel_id && !extra.payload?.userInfo?.local);
if (openPushNotification) {
await resetToHome(props);
return pushNotificationEntry(props.serverUrl!, extra.payload!, 'notification');
return pushNotificationEntry(props.serverUrl!, extra.payload!, 'Notification');
}
appEntry(props.serverUrl!);

View file

@ -103,7 +103,7 @@ class AppsManager {
}
};
fetchBindings = async (serverUrl: string, channelId: string, forThread = false, groupLabel?: string) => {
fetchBindings = async (serverUrl: string, channelId: string, forThread = false, groupLabel?: RequestGroupLabel) => {
try {
const {database} = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
const userId = await getCurrentUserId(database);
@ -135,13 +135,14 @@ class AppsManager {
}
};
refreshAppBindings = async (serverUrl: string, groupLabel?: string) => {
refreshAppBindings = async (serverUrl: string, groupLabel?: RequestGroupLabel) => {
try {
const {database} = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
const appsEnabled = (await getConfig(database))?.FeatureFlagAppsEnabled === 'true';
const appsEnabled = await this.isAppsEnabled(serverUrl);
if (!appsEnabled) {
this.getEnabledSubject(serverUrl).next(false);
this.clearServer(serverUrl);
return;
}
const channelId = await getCurrentChannelId(database);

View file

@ -0,0 +1,13 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
export enum NetworkRequestMetrics {
AverageSpeed = 'mobile_network_requests_average_speed',
EffectiveLatency = 'mobile_network_requests_effective_latency',
ElapsedTime = 'mobile_network_requests_elapsed_time',
Latency = 'mobile_network_requests_latency',
TotalCompressedSize = 'mobile_network_requests_total_compressed_size',
TotalParallelRequests = 'mobile_network_requests_total_parallel_requests',
TotalRequests = 'mobile_network_requests_total_requests',
TotalSequentialRequests = 'mobile_network_requests_total_sequential_requests',
TotalSize = 'mobile_network_requests_total_size',
}

View file

@ -2,6 +2,7 @@
// See LICENSE.txt for license information.
import RNUtils from '@mattermost/rnutils';
import {AppState} from 'react-native';
import performance from 'react-native-performance';
import {mockApiClient} from '@test/mock_api_client';
@ -10,6 +11,7 @@ import {logWarning} from '@utils/log';
import NetworkManager from '../network_manager';
import {NetworkRequestMetrics} from './constant';
import {testExports as batcherTestExports} from './performance_metrics_batcher';
import {getBaseReportRequest} from './test_utils';
@ -33,263 +35,348 @@ jest.mock('@utils/log', () => ({
performance.timeOrigin = TEST_EPOCH;
describe('load metrics', () => {
describe('performance_metrics_manager', () => {
const serverUrl = 'http://www.someserverurl.com/';
const expectedUrl = `${serverUrl}/api/v4/client_perf`;
let PerformanceMetricsManager = new PerformanceMetricsManagerClass();
const getMeasure = (timestamp: number, value: number): PerformanceReportMeasure => {
return {
metric: 'mobile_load',
timestamp,
value,
};
};
beforeEach(async () => {
jest.useFakeTimers({doNotFake: [
'cancelAnimationFrame',
'cancelIdleCallback',
'clearImmediate',
'clearInterval',
'clearTimeout',
'hrtime',
'nextTick',
'queueMicrotask',
'requestAnimationFrame',
'requestIdleCallback',
'setImmediate',
'setInterval',
]}).setSystemTime(new Date(TEST_EPOCH));
NetworkManager.createClient(serverUrl);
const {operator} = await TestHelper.setupServerDatabase(serverUrl);
await operator.handleConfigs({configs: [{id: 'EnableClientMetrics', value: 'true'}], configsToDelete: [], prepareRecordsOnly: false});
jest.useFakeTimers({doNotFake: [
'cancelAnimationFrame',
'cancelIdleCallback',
'clearImmediate',
'clearInterval',
'clearTimeout',
'hrtime',
'nextTick',
'queueMicrotask',
'requestAnimationFrame',
'requestIdleCallback',
'setImmediate',
'setInterval',
]}).setSystemTime(new Date(TEST_EPOCH));
PerformanceMetricsManager = new PerformanceMetricsManagerClass();
const mockHasRegisteredLoad = {hasRegisteredLoad: false};
jest.mocked(RNUtils.setHasRegisteredLoad).mockImplementation(() => {
mockHasRegisteredLoad.hasRegisteredLoad = true;
});
jest.mocked(RNUtils.getHasRegisteredLoad).mockImplementation(() => mockHasRegisteredLoad);
});
afterEach(async () => {
jest.useRealTimers();
NetworkManager.invalidateClient(serverUrl);
await TestHelper.tearDown();
performance.clearMarks();
});
it('only load on target', async () => {
performance.mark('nativeLaunchStart');
const measure = getMeasure(TEST_EPOCH + INTERVAL_TIME, INTERVAL_TIME);
const expectedRequest = getBaseReportRequest(measure.timestamp, measure.timestamp + 1);
expectedRequest.body.histograms = [measure];
jest.clearAllMocks();
PerformanceMetricsManager.setLoadTarget('CHANNEL');
PerformanceMetricsManager.finishLoad('HOME', serverUrl);
await TestHelper.tick();
jest.advanceTimersByTime(INTERVAL_TIME);
await TestHelper.tick();
expect(mockApiClient.post).not.toHaveBeenCalled();
PerformanceMetricsManager.finishLoad('CHANNEL', serverUrl);
await TestHelper.tick();
jest.advanceTimersByTime(INTERVAL_TIME);
await TestHelper.tick();
expect(mockApiClient.post).toHaveBeenCalledWith(expectedUrl, expectedRequest);
});
it('only register load once', async () => {
performance.mark('nativeLaunchStart');
const measure = getMeasure(TEST_EPOCH, 0);
const expectedRequest = getBaseReportRequest(measure.timestamp, measure.timestamp + 1);
expectedRequest.body.histograms = [measure];
PerformanceMetricsManager.setLoadTarget('HOME');
PerformanceMetricsManager.finishLoad('HOME', serverUrl);
await TestHelper.tick();
jest.advanceTimersByTime(INTERVAL_TIME);
await TestHelper.tick();
expect(mockApiClient.post).toHaveBeenCalledWith(expectedUrl, expectedRequest);
mockApiClient.post.mockClear();
PerformanceMetricsManager.finishLoad('HOME', serverUrl);
await TestHelper.tick();
jest.advanceTimersByTime(INTERVAL_TIME);
await TestHelper.tick();
expect(mockApiClient.post).not.toHaveBeenCalled();
});
it('retry if the mark is not yet present', async () => {
const measure = getMeasure(TEST_EPOCH + (RETRY_TIME * 2), RETRY_TIME);
const expectedRequest = getBaseReportRequest(measure.timestamp, measure.timestamp + 1);
expectedRequest.body.histograms = [measure];
PerformanceMetricsManager.setLoadTarget('CHANNEL');
PerformanceMetricsManager.finishLoad('CHANNEL', serverUrl);
await TestHelper.tick();
jest.advanceTimersByTime(RETRY_TIME);
await TestHelper.tick();
performance.mark('nativeLaunchStart');
await TestHelper.tick();
jest.advanceTimersByTime(RETRY_TIME);
await TestHelper.tick();
await TestHelper.tick();
jest.advanceTimersByTime(INTERVAL_TIME);
await TestHelper.tick();
expect(mockApiClient.post).toHaveBeenCalledWith(expectedUrl, expectedRequest);
});
it('fail graciously if no measure is set', async () => {
PerformanceMetricsManager.setLoadTarget('CHANNEL');
PerformanceMetricsManager.finishLoad('CHANNEL', serverUrl);
for (let i = 0; i < MAX_RETRIES; i++) {
// eslint-disable-next-line no-await-in-loop
await TestHelper.tick();
jest.advanceTimersByTime(MAX_RETRIES);
// eslint-disable-next-line no-await-in-loop
await TestHelper.tick();
}
await TestHelper.tick();
jest.advanceTimersByTime(INTERVAL_TIME);
await TestHelper.tick();
expect(mockApiClient.post).not.toHaveBeenCalled();
expect(logWarning).toHaveBeenCalled();
});
});
describe('other metrics', () => {
const serverUrl1 = 'http://www.someserverurl.com/';
const expectedUrl1 = `${serverUrl1}/api/v4/client_perf`;
const serverUrl2 = 'http://www.otherserverurl.com/';
const expectedUrl2 = `${serverUrl2}/api/v4/client_perf`;
const measure1: PerformanceReportMeasure = {
metric: 'mobile_channel_switch',
timestamp: TEST_EPOCH + 100,
value: 100,
};
const measure2: PerformanceReportMeasure = {
metric: 'mobile_team_switch',
timestamp: TEST_EPOCH + 150 + 50,
value: 150,
};
const PerformanceMetricsManager = new PerformanceMetricsManagerClass();
beforeEach(async () => {
NetworkManager.createClient(serverUrl1);
NetworkManager.createClient(serverUrl2);
const {operator: operator1} = await TestHelper.setupServerDatabase(serverUrl1);
const {operator: operator2} = await TestHelper.setupServerDatabase(serverUrl2);
await operator1.handleConfigs({configs: [{id: 'EnableClientMetrics', value: 'true'}], configsToDelete: [], prepareRecordsOnly: false});
await operator2.handleConfigs({configs: [{id: 'EnableClientMetrics', value: 'true'}], configsToDelete: [], prepareRecordsOnly: false});
jest.useFakeTimers({doNotFake: [
'cancelAnimationFrame',
'cancelIdleCallback',
'clearImmediate',
'clearInterval',
'clearTimeout',
'hrtime',
'nextTick',
'queueMicrotask',
'requestAnimationFrame',
'requestIdleCallback',
'setImmediate',
'setInterval',
]}).setSystemTime(new Date(TEST_EPOCH));
});
afterEach(async () => {
jest.useRealTimers();
NetworkManager.invalidateClient(serverUrl1);
NetworkManager.invalidateClient(serverUrl2);
await TestHelper.tearDown();
performance.clearMarks();
});
describe('load metrics', () => {
const getMeasure = (timestamp: number, value: number): PerformanceReportMeasure => {
return {
metric: 'mobile_load',
timestamp,
value,
};
};
beforeEach(async () => {
PerformanceMetricsManager = new PerformanceMetricsManagerClass();
const mockHasRegisteredLoad = {hasRegisteredLoad: false};
jest.mocked(RNUtils.setHasRegisteredLoad).mockImplementation(() => {
mockHasRegisteredLoad.hasRegisteredLoad = true;
});
jest.mocked(RNUtils.getHasRegisteredLoad).mockImplementation(() => mockHasRegisteredLoad);
});
afterEach(async () => {
performance.clearMarks();
});
it('only load on target', async () => {
performance.mark('nativeLaunchStart');
const measure = getMeasure(TEST_EPOCH + INTERVAL_TIME, INTERVAL_TIME);
const expectedRequest = getBaseReportRequest(measure.timestamp, measure.timestamp + 1);
expectedRequest.body.histograms = [measure];
PerformanceMetricsManager.setLoadTarget('CHANNEL');
PerformanceMetricsManager.finishLoad('HOME', serverUrl);
await TestHelper.tick();
jest.advanceTimersByTime(INTERVAL_TIME);
await TestHelper.tick();
expect(mockApiClient.post).not.toHaveBeenCalled();
PerformanceMetricsManager.finishLoad('CHANNEL', serverUrl);
await TestHelper.tick();
jest.advanceTimersByTime(INTERVAL_TIME);
await TestHelper.tick();
expect(mockApiClient.post).toHaveBeenCalledWith(expectedUrl, expectedRequest);
});
it('only register load once', async () => {
performance.mark('nativeLaunchStart');
const measure = getMeasure(TEST_EPOCH, 0);
const expectedRequest = getBaseReportRequest(measure.timestamp, measure.timestamp + 1);
expectedRequest.body.histograms = [measure];
PerformanceMetricsManager.setLoadTarget('HOME');
PerformanceMetricsManager.finishLoad('HOME', serverUrl);
await TestHelper.tick();
jest.advanceTimersByTime(INTERVAL_TIME);
await TestHelper.tick();
expect(mockApiClient.post).toHaveBeenCalledWith(expectedUrl, expectedRequest);
mockApiClient.post.mockClear();
PerformanceMetricsManager.finishLoad('HOME', serverUrl);
await TestHelper.tick();
jest.advanceTimersByTime(INTERVAL_TIME);
await TestHelper.tick();
expect(mockApiClient.post).not.toHaveBeenCalled();
});
it('retry if the mark is not yet present', async () => {
const measure = getMeasure(TEST_EPOCH + (RETRY_TIME * 2), RETRY_TIME);
const expectedRequest = getBaseReportRequest(measure.timestamp, measure.timestamp + 1);
expectedRequest.body.histograms = [measure];
PerformanceMetricsManager.setLoadTarget('CHANNEL');
PerformanceMetricsManager.finishLoad('CHANNEL', serverUrl);
await TestHelper.tick();
jest.advanceTimersByTime(RETRY_TIME);
await TestHelper.tick();
performance.mark('nativeLaunchStart');
await TestHelper.tick();
jest.advanceTimersByTime(RETRY_TIME);
await TestHelper.tick();
await TestHelper.tick();
jest.advanceTimersByTime(INTERVAL_TIME);
await TestHelper.tick();
expect(mockApiClient.post).toHaveBeenCalledWith(expectedUrl, expectedRequest);
});
it('fail graciously if no measure is set', async () => {
PerformanceMetricsManager.setLoadTarget('CHANNEL');
PerformanceMetricsManager.finishLoad('CHANNEL', serverUrl);
for (let i = 0; i < MAX_RETRIES; i++) {
// eslint-disable-next-line no-await-in-loop
await TestHelper.tick();
jest.advanceTimersByTime(MAX_RETRIES);
// eslint-disable-next-line no-await-in-loop
await TestHelper.tick();
}
await TestHelper.tick();
jest.advanceTimersByTime(INTERVAL_TIME);
await TestHelper.tick();
expect(mockApiClient.post).not.toHaveBeenCalled();
expect(logWarning).toHaveBeenCalled();
});
});
it('do not send metrics when we do not start them', async () => {
PerformanceMetricsManager.endMetric('mobile_channel_switch', serverUrl1);
describe('app state changes', () => {
const getMeasure = (timestamp: number, value: number): PerformanceReportMeasure => {
return {
metric: 'mobile_channel_switch',
timestamp,
value,
};
};
jest.advanceTimersByTime(INTERVAL_TIME);
await TestHelper.tick();
expect(mockApiClient.post).not.toHaveBeenCalled();
beforeEach(async () => {
AppState.currentState = 'active';
PerformanceMetricsManager = new PerformanceMetricsManagerClass();
});
it('forces send on app state change to inactive, or anything other than active', async () => {
const appStateSpy = jest.spyOn(AppState, 'addEventListener');
const measure = getMeasure(TEST_EPOCH, 0);
PerformanceMetricsManager.startMetric('mobile_channel_switch');
PerformanceMetricsManager.endMetric('mobile_channel_switch', serverUrl);
appStateSpy.mock.calls[0][1]('inactive');
await TestHelper.tick();
const expectedRequest = getBaseReportRequest(measure.timestamp, measure.timestamp + 1);
expectedRequest.body.histograms = [measure];
expect(mockApiClient.post).toHaveBeenCalled();
expect(mockApiClient.post).toHaveBeenCalledWith(`${serverUrl}/api/v4/client_perf`, expectedRequest);
});
});
it('send metric after it has been started', async () => {
const expectedRequest = getBaseReportRequest(measure1.timestamp, measure1.timestamp + 1);
expectedRequest.body.histograms = [measure1];
describe('time to interaction', () => {
beforeEach(() => {
PerformanceMetricsManager = new PerformanceMetricsManagerClass();
});
PerformanceMetricsManager.startMetric('mobile_channel_switch');
jest.advanceTimersByTime(100);
it('measures time to interaction correctly', () => {
PerformanceMetricsManager.startTimeToInteraction();
jest.advanceTimersByTime(100);
const result = PerformanceMetricsManager.measureTimeToInteraction();
expect(result?.duration).toBe(100);
});
PerformanceMetricsManager.endMetric('mobile_channel_switch', serverUrl1);
await TestHelper.tick();
jest.advanceTimersByTime(INTERVAL_TIME);
await TestHelper.tick();
expect(mockApiClient.post).toHaveBeenCalledWith(expectedUrl1, expectedRequest);
it('handles missing TTI mark gracefully', () => {
const result = PerformanceMetricsManager.measureTimeToInteraction();
expect(result).toBeUndefined();
});
});
it('a second end metric does not generate a second measure', async () => {
const expectedRequest = getBaseReportRequest(measure1.timestamp, measure1.timestamp + 1);
expectedRequest.body.histograms = [measure1];
describe('network request metrics', () => {
beforeEach(async () => {
PerformanceMetricsManager = new PerformanceMetricsManagerClass();
});
PerformanceMetricsManager.startMetric('mobile_channel_switch');
jest.advanceTimersByTime(100);
afterEach(async () => {
NetworkManager.invalidateClient(serverUrl);
await TestHelper.tearDown();
});
PerformanceMetricsManager.endMetric('mobile_channel_switch', serverUrl1);
await TestHelper.tick();
jest.advanceTimersByTime(100);
it('collects network request metrics', async () => {
const timestamp = TEST_EPOCH;
jest.setSystemTime(new Date(timestamp));
PerformanceMetricsManager.endMetric('mobile_channel_switch', serverUrl1);
await TestHelper.tick();
PerformanceMetricsManager.collectNetworkRequestData(
NetworkRequestMetrics.ElapsedTime,
100,
{serverUrl, groupLabel: 'Login'},
);
jest.advanceTimersByTime(INTERVAL_TIME);
await TestHelper.tick();
expect(mockApiClient.post).toHaveBeenCalledWith(expectedUrl1, expectedRequest);
jest.advanceTimersByTime(INTERVAL_TIME);
await TestHelper.tick();
const expectedRequest = getBaseReportRequest(timestamp, timestamp + 1);
expectedRequest.body.histograms = [{
metric: NetworkRequestMetrics.ElapsedTime,
value: 100,
timestamp,
label: {network_request_group: 'Login'},
}];
expect(mockApiClient.post).toHaveBeenCalledWith(
`${serverUrl}/api/v4/client_perf`,
expectedRequest,
);
});
});
it('different metrics do not interfere', async () => {
const expectedRequest = getBaseReportRequest(measure1.timestamp, measure2.timestamp);
expectedRequest.body.histograms = [measure1, measure2];
describe('other metrics', () => {
const serverUrl1 = 'http://www.someserverurl.com/';
const expectedUrl1 = `${serverUrl1}/api/v4/client_perf`;
PerformanceMetricsManager.startMetric('mobile_channel_switch');
jest.advanceTimersByTime(50);
PerformanceMetricsManager.startMetric('mobile_team_switch');
jest.advanceTimersByTime(50);
const serverUrl2 = 'http://www.otherserverurl.com/';
const expectedUrl2 = `${serverUrl2}/api/v4/client_perf`;
PerformanceMetricsManager.endMetric('mobile_channel_switch', serverUrl1);
await TestHelper.tick();
jest.advanceTimersByTime(100);
PerformanceMetricsManager.endMetric('mobile_team_switch', serverUrl1);
await TestHelper.tick();
const measure1: PerformanceReportMeasure = {
metric: 'mobile_channel_switch',
timestamp: TEST_EPOCH + 100,
value: 100,
};
jest.advanceTimersByTime(INTERVAL_TIME);
await TestHelper.tick();
expect(mockApiClient.post).toHaveBeenCalledWith(expectedUrl1, expectedRequest);
});
const measure2: PerformanceReportMeasure = {
metric: 'mobile_team_switch',
timestamp: TEST_EPOCH + 150 + 50,
value: 150,
};
it('metrics to different servers do not interfere', async () => {
const expectedRequest1 = getBaseReportRequest(measure1.timestamp, measure1.timestamp + 1);
expectedRequest1.body.histograms = [measure1];
beforeEach(async () => {
NetworkManager.createClient(serverUrl1);
NetworkManager.createClient(serverUrl2);
const {operator: operator1} = await TestHelper.setupServerDatabase(serverUrl1);
const {operator: operator2} = await TestHelper.setupServerDatabase(serverUrl2);
await operator1.handleConfigs({configs: [{id: 'EnableClientMetrics', value: 'true'}], configsToDelete: [], prepareRecordsOnly: false});
await operator2.handleConfigs({configs: [{id: 'EnableClientMetrics', value: 'true'}], configsToDelete: [], prepareRecordsOnly: false});
});
afterEach(async () => {
NetworkManager.invalidateClient(serverUrl1);
NetworkManager.invalidateClient(serverUrl2);
const expectedRequest2 = getBaseReportRequest(measure2.timestamp, measure2.timestamp + 1);
expectedRequest2.body.histograms = [measure2];
performance.clearMarks();
});
PerformanceMetricsManager.startMetric('mobile_channel_switch');
jest.advanceTimersByTime(50);
PerformanceMetricsManager.startMetric('mobile_team_switch');
jest.advanceTimersByTime(50);
it('do not send metrics when we do not start them', async () => {
PerformanceMetricsManager.endMetric('mobile_channel_switch', serverUrl1);
PerformanceMetricsManager.endMetric('mobile_channel_switch', serverUrl1);
await TestHelper.tick();
jest.advanceTimersByTime(100);
PerformanceMetricsManager.endMetric('mobile_team_switch', serverUrl2);
await TestHelper.tick();
jest.advanceTimersByTime(INTERVAL_TIME);
await TestHelper.tick();
expect(mockApiClient.post).not.toHaveBeenCalled();
});
jest.advanceTimersByTime(INTERVAL_TIME);
await TestHelper.tick();
expect(mockApiClient.post).toHaveBeenCalledWith(expectedUrl1, expectedRequest1);
expect(mockApiClient.post).toHaveBeenCalledWith(expectedUrl2, expectedRequest2);
it('send metric after it has been started', async () => {
const expectedRequest = getBaseReportRequest(measure1.timestamp, measure1.timestamp + 1);
expectedRequest.body.histograms = [measure1];
PerformanceMetricsManager.startMetric('mobile_channel_switch');
jest.advanceTimersByTime(100);
PerformanceMetricsManager.endMetric('mobile_channel_switch', serverUrl1);
await TestHelper.tick();
jest.advanceTimersByTime(INTERVAL_TIME);
await TestHelper.tick();
expect(mockApiClient.post).toHaveBeenCalledWith(expectedUrl1, expectedRequest);
});
it('a second end metric does not generate a second measure', async () => {
const expectedRequest = getBaseReportRequest(measure1.timestamp, measure1.timestamp + 1);
expectedRequest.body.histograms = [measure1];
PerformanceMetricsManager.startMetric('mobile_channel_switch');
jest.advanceTimersByTime(100);
PerformanceMetricsManager.endMetric('mobile_channel_switch', serverUrl1);
await TestHelper.tick();
jest.advanceTimersByTime(100);
PerformanceMetricsManager.endMetric('mobile_channel_switch', serverUrl1);
await TestHelper.tick();
jest.advanceTimersByTime(INTERVAL_TIME);
await TestHelper.tick();
expect(mockApiClient.post).toHaveBeenCalledWith(expectedUrl1, expectedRequest);
});
it('different metrics do not interfere', async () => {
const expectedRequest = getBaseReportRequest(measure1.timestamp, measure2.timestamp);
expectedRequest.body.histograms = [measure1, measure2];
PerformanceMetricsManager.startMetric('mobile_channel_switch');
jest.advanceTimersByTime(50);
PerformanceMetricsManager.startMetric('mobile_team_switch');
jest.advanceTimersByTime(50);
PerformanceMetricsManager.endMetric('mobile_channel_switch', serverUrl1);
await TestHelper.tick();
jest.advanceTimersByTime(100);
PerformanceMetricsManager.endMetric('mobile_team_switch', serverUrl1);
await TestHelper.tick();
jest.advanceTimersByTime(INTERVAL_TIME);
await TestHelper.tick();
expect(mockApiClient.post).toHaveBeenCalledWith(expectedUrl1, expectedRequest);
});
it('metrics to different servers do not interfere', async () => {
const expectedRequest1 = getBaseReportRequest(measure1.timestamp, measure1.timestamp + 1);
expectedRequest1.body.histograms = [measure1];
const expectedRequest2 = getBaseReportRequest(measure2.timestamp, measure2.timestamp + 1);
expectedRequest2.body.histograms = [measure2];
PerformanceMetricsManager.startMetric('mobile_channel_switch');
jest.advanceTimersByTime(50);
PerformanceMetricsManager.startMetric('mobile_team_switch');
jest.advanceTimersByTime(50);
PerformanceMetricsManager.endMetric('mobile_channel_switch', serverUrl1);
await TestHelper.tick();
jest.advanceTimersByTime(100);
PerformanceMetricsManager.endMetric('mobile_team_switch', serverUrl2);
await TestHelper.tick();
jest.advanceTimersByTime(INTERVAL_TIME);
await TestHelper.tick();
expect(mockApiClient.post).toHaveBeenCalledWith(expectedUrl1, expectedRequest1);
expect(mockApiClient.post).toHaveBeenCalledWith(expectedUrl2, expectedRequest2);
});
});
});

View file

@ -5,11 +5,15 @@ import RNUtils from '@mattermost/rnutils';
import {AppState, type AppStateStatus} from 'react-native';
import performance from 'react-native-performance';
import {logWarning} from '@utils/log';
import {logDebug, logWarning} from '@utils/log';
import Batcher from './performance_metrics_batcher';
import type {NetworkRequestMetrics} from './constant';
import type {MarkOptions} from 'react-native-performance/lib/typescript/performance';
type Target = 'HOME' | 'CHANNEL' | 'THREAD' | undefined;
type MetricName = 'mobile_channel_switch' |
'mobile_team_switch';
@ -22,7 +26,7 @@ class PerformanceMetricsManager {
private lastAppStateIsActive = AppState.currentState === 'active';
constructor() {
AppState.addEventListener('change', (appState) => this.onAppStateChange(appState));
AppState.addEventListener('change', (appState: AppStateStatus) => this.onAppStateChange(appState));
}
private onAppStateChange(appState: AppStateStatus) {
@ -104,6 +108,31 @@ class PerformanceMetricsManager {
performance.clearMarks(metricName);
performance.clearMeasures(measureName);
}
public startTimeToInteraction(options?: MarkOptions) {
performance.mark('tti', options);
}
public measureTimeToInteraction() {
try {
const result = performance.measure('TTI', 'tti');
performance.clearMarks('tti');
performance.clearMeasures('TTI');
logDebug('Time to Interaction', result.duration);
return result;
} catch {
return undefined;
}
}
public collectNetworkRequestData = (name: NetworkRequestMetrics, value: number, {serverUrl, groupLabel}: NetworkRequestDataOtherInfo) => {
this.ensureBatcher(serverUrl).addToBatch({
metric: name,
value,
timestamp: Date.now(),
label: {network_request_group: groupLabel},
});
};
}
export const testExports = {

View file

@ -21,6 +21,16 @@ class Batcher {
this.serverUrl = serverUrl;
}
private async fetchClientPerformanceSetting() {
const database = DatabaseManager.serverDatabases[this.serverUrl]?.database;
if (!database) {
return false;
}
const value = await getConfigValue(database, 'EnableClientMetrics');
return value === 'true';
}
private started() {
return Boolean(this.sendTimeout);
}
@ -46,13 +56,7 @@ class Batcher {
// Empty the batch as soon as possible to avoid race conditions
this.batch = [];
const database = DatabaseManager.serverDatabases[this.serverUrl]?.database;
if (!database) {
return;
}
const clientPerformanceSetting = await getConfigValue(database, 'EnableClientMetrics');
if (clientPerformanceSetting !== 'true') {
if (!await this.fetchClientPerformanceSetting()) {
return;
}

View file

@ -5,6 +5,7 @@ type PerformanceReportMeasure = {
metric: string;
value: number;
timestamp: number;
label?: Record<string, string>;
}
type PerformanceReport = {
@ -21,3 +22,8 @@ type PerformanceReport = {
counters: PerformanceReportMeasure[];
histograms: PerformanceReportMeasure[];
}
type NetworkRequestDataOtherInfo = {
serverUrl: string;
groupLabel: RequestGroupLabel;
}

View file

@ -109,7 +109,7 @@ class WebsocketManager {
}
};
public openAll = async (groupLabel?: string) => {
public openAll = async (groupLabel?: BaseRequestGroupLabel) => {
let queued = 0;
for await (const clientUrl of Object.keys(this.clients)) {
const activeServerUrl = await DatabaseManager.getActiveServerUrl();
@ -148,7 +148,7 @@ class WebsocketManager {
}
};
public initializeClient = async (serverUrl: string, groupLabel = 'reconnection') => {
public initializeClient = async (serverUrl: string, groupLabel: BaseRequestGroupLabel = 'WebSocket Reconnect') => {
const client: WebSocketClient = this.clients[serverUrl];
clearTimeout(this.connectionTimerIDs[serverUrl]);
delete this.connectionTimerIDs[serverUrl];
@ -271,7 +271,7 @@ class WebsocketManager {
}
this.isBackgroundTimerRunning = false;
if (this.netConnected) {
this.openAll('reconnection');
this.openAll('WebSocket Reconnect');
}
return;

View file

@ -390,9 +390,9 @@ describe('Actions.Calls', () => {
const {result} = renderHook(() => useCallsConfig('server1'));
await act(async () => {
await CallsActions.loadConfig('server1', false, 'calls');
await CallsActions.loadConfig('server1', false, 'Server Switch');
});
expect(mockClient.getCallsConfig).toHaveBeenCalledWith('calls');
expect(mockClient.getCallsConfig).toHaveBeenCalledWith('Server Switch');
assert.equal(result.current.DefaultEnabled, true);
assert.equal(result.current.AllowEnableCalls, true);
});

View file

@ -53,7 +53,7 @@ import type {IntlShape} from 'react-intl';
let connection: CallsConnection | null = null;
export const getConnectionForTesting = () => connection;
export const loadConfig = async (serverUrl: string, force = false, groupLabel?: string) => {
export const loadConfig = async (serverUrl: string, force = false, groupLabel?: RequestGroupLabel) => {
const now = Date.now();
const config = getCallsConfig(serverUrl);
@ -77,7 +77,7 @@ export const loadConfig = async (serverUrl: string, force = false, groupLabel?:
}
};
export const loadCalls = async (serverUrl: string, userId: string, groupLabel?: string) => {
export const loadCalls = async (serverUrl: string, userId: string, groupLabel?: RequestGroupLabel) => {
let resp: CallChannelState[] = [];
try {
const client = NetworkManager.getClient(serverUrl);
@ -192,7 +192,7 @@ export const createCallAndAddToIds = (channelId: string, call: CallState, ids?:
return convertedCall;
};
export const loadConfigAndCalls = async (serverUrl: string, userId: string, groupLabel?: string) => {
export const loadConfigAndCalls = async (serverUrl: string, userId: string, groupLabel?: RequestGroupLabel) => {
const res = await checkIsCallsPluginEnabled(serverUrl);
if (res.data) {
loadConfig(serverUrl, true, groupLabel);

View file

@ -7,10 +7,10 @@ import type {RTCIceServer} from 'react-native-webrtc';
export interface ClientCallsMix {
getEnabled: () => Promise<Boolean>;
getCalls: (groupLabel?: string) => Promise<CallChannelState[]>;
getCalls: (groupLabel?: RequestGroupLabel) => Promise<CallChannelState[]>;
getCallForChannel: (channelId: string) => Promise<CallChannelState>;
getCallsConfig: (groupLabel?: string) => Promise<CallsConfig>;
getVersion: (groupLabel?: string) => Promise<CallsVersion>;
getCallsConfig: (groupLabel?: RequestGroupLabel) => Promise<CallsConfig>;
getVersion: (groupLabel?: RequestGroupLabel) => Promise<CallsVersion>;
enableChannelCalls: (channelId: string, enable: boolean) => Promise<CallChannelState>;
endCall: (channelId: string) => Promise<ApiResp>;
genTURNCredentials: () => Promise<RTCIceServer[]>;
@ -38,7 +38,7 @@ const ClientCalls = (superclass: any) => class extends superclass {
}
};
getCalls = async (groupLabel?: string) => {
getCalls = async (groupLabel?: RequestGroupLabel) => {
return this.doFetch(
`${this.getCallsRoute()}/channels?mobilev2=true`,
{method: 'get', groupLabel},
@ -52,14 +52,14 @@ const ClientCalls = (superclass: any) => class extends superclass {
);
};
getCallsConfig = async (groupLabel?: string) => {
getCallsConfig = async (groupLabel?: RequestGroupLabel) => {
return this.doFetch(
`${this.getCallsRoute()}/config`,
{method: 'get', groupLabel},
) as CallsConfig;
};
getVersion = async (groupLabel?: string) => {
getVersion = async (groupLabel?: RequestGroupLabel) => {
try {
return this.doFetch(
`${this.getCallsRoute()}/version`,

View file

@ -157,7 +157,7 @@ export const CallNotification = ({
const onContainerPress = useCallback(async () => {
if (incomingCall.serverUrl !== serverUrl) {
await DatabaseManager.setActiveServerDatabase(incomingCall.serverUrl);
await WebsocketManager.initializeClient(incomingCall.serverUrl, 'calls');
await WebsocketManager.initializeClient(incomingCall.serverUrl, 'Server Switch');
}
switchToChannelById(incomingCall.serverUrl, incomingCall.channelID);
}, [incomingCall, serverUrl]);

View file

@ -466,7 +466,7 @@ const CallScreen = ({
await popTopScreen(Screens.THREAD);
}
await DatabaseManager.setActiveServerDatabase(currentCall.serverUrl);
WebsocketManager.initializeClient(currentCall.serverUrl, 'calls');
WebsocketManager.initializeClient(currentCall.serverUrl, 'Server Switch');
await goToScreen(Screens.THREAD, callThreadOptionTitle, {rootId: currentCall.threadId});
}, [currentCall?.serverUrl, currentCall?.threadId, fromThreadScreen, componentId, callThreadOptionTitle]);

View file

@ -276,7 +276,9 @@ describe('prepareMyChannelsForTeam', () => {
(operator.handleMyChannel as jest.Mock).mockReturnValue(myChannelRecords);
(operator.handleMyChannelSettings as jest.Mock).mockReturnValue(myChannelSettingsRecords);
query = jest.fn().mockResolvedValueOnce(Object.entries(allChannelsForTeam).map((c) => c[1])).mockResolvedValueOnce(Object.entries(allChannelsInfoForTeam).map((i) => i[1]));
const result = await prepareMyChannelsForTeam(operator, teamId, channels, channelMembers, true);
query = jest.fn();
expect(result).toEqual([channelRecords, channelInfoRecords, membershipRecords, myChannelRecords, myChannelSettingsRecords]);
expect(operator.handleChannel).toHaveBeenCalledWith({channels, prepareRecordsOnly: true});
@ -967,7 +969,7 @@ describe('Channel Functions', () => {
Q.where('team_id', teamId),
Q.where('delete_at', Q.eq(0)),
Q.where('type', General.OPEN_CHANNEL),
Q.where('channel_id', Q.notEq('ignoreid')),
Q.where('id', Q.notEq('ignoreid')),
),
Q.sortBy('display_name', Q.asc),
);

View file

@ -81,37 +81,30 @@ export function prepareMissingChannelsForAllTeams(operator: ServerDataOperator,
return prepareChannels(operator, channels, channelInfos, memberships, memberships, isCRTEnabled);
}
export const prepareMyChannelsForTeam = async (operator: ServerDataOperator, teamId: string, channels: Channel[], channelMembers: ChannelMembership[], isCRTEnabled?: boolean) => {
const {database} = operator;
const channelsQuery = queryAllChannelsForTeam(database, teamId);
const allChannelsForTeam = (await channelsQuery.fetch()).
reduce((map: Record<string, ChannelModel>, channel) => {
map[channel.id] = channel;
return map;
}, {});
const channelInfosQuery = queryAllChannelsInfoForTeam(database, teamId);
const allChannelsInfoForTeam = (await channelInfosQuery.fetch()).
reduce((map: Record<string, ChannelInfoModel>, info) => {
map[info.id] = info;
return map;
}, {});
const buildChannelInfos = async (database: Database, channels: Channel[]) => {
const channelInfos: ChannelInfo[] = [];
const memberships = channelMembers.map((cm) => {
return {...cm, id: cm.channel_id};
});
const channelsQuery = await queryAllChannels(database);
const storedChannelsMap = channelsQuery.reduce<Record<string, ChannelModel>>((map, channel) => {
map[channel.id] = channel;
return map;
}, {});
const channelInfosQuery = await queryAllChannelsInfo(database);
const storedChannelInfosMap = channelInfosQuery.reduce<Record<string, ChannelInfoModel>>((map, info) => {
map[info.id] = info;
return map;
}, {});
for (const c of channels) {
const storedChannel = allChannelsForTeam[c.id];
const storedChannel = storedChannelsMap[c.id];
let storedInfo: ChannelInfoModel | undefined;
let member_count = 0;
let guest_count = 0;
let pinned_post_count = 0;
let files_count = 0;
if (storedChannel) {
storedInfo = allChannelsInfoForTeam[c.id];
storedInfo = storedChannelInfosMap[c.id];
if (storedInfo) {
member_count = storedInfo.memberCount;
guest_count = storedInfo.guestCount;
@ -131,6 +124,28 @@ export const prepareMyChannelsForTeam = async (operator: ServerDataOperator, tea
});
}
return channelInfos;
};
export const prepareAllMyChannels = async (operator: ServerDataOperator, channels: Channel[], channelMemberships: ChannelMembership[], isCRTEnabled?: boolean) => {
const {database} = operator;
const fetchedChannelIds = new Set(channels.map((c) => c.id));
const memberships = channelMemberships.filter((m) => fetchedChannelIds.has(m.channel_id)).map((m) => ({...m, id: m.channel_id}));
const channelInfos = await buildChannelInfos(database, channels);
return prepareChannels(operator, channels, channelInfos, channelMemberships, memberships, isCRTEnabled);
};
export const prepareMyChannelsForTeam = async (operator: ServerDataOperator, teamId: string, channels: Channel[], channelMembers: ChannelMembership[], isCRTEnabled?: boolean) => {
const {database} = operator;
const channelInfos = await buildChannelInfos(database, channels);
const memberships = channelMembers.map((cm) => {
return {...cm, id: cm.channel_id};
});
return prepareChannels(operator, channels, channelInfos, channelMembers, memberships, isCRTEnabled);
};
@ -316,7 +331,7 @@ export const getDefaultChannelForTeam = async (database: Database, teamId: strin
];
if (ignoreId) {
clauses.push(Q.where('channel_id', Q.notEq(ignoreId)));
clauses.push(Q.where('id', Q.notEq(ignoreId)));
}
const myChannels = await database.get<ChannelModel>(CHANNEL).query(

View file

@ -5,10 +5,10 @@ import {MM_TABLES} from '@constants/database';
import DatabaseManager from '@database/manager';
import {prepareCategoriesAndCategoriesChannels} from './categories';
import {prepareDeleteChannel, prepareMyChannelsForTeam} from './channel';
import {prepareAllMyChannels, prepareDeleteChannel, queryAllChannels, queryChannelsById} from './channel';
import {prepareMyPreferences} from './preference';
import {resetLastFullSync} from './system';
import {prepareDeleteTeam, prepareMyTeams} from './team';
import {prepareDeleteTeam, prepareMyTeams, queryMyTeams, queryTeamsById} from './team';
import {prepareUsers} from './user';
import type {MyChannelsRequest} from '@actions/remote/channel';
@ -18,13 +18,10 @@ import type {MyUserRequest} from '@actions/remote/user';
import type ServerDataOperator from '@database/operator/server_data_operator';
import type {Model} from '@nozbe/watermelondb';
import type ChannelModel from '@typings/database/models/servers/channel';
import type TeamModel from '@typings/database/models/servers/team';
type PrepareModelsArgs = {
operator: ServerDataOperator;
initialTeamId?: string;
removeTeams?: TeamModel[];
removeChannels?: ChannelModel[];
teamData?: MyTeamsRequest;
chData?: MyChannelsRequest;
prefData?: MyPreferencesRequest;
@ -32,6 +29,13 @@ type PrepareModelsArgs = {
isCRTEnabled?: boolean;
}
type PrepareModelsForDeletionArgs = {
operator: ServerDataOperator;
initialTeamId?: string;
teamData?: MyTeamsRequest;
chData?: MyChannelsRequest;
}
const {
CHANNEL_BOOKMARK,
POST,
@ -44,21 +48,48 @@ const {
MY_CHANNEL,
} = MM_TABLES.SERVER;
export async function prepareModels({operator, initialTeamId, removeTeams, removeChannels, teamData, chData, prefData, meData, isCRTEnabled}: PrepareModelsArgs): Promise<Array<Promise<Model[]>>> {
export async function prepareEntryModelsForDeletion({operator, teamData, chData}: PrepareModelsForDeletionArgs): Promise<Array<Promise<Model[]>>> {
const modelPromises: Array<Promise<Model[]>> = [];
const {database} = operator;
let teamIdsToDelete: Set<string>|undefined;
if (teamData?.teams?.length && teamData.memberships?.length) {
const myTeams = await queryMyTeams(database).fetch();
const joinedTeams = new Set(teamData.memberships?.filter((m) => m.delete_at === 0).map((m) => m.team_id));
const myTeamsToDelete = myTeams.filter((m) => !joinedTeams.has(m.id));
teamIdsToDelete = new Set(myTeamsToDelete.map((m) => m.id));
const removeTeams = await queryTeamsById(database, Array.from(teamIdsToDelete)).fetch();
if (removeTeams?.length) {
removeTeams.forEach((team) => {
modelPromises.push(prepareDeleteTeam(team));
});
}
if (removeChannels?.length) {
removeChannels.forEach((channel) => {
modelPromises.push(prepareDeleteChannel(channel));
});
if (chData?.channels?.length && chData.memberships?.length) {
const {channels} = chData;
const fetchedChannelIds = new Set(channels.map((c) => c.id));
const channelsQuery = await queryAllChannels(database);
const storedChannelsMap = channelsQuery.reduce<Record<string, ChannelModel>>((map, channel) => {
map[channel.id] = channel;
return map;
}, {});
const removeChannelIds: string[] = Object.keys(storedChannelsMap).filter((id) => !fetchedChannelIds.has(id) && !teamIdsToDelete?.has(storedChannelsMap[id]?.teamId));
let removeChannels: ChannelModel[]|undefined;
if (removeChannelIds?.length) {
removeChannels = await queryChannelsById(database, removeChannelIds).fetch();
removeChannels.forEach((c) => {
modelPromises.push(prepareDeleteChannel(c));
});
}
}
return modelPromises;
}
export async function prepareEntryModels({operator, teamData, chData, prefData, meData, isCRTEnabled}: PrepareModelsArgs): Promise<Array<Promise<Model[]>>> {
const modelPromises: Array<Promise<Model[]>> = [];
if (teamData?.teams?.length && teamData.memberships?.length) {
modelPromises.push(...prepareMyTeams(operator, teamData.teams, teamData.memberships));
}
@ -68,9 +99,8 @@ export async function prepareModels({operator, initialTeamId, removeTeams, remov
}
if (chData?.channels?.length && chData.memberships?.length) {
if (initialTeamId) {
modelPromises.push(...await prepareMyChannelsForTeam(operator, initialTeamId, chData.channels, chData.memberships, isCRTEnabled));
}
const {channels, memberships} = chData;
modelPromises.push(...await prepareAllMyChannels(operator, channels, memberships, isCRTEnabled));
}
if (prefData?.preferences?.length) {

View file

@ -109,7 +109,7 @@ const ChannelListScreen = (props: ChannelProps) => {
}
}
return false;
}, [intl]);
}, [intl, navigation]);
const animated = useAnimatedStyle(() => {
if (!isFocused) {
@ -136,7 +136,7 @@ const ChannelListScreen = (props: ChannelProps) => {
if (!props.hasTeams) {
resetToTeams();
}
}, [Boolean(props.hasTeams)]);
}, [props.hasTeams]);
useEffect(() => {
const back = BackHandler.addEventListener('hardwareBackPress', handleBackPress);
@ -172,6 +172,7 @@ const ChannelListScreen = (props: ChannelProps) => {
useEffect(() => {
PerformanceMetricsManager.finishLoad('HOME', serverUrl);
PerformanceMetricsManager.measureTimeToInteraction();
}, []);
return (
@ -201,7 +202,7 @@ const ChannelListScreen = (props: ChannelProps) => {
moreThanOneTeam={props.hasMoreThanOneTeam}
hasChannels={props.hasChannels}
/>
{isTablet &&
{isTablet && props.hasChannels &&
<AdditionalTabletView/>
}
{props.showIncomingCalls && !isTablet &&

View file

@ -179,7 +179,7 @@ const ServerItem = ({
setBadge({isUnread, mentions});
};
const logoutServer = async () => {
const logoutServer = useCallback(async () => {
Navigation.updateProps(Screens.HOME, {extra: undefined});
await logout(server.url);
@ -188,14 +188,14 @@ const ServerItem = ({
} else {
DeviceEventEmitter.emit(Events.SWIPEABLE, '');
}
};
}, [isActive, server.url]);
const removeServer = async () => {
const removeServer = useCallback(async () => {
const skipLogoutFromServer = server.lastActiveAt === 0;
await dismissBottomSheet();
Navigation.updateProps(Screens.HOME, {extra: undefined});
await logout(server.url, skipLogoutFromServer, true);
};
}, [server.lastActiveAt, server.url]);
const startTutorial = () => {
viewRef.current?.measureInWindow((x, y, w, h) => {
@ -222,7 +222,7 @@ const ServerItem = ({
setShowTutorial(true);
});
}
}, [showTutorial]);
}, [highlight, isTablet, tutorialWatched]);
useLayoutEffect(() => {
if (showTutorial && !tutorialShown.current) {
@ -239,7 +239,7 @@ const ServerItem = ({
}
return style;
}, [isActive]);
}, [isActive, styles.active, styles.container]);
const serverStyle = useMemo(() => {
const style: StyleProp<ViewStyle> = [styles.row];
@ -248,7 +248,7 @@ const ServerItem = ({
}
return style;
}, [server.lastActiveAt]);
}, [server.lastActiveAt, styles.offline, styles.row]);
const handleLogin = useCallback(async () => {
swipeable.current?.close();
@ -275,7 +275,7 @@ const ServerItem = ({
canReceiveNotifications(server.url, result.canReceiveNotifications as string, intl);
loginToServer(theme, server.url, displayName, data.config!, data.license!);
}, [server, theme, intl]);
}, [server.url, intl, theme, displayName]);
const handleDismissTutorial = useCallback(() => {
swipeable.current?.close();
@ -286,15 +286,15 @@ const ServerItem = ({
const handleEdit = useCallback(() => {
DeviceEventEmitter.emit(Events.SWIPEABLE, '');
editServer(theme, server);
}, [server]);
}, [server, theme]);
const handleLogout = useCallback(async () => {
alertServerLogout(server.displayName, logoutServer, intl);
}, [isActive, intl, server]);
}, [server.displayName, logoutServer, intl]);
const handleRemove = useCallback(() => {
alertServerRemove(server.displayName, removeServer, intl);
}, [isActive, server, intl]);
}, [server.displayName, removeServer, intl]);
const handleShowTutorial = useCallback(() => {
swipeable.current?.openRight();
@ -311,12 +311,12 @@ const ServerItem = ({
await dismissBottomSheet();
Navigation.updateProps(Screens.HOME, {extra: undefined});
DatabaseManager.setActiveServerDatabase(server.url);
WebsocketManager.initializeClient(server.url, 'entry');
WebsocketManager.initializeClient(server.url, 'Server Switch');
return;
}
handleLogin();
}, [server, isActive, theme, intl]);
}, [isActive, server.lastActiveAt, server.url, handleLogin]);
const onSwipeableWillOpen = useCallback(() => {
DeviceEventEmitter.emit(Events.SWIPEABLE, server.url);

View file

@ -127,7 +127,7 @@ describe('handleDeepLink', () => {
const result = await handleDeepLink('https://existingserver.com/team/channels/town-square');
expect(dismissAllModalsAndPopToRoot).toHaveBeenCalled();
expect(DatabaseManager.setActiveServerDatabase).toHaveBeenCalledWith('https://existingserver.com');
expect(WebsocketManager.initializeClient).toHaveBeenCalledWith('https://existingserver.com', 'deeplink');
expect(WebsocketManager.initializeClient).toHaveBeenCalledWith('https://existingserver.com', 'DeepLink');
expect(result).toEqual({error: false});
});

View file

@ -61,7 +61,7 @@ export async function handleDeepLink(deepLinkUrl: string, intlShape?: IntlShape,
if (existingServerUrl !== currentServerUrl && NavigationStore.getVisibleScreen()) {
await dismissAllModalsAndPopToRoot();
DatabaseManager.setActiveServerDatabase(existingServerUrl);
WebsocketManager.initializeClient(existingServerUrl, 'deeplink');
WebsocketManager.initializeClient(existingServerUrl, 'DeepLink');
await NavigationStore.waitUntilScreenHasLoaded(Screens.HOME);
}

View file

@ -41,6 +41,13 @@ jest.mock('@utils/log', () => ({
jest.mock('@mattermost/rnutils', () => ({
getRealFilePath: jest.fn(),
isRunningInSplitView: jest.fn().mockReturnValue({isSplit: false, isTablet: false}),
getConstants: jest.fn().mockReturnValue({
appGroupIdentifier: 'group.mattermost.rnbeta',
appGroupSharedDirectory: {
sharedDirectory: '',
databasePath: '',
},
}),
}));
describe('FilePickerUtil', () => {

View file

@ -181,6 +181,11 @@ lane :configure do
json['ShowOnboarding'] = true
end
# Configure Show Onboarding
if ENV['COLLECT_NETWORK_METRICS'] == 'true'
json['CollectNetworkMetrics'] = true
end
# Configure the auth schemes
auth_scheme = ENV['APP_AUTH_SCHEME'].to_s.empty? ? 'mmauth' : ENV['APP_AUTH_SCHEME']
auth_scheme_dev = ENV['APP_AUTH_SCHEME_BETA'].to_s.empty? ? 'mmauthbeta' : ENV['APP_AUTH_SCHEME_BETA']

View file

@ -3,13 +3,16 @@
type logLevel = 'ERROR' | 'WARNING' | 'INFO';
type BaseRequestGroupLabel = 'Login' | 'Cold Start' | 'Notification' | 'DeepLink' | 'WebSocket Reconnect' | 'Server Switch';
type RequestGroupLabel = BaseRequestGroupLabel | `${BaseRequestGroupLabel} Deferred`;
type ClientOptions = {
body?: any;
method?: string;
noRetry?: boolean;
timeoutInterval?: number;
headers?: Record<string, any>;
groupLabel?: string;
groupLabel?: RequestGroupLabel;
};
type ClientErrorIntl =