Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions android/app/proguard-rules.pro
Original file line number Diff line number Diff line change
Expand Up @@ -51,3 +51,9 @@

# https://shopify.github.io/react-native-skia/docs/getting-started/installation/#proguard
-keep class com.shopify.reactnative.skia.** { *; }

# Strip verbose and debug log calls from release builds
-assumenosideeffects class android.util.Log {
public static int d(...);
public static int v(...);
}
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@
import androidx.core.graphics.drawable.IconCompat;
import androidx.versionedparcelable.ParcelUtils;

import com.expensify.chat.BuildConfig;
import com.expensify.chat.R;
import com.expensify.chat.shortcutManagerModule.ShortcutManagerUtils;
import com.expensify.chat.customairshipextender.PayloadHandler;
Expand Down Expand Up @@ -103,7 +104,7 @@ public CustomNotificationProvider(@NonNull Context context, @NonNull AirshipConf
protected NotificationCompat.Builder onExtendBuilder(@NonNull Context context, @NonNull NotificationCompat.Builder builder, @NonNull NotificationArguments arguments) {
super.onExtendBuilder(context, builder, arguments);
PushMessage message = arguments.getMessage();
Log.d(TAG, "buildNotification: " + message.toString());
if (BuildConfig.DEBUG) Log.d(TAG, "buildNotification: " + message.toString());

// Improve notification delivery by categorizing as a time-critical message
builder.setCategory(CATEGORY_MESSAGE);
Expand All @@ -127,20 +128,20 @@ protected NotificationCompat.Builder onExtendBuilder(@NonNull Context context, @
try {
String rawPayload = message.getExtra(PAYLOAD_KEY);
if (rawPayload == null) {
Log.d(TAG, "Failed to parse payload - payload is empty. SendID=" + message.getSendId());
if (BuildConfig.DEBUG) Log.d(TAG, "Failed to parse payload - payload is empty. SendID=" + message.getSendId());
return builder;
}

PayloadHandler handler = new PayloadHandler();
String processedPayload = handler.processPayload(rawPayload);
JsonMap payload = JsonValue.parseString(processedPayload).optMap();
if (!payload.containsKey(ONYX_DATA_KEY)) {
Log.d(TAG, "Failed to process payload - no onyx data. SendID=" + message.getSendId());
if (BuildConfig.DEBUG) Log.d(TAG, "Failed to process payload - no onyx data. SendID=" + message.getSendId());
return builder;
}

Objects.requireNonNull(payload.get(ONYX_DATA_KEY)).isNull();
Log.d(TAG, "payload contains onxyData");
if (BuildConfig.DEBUG) Log.d(TAG, "payload contains onxyData");
String alert = message.getExtra(PushMessage.EXTRA_ALERT);
applyMessageStyle(context, builder, payload, arguments.getNotificationId(), alert);
} catch (Exception e) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import android.app.Activity;
import android.os.Handler;
import android.util.Log;
import com.group_ib.react.BuildConfig;
import androidx.annotation.NonNull;
import main.java.com.group_ib.react.session.SessionEvents;
import main.java.com.group_ib.react.session.SessionListenerImpl;
Expand Down Expand Up @@ -66,7 +67,9 @@ public void initialize() {
super.initialize();
final Activity activity = getCurrentActivity();
try {
MobileSdk.enableDebugLogs();
if (BuildConfig.DEBUG) {
MobileSdk.enableDebugLogs();
}
PackageCollectionModule.init();
sdk = MobileSdk.init(activity != null ? activity : context);
sdk.setSessionListener(sessionListener);
Expand Down
13 changes: 9 additions & 4 deletions src/libs/API/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import {push as pushToSequentialQueue, waitForIdle as waitForSequentialQueueIdle
import {getIsOffline} from '@libs/NetworkState';
import Pusher from '@libs/Pusher';
import {addMiddleware, processWithMiddleware} from '@libs/Request';
import sanitizeLogParams from '@libs/sanitizeLogParams';
import {getAll, getLength as getPersistedRequestsLength} from '@userActions/PersistedRequests';
import CONST from '@src/CONST';
import type OnyxRequest from '@src/types/onyx/Request';
Expand Down Expand Up @@ -61,6 +62,10 @@ addMiddleware(FraudMonitoring);
// incrementing number would collide across tabs.
let requestIndex = Date.now();

function buildLogParams(command: string, params: Record<string, unknown>): Record<string, unknown> {
return {command, ...Object.fromEntries(Object.entries(sanitizeLogParams(params)))};
}

/**
* Prepare the request to be sent. Bind data together with request metadata and apply optimistic Onyx data.
*/
Expand Down Expand Up @@ -168,7 +173,7 @@ function write<TCommand extends WriteCommand, TKey extends OnyxKey>(
onyxData: OnyxData<TKey> = {},
conflictResolver: RequestConflictResolver<TKey> = {},
): Promise<void | Response<TKey>> {
Log.info('[API] Called API write', false, {command, ...apiCommandParameters});
Log.info('[API] Called API write', false, buildLogParams(command, apiCommandParameters ?? {}));
const request = prepareRequest(command, CONST.API_REQUEST_TYPE.WRITE, apiCommandParameters, onyxData, conflictResolver);

return processRequest(request, CONST.API_REQUEST_TYPE.WRITE);
Expand Down Expand Up @@ -220,7 +225,7 @@ function makeRequestWithSideEffects<TCommand extends SideEffectRequestCommand, T
apiCommandParameters: ApiRequestCommandParameters[TCommand],
onyxData: OnyxData<TKey> = {},
): Promise<void | Response<TKey>> {
Log.info('[API] Called API makeRequestWithSideEffects', false, {command, ...apiCommandParameters});
Log.info('[API] Called API makeRequestWithSideEffects', false, buildLogParams(command, apiCommandParameters ?? {}));
const request = prepareRequest(command, CONST.API_REQUEST_TYPE.MAKE_REQUEST_WITH_SIDE_EFFECTS, apiCommandParameters, onyxData);

// Return a promise containing the response from HTTPS
Expand All @@ -246,7 +251,7 @@ function read<TCommand extends ReadCommand>(command: TCommand, apiCommandParamet
function read<TCommand extends ReadCommand, TKey extends OnyxKey>(command: TCommand, apiCommandParameters: ApiRequestCommandParameters[TCommand], onyxData: OnyxData<TKey>): void;

function read<TCommand extends ReadCommand, TKey extends OnyxKey>(command: TCommand, apiCommandParameters: ApiRequestCommandParameters[TCommand], onyxData: OnyxData<TKey> = {}): void {
Log.info('[API] Called API.read', false, {command, ...apiCommandParameters});
Log.info('[API] Called API.read', false, buildLogParams(command, apiCommandParameters ?? {}));

// Apply optimistic updates of read requests immediately
const request = prepareRequest(command, CONST.API_REQUEST_TYPE.READ, apiCommandParameters, onyxData);
Expand Down Expand Up @@ -290,7 +295,7 @@ function paginate<TRequestType extends ApiRequestType, TCommand extends CommandO
config: PaginationConfig,
conflictResolver: RequestConflictResolver<TKey> = {},
): Promise<Response<TKey> | void> | void {
Log.info('[API] Called API.paginate', false, {command, ...apiCommandParameters});
Log.info('[API] Called API.paginate', false, buildLogParams(command, apiCommandParameters ?? {}));
const request: PaginatedRequest<TKey> = {
...prepareRequest(command, type, apiCommandParameters, onyxData, conflictResolver),
...config,
Expand Down
9 changes: 5 additions & 4 deletions src/libs/Middleware/Logging.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import type {OnyxKey} from 'react-native-onyx';
import {SIDE_EFFECT_REQUEST_COMMANDS} from '@libs/API/types';
import type HttpsError from '@libs/Errors/HttpsError';
import Log from '@libs/Log';
import sanitizeLogParams from '@libs/sanitizeLogParams';
import CONST from '@src/CONST';
import type Request from '@src/types/onyx/Request';
import type Response from '@src/types/onyx/Response';
Expand Down Expand Up @@ -67,11 +68,11 @@ function logRequestDetails<TKey extends OnyxKey>(message: string, request: Reque
* requests because they contain sensitive information.
*/
if (request.command !== 'AuthenticatePusher') {
extraData.request = {
extraData.request = sanitizeLogParams({
...request,
data: serializeLoggingData(request.data),
};
extraData.response = response;
});
extraData.response = sanitizeLogParams(response);
}

Log.info(message, false, logParams, false, extraData);
Expand All @@ -90,7 +91,7 @@ const Logging: Middleware = (response, request) => {
message: error.message,
status: error.status,
title: error.title,
request,
request: sanitizeLogParams(request),
};

// If the command that failed is Log it's possible that the next call to Log may also fail.
Expand Down
3 changes: 2 additions & 1 deletion src/libs/Navigation/NavigationRoot.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import useThemePreference from '@hooks/useThemePreference';
import FS from '@libs/Fullstory';
import Log from '@libs/Log';
import {setupNavigationFocusReturn, teardownNavigationFocusReturn} from '@libs/NavigationFocusReturn';
import {sanitizeUrlForLogging} from '@libs/sanitizeLogParams';
import shouldOpenLastVisitedPath from '@libs/shouldOpenLastVisitedPath';
import {getPathFromURL} from '@libs/Url';
import {getBaseTheme} from '@styles/theme/utils';
Expand Down Expand Up @@ -72,7 +73,7 @@ function parseAndLogRoute(state: NavigationState) {
if (currentPath.includes('/transition')) {
Log.info('Navigating from transition link from OldDot using short lived authToken');
} else {
Log.info('Navigating to route', false, {path: currentPath});
Log.info('Navigating to route', false, {path: sanitizeUrlForLogging(currentPath)});
}

Navigation.setIsNavigationReady();
Expand Down
3 changes: 2 additions & 1 deletion src/libs/actions/App.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import willRouteNavigateToRHP from '@libs/Navigation/helpers/willRouteNavigateTo
import Navigation, {navigationRef} from '@libs/Navigation/Navigation';
import isTrackOnboardingChoice from '@libs/OnboardingUtils';
import {isPublicRoom, isValidReport} from '@libs/ReportUtils';
import {sanitizeUrlForLogging} from '@libs/sanitizeLogParams';
import {isLoggingInAsNewUser as isLoggingInAsNewUserSessionUtils} from '@libs/SessionUtils';
import {clearSoundAssetsCache} from '@libs/Sound';
import {cancelAllSpans, endSpan, getSpan, startSpan} from '@libs/telemetry/activeSpans';
Expand Down Expand Up @@ -246,7 +247,7 @@ function saveCurrentPathBeforeBackground() {
const currentPath = getPathFromState(currentState);

if (currentPath) {
Log.info('Saving current path before background', false, {currentPath});
Log.info('Saving current path before background', false, {currentPath: sanitizeUrlForLogging(currentPath)});
updateLastVisitedPath(currentPath);
}
} catch (error) {
Expand Down
5 changes: 3 additions & 2 deletions src/libs/actions/PersistedRequests.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import {deepEqual} from 'fast-equals';
import type {OnyxKey} from 'react-native-onyx';
import Onyx from 'react-native-onyx';
import Log from '@libs/Log';
import sanitizeLogParams from '@libs/sanitizeLogParams';
import ONYXKEYS from '@src/ONYXKEYS';
import type {Request} from '@src/types/onyx';
import type {AnyRequest} from '@src/types/onyx/Request';
Expand Down Expand Up @@ -316,7 +317,7 @@ function deleteRequestsByIndices(indices: number[]): Promise<void> {
function update<TKey extends OnyxKey>(oldRequestIndex: number, newRequest: Request<TKey>): Promise<void> {
const requests = [...persistedRequests];
const oldRequest = requests.at(oldRequestIndex);
Log.info('[PersistedRequests] Updating a request', false, {oldRequest, newRequest, oldRequestIndex});
Log.info('[PersistedRequests] Updating a request', false, {oldRequest: sanitizeLogParams(oldRequest), newRequest: sanitizeLogParams(newRequest), oldRequestIndex});
requests.splice(oldRequestIndex, 1, newRequest as AnyRequest);
persistedRequests = requests;
if (newRequest.requestID != null) {
Expand All @@ -338,7 +339,7 @@ function shouldPersistOngoingRequest(request: AnyRequest | null): boolean {
}

function updateOngoingRequest<TKey extends OnyxKey>(newRequest: Request<TKey>) {
Log.info('[PersistedRequests] Updating the ongoing request', false, {ongoingRequest, newRequest});
Log.info('[PersistedRequests] Updating the ongoing request', false, {ongoingRequest: sanitizeLogParams(ongoingRequest), newRequest: sanitizeLogParams(newRequest)});
ongoingRequest = newRequest as AnyRequest;

if (shouldPersistOngoingRequest(ongoingRequest)) {
Expand Down
4 changes: 3 additions & 1 deletion src/libs/asyncOpenURL/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import {Linking} from 'react-native';
import Log from '@libs/Log';
import {sanitizeUrlForLogging} from '@libs/sanitizeLogParams';
import type AsyncOpenURL from './types';

const asyncOpenURL: AsyncOpenURL = (promise, url) => {
Expand All @@ -12,7 +13,8 @@ const asyncOpenURL: AsyncOpenURL = (promise, url) => {
Linking.openURL(typeof url === 'string' ? url : url(params));
})
.catch(() => {
Log.warn('[asyncOpenURL] error occurred while opening URL', {url});
const safeUrl = typeof url === 'string' ? sanitizeUrlForLogging(url) : '[function]';
Log.warn('[asyncOpenURL] error occurred while opening URL', {url: safeUrl});
});
};

Expand Down
59 changes: 59 additions & 0 deletions src/libs/sanitizeLogParams.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
const SENSITIVE_KEYS = new Set([
'authToken',
'encryptedAuthToken',
'password',
'partnerUserSecret',
'partnerPassword',
'twoFactorAuthCode',
'idToken',
'token',
'validateCode',
'autoGeneratedPassword',
'autoGeneratedLogin',
'oldDotCurrentAuthToken',
'oldDotCurrentEncryptedAuthToken',
'oldDotAutoGeneratedPassword',
]);

const REDACTED = '<redacted>';

function sanitizeLogParams<T>(params: T, depth = 0): T {
if (depth > 5 || params == null || typeof params !== 'object') {
return params;
}

if (Array.isArray(params)) {
return (params as unknown[]).map((item) => sanitizeLogParams(item, depth + 1)) as unknown as T;
}

const sanitized: Record<string, unknown> = {};
for (const [key, value] of Object.entries(params)) {
if (SENSITIVE_KEYS.has(key)) {
sanitized[key] = REDACTED;
} else if (value != null && typeof value === 'object') {
sanitized[key] = sanitizeLogParams(value, depth + 1);
} else {
sanitized[key] = value;
}
}

return sanitized as T;
}

/**
* Redact sensitive segments from URL paths before logging.
* Handles /v/:accountID/:validateCode and /u/:accountID/:validateCode patterns,
* and strips query parameters that may contain tokens.
*/
function sanitizeUrlForLogging(url: string): string {
return (
url
// Redact validateCode in /v/:accountID/:validateCode and /u/:accountID/:validateCode
.replace(/\/([vu])\/(\d+)\/[^/?#]+/, '/$1/$2/<redacted>')
// Strip query string (may contain authToken, token, etc.)
.replace(/\?.*$/, '?<redacted>')
);
}

export default sanitizeLogParams;
export {sanitizeUrlForLogging};
Loading