packages/@fbcmobile-ui/Logging/UserActionLogger.js (319 lines of code) (raw):
/**
* Copyright (c) Facebook, Inc. and its affiliates.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow strict-local
* @format
*/
'use strict';
import type {LocationProviderInfo, PowerState} from 'react-native-device-info';
import * as beaver from 'beaver-logger';
import AsyncStorage from '@react-native-community/async-storage';
import DeviceInfo from 'react-native-device-info';
import NetInfo from '@react-native-community/netinfo';
export type MetricData = {|
key: string,
metric: number,
|};
export type ErrorData = {|
key: string,
errorMessage: string,
|};
export type EventData = {|
key: string,
logMessage?: ?string,
|};
export type QueryData = {|
key: string,
metric: number,
operationKind?: ?string,
queryName?: ?string,
correlationId?: ?string,
|};
export type BugData = {|
key: string,
details: ?string,
screenShot: ?string, // base-64 encoded image
|};
export type FetchParams = {
method: 'POST',
headers: {
accept: string,
origin: string,
host: string,
'x-auth-organization': ?string,
'cache-control': string,
'content-type': string,
'upgrade-insecure-requests': string,
'x-csrf-token': ?string,
},
credentials: string,
body?: ?string,
};
type LogData = {
eventType: string,
error?: ?string,
metricValue?: ?number,
tenant?: ?string,
logMessage?: ?string,
screenShot?: ?string,
operationKind?: ?string,
queryName?: ?string,
correlationId?: ?string,
};
const KEYS = {
LOG_EVENTS: 'log_events',
};
class UserActionLogger {
static logger: UserActionLogger;
_asyncFieldsLoaded: boolean;
// DeviceInfo fields
_apiLevel: number;
_applicationName: string;
_availableLocationProviders: LocationProviderInfo;
_brand: string;
_buildNumber: string;
_bundleId: string;
_carrier: string;
_codeName: string;
_device: string;
_deviceId: string;
_display: string;
_devicename: string;
_firstInstallTime: number;
_freeDiskStorage: number;
_hardware: string;
_host: string;
_incremental: string;
_installerPackageName: string;
_installReferrer: string;
_lastUpdateTime: number;
_manufacturer: string;
_model: string;
_product: string;
_readableVersion: string;
_systemVersion: string;
_buildId: string;
_tags: string;
_type: string;
_userAgent: string;
_version: string;
_isEmulator: boolean;
_isTablet: string;
_supportedAbis: string[];
_baseUrl: string;
_fetchParams: FetchParams;
_customPayload: {[string]: string};
_beaverLogger;
constructor() {
// beaver-logger expects a value for window.location.protocol but
// React Native doesn't polyfill window.location. We'll do it here.
if (typeof window != 'undefined' && !window.location) {
window.location = {protocol: 'http'};
}
this._asyncFieldsLoaded = false;
// Listen for changes to network status. If the network is available again
// try to flush all the stored log events from disk.
let isConnected = true;
NetInfo.addEventListener(state => {
if (state.isConnected != isConnected) {
isConnected = state.isConnected;
if (isConnected) {
this._flushEventsFromDisk();
}
}
});
this._applicationName = DeviceInfo.getApplicationName();
this._brand = DeviceInfo.getBrand();
this._buildNumber = DeviceInfo.getBuildNumber();
this._bundleId = DeviceInfo.getBundleId();
this._deviceId = DeviceInfo.getDeviceId();
this._model = DeviceInfo.getModel();
this._readableVersion = DeviceInfo.getReadableVersion();
this._systemVersion = DeviceInfo.getSystemVersion();
this._version = DeviceInfo.getVersion();
this._isTablet = DeviceInfo.isTablet() ? 'true' : 'false';
this._beaverLogger = beaver.Logger({
// Url to send logs to - is customized in transport function
url: '',
// Prefix to prepend to all events
prefix: 'mobile_app',
logLevel: beaver.LOG_LEVEL.INFO,
// Interval to flush logs to server - 5 seconds
flushInterval: 5 * 1000,
transport: async ({_url, _method, json}): Promise<any> => {
const eventData = json.events.map(e => {
const {event, level, payload} = e;
const {timestamp, ...rest} = payload;
return {
event: {name: event},
level,
ts: timestamp / 1000,
...rest,
};
});
if (!this._baseUrl) {
return;
}
this._fetchParams.body = JSON.stringify(eventData);
fetch(this._baseUrl, this._fetchParams)
.then(_response => {
// noop - the service returns an empty response when logs are received
})
.catch((_error: Error) => {
// Store failed log events to disk so they don't get lost.
this._setLogEvent(JSON.stringify(eventData));
});
},
});
}
_flushEventsFromDisk() {
this._getLogEvents().then((events: Array<string>) => {
const eventsThatFailedToUpload = [];
events.forEach(event => {
this._fetchParams.body = event;
fetch(this._baseUrl, this._fetchParams)
.then(_response => {
// noop - the service returns an empty response when logs are received
})
.catch((_error: Error) => {
// store events that failed to upload so we can try to upload them again later
eventsThatFailedToUpload.push(event);
});
});
this._setLogEvent(null); // clear out the disk cache
if (eventsThatFailedToUpload.length > 0) {
this._setLogEvents(eventsThatFailedToUpload);
}
});
}
/**
* Log Events functions
* Events are expected to be stringified before calling these functions.
* Passing a null value clears the log event data from local storage.
*/
async _setLogEvent(logEvent: ?LogData): Promise<void> {
if (logEvent) {
const value = await AsyncStorage.getItem(KEYS.LOG_EVENTS);
const logEvents: Array<LogData> = value ? JSON.parse(value) : [];
logEvents.push(logEvent);
return await AsyncStorage.setItem(
KEYS.LOG_EVENTS,
JSON.stringify(logEvents),
);
}
AsyncStorage.removeItem(KEYS.LOG_EVENTS);
}
async _setLogEvents(logEvents: Array<string>): Promise<void> {
if (logEvents.length > 0) {
AsyncStorage.setItem(KEYS.LOG_EVENTS, logEvents);
}
}
async _getLogEvents(): Promise<Array<string>> {
const value = await AsyncStorage.getItem(KEYS.LOG_EVENTS);
const logEvents: Array<string> = value ? JSON.parse(value) : [];
return logEvents;
}
/*
* Example logging a metric:
* logMetric({key: METRIC.MY_METRIC_MS, metric: 12345});
*/
static logMetric(data: MetricData) {
UserActionLogger.getInstance()._logEvent({
eventType: data.key,
metricValue: data.metric,
});
}
/*
* Example logging an error:
* logError({key: ERROR.MY_ERROR, errorMessage: 'really bad error'});
*/
static logError(data: ErrorData) {
UserActionLogger.getInstance()._logEvent({
eventType: data.key,
error: data.errorMessage,
});
}
/*
* Example logging an event:
* logEvent({
* key: EVENT.MY_EVENT,
* logMessage: 'Here is the event message',
* });
*/
static logEvent(data: EventData) {
UserActionLogger.getInstance()._logEvent({
eventType: data.key,
logMessage: data.logMessage,
});
}
/*
* Example logging a query:
* logQuery({
* key: METRIC.MY_QUERY_METRIC_MS,
* metric: 12345,
* operationKind: 'mutation',
* queryName: 'MySpecialCreateMutation',
* correlationId: 'abcdef1234567890',
* });
*/
static logQuery(data: QueryData) {
UserActionLogger.getInstance()._logEvent({
eventType: data.key,
metricValue: data.metric,
operationKind: data.operationKind,
queryName: data.queryName,
correlationId: data.correlationId,
});
}
/*
* Example logging a bug report:
* logBug({
* key: EVENT.BUG_REPORT,
* details: 'There is a bug in this screen',
* screenShot: 'data:image/png;base64,ABCDEFG123456...',
* });
*/
static async logBug(data: BugData) {
// Get extra device data if this is a bug report
const usedMemory: number = await DeviceInfo.getUsedMemory();
const isAirplaneMode: boolean = await DeviceInfo.isAirplaneMode();
const isBatteryCharging: boolean = await DeviceInfo.isBatteryCharging();
const isLocationEnabled: boolean = await DeviceInfo.isLocationEnabled();
const powerState: PowerState | {} = await DeviceInfo.getPowerState();
let lowPowerMode, batteryLevel, batteryState;
if (powerState) {
lowPowerMode = powerState.lowPowerMode ? powerState.lowPowerMode : null;
batteryLevel = powerState.batteryLevel ? powerState.batteryLevel : null;
batteryState = powerState.batteryState ? powerState.batteryState : null;
}
UserActionLogger.getInstance()._logEvent({
eventType: data.key,
logMessage: data.details,
screenShot: data.screenShot,
usedMemory,
isAirplaneMode: isAirplaneMode ? 'true' : 'false',
isBatteryCharging: isBatteryCharging ? 'true' : 'false',
isLocationEnabled: isLocationEnabled ? 'true' : 'false',
lowPowerMode: lowPowerMode ? 'true' : 'false',
batteryLevel,
batteryState,
});
}
/*
* Explicitly flush all log events to the server without
* waiting for the configured flush interval.
*/
static async flush() {
await UserActionLogger.getInstance()._beaverLogger.flush();
}
/*
* Get an instance of the logger
*/
static getInstance(): UserActionLogger {
if (UserActionLogger.logger == null) {
UserActionLogger.logger = new UserActionLogger();
}
return this.logger;
}
/*
* Initialize the logger with all async data
*/
async init() {
// syncronous calls
this._applicationName = DeviceInfo.getApplicationName();
this._brand = DeviceInfo.getBrand();
this._buildNumber = DeviceInfo.getBuildNumber();
this._bundleId = DeviceInfo.getBundleId();
this._deviceId = DeviceInfo.getDeviceId();
this._model = DeviceInfo.getModel();
this._readableVersion = DeviceInfo.getReadableVersion();
this._systemVersion = DeviceInfo.getSystemVersion();
this._version = DeviceInfo.getVersion();
this._isTablet = DeviceInfo.isTablet() ? 'true' : 'false';
// async calls
const apiLevel = DeviceInfo.getApiLevel();
const availableLocationProviders = DeviceInfo.getAvailableLocationProviders();
const buildId = DeviceInfo.getBuildId();
const carrier = DeviceInfo.getCarrier();
const codeName = DeviceInfo.getCodename();
const device = DeviceInfo.getDevice();
const display = DeviceInfo.getDisplay();
const devicename = DeviceInfo.getDeviceName();
const firstInstallTime = DeviceInfo.getFirstInstallTime();
const freeDiskStorage = DeviceInfo.getFreeDiskStorage();
const hardware = DeviceInfo.getHardware();
const host = DeviceInfo.getHost();
const incremental = DeviceInfo.getIncremental();
const installerPackageName = DeviceInfo.getInstallerPackageName();
const installReferrer = DeviceInfo.getInstallReferrer();
const lastUpdateTime = DeviceInfo.getLastUpdateTime();
const manufacturer = DeviceInfo.getManufacturer();
const product = DeviceInfo.getProduct();
const tags = DeviceInfo.getTags();
const type = DeviceInfo.getType();
const userAgent = DeviceInfo.getUserAgent();
const isEmulator = DeviceInfo.isEmulator();
const supportedAbis = DeviceInfo.supportedAbis();
this._apiLevel = await apiLevel;
this._availableLocationProviders = await availableLocationProviders;
this._buildId = await buildId;
this._carrier = await carrier;
this._codeName = await codeName;
this._device = await device;
this._display = await display;
this._devicename = await devicename;
this._firstInstallTime = await firstInstallTime;
this._freeDiskStorage = await freeDiskStorage;
this._hardware = await hardware;
this._host = await host;
this._incremental = await incremental;
this._installerPackageName = await installerPackageName;
this._installReferrer = await installReferrer;
this._lastUpdateTime = await lastUpdateTime;
this._manufacturer = await manufacturer;
this._product = await product;
this._tags = await tags;
this._type = await type;
this._userAgent = await userAgent;
this._isEmulator = await isEmulator;
this._supportedAbis = await supportedAbis;
this._asyncFieldsLoaded = true;
}
setFetchParams(fetchParams: FetchParams) {
this._fetchParams = fetchParams;
}
setCustomPayload(payload: {[string]: string}) {
this._customPayload = payload;
}
setBaseUrl(baseUrl: string) {
this._baseUrl = baseUrl;
}
_logEvent(payload: LogData) {
if (this._asyncFieldsLoaded) {
this._sendToBeaverLogger(payload);
} else {
this.init().then(() => {
console.error(
'Consider calling UserActionLogger.getInstance().init() before attempting to use any logging functions',
);
this._sendToBeaverLogger(payload);
});
}
}
_sendToBeaverLogger(payload: LogData) {
this._beaverLogger.addPayloadBuilder(payload => {
return {
...this._customPayload,
apiLevel: this._apiLevel,
applicationName: this._applicationName,
availableLocationProviders: JSON.stringify(
this._availableLocationProviders,
),
brand: this._brand,
buildId: this._buildId,
buildNumber: this._buildNumber,
bundleId: this._bundleId,
carrier: this._carrier,
codeName: this._codeName,
device: this._device,
deviceId: this._deviceId,
display: this._display,
devicename: this._devicename,
firstInstallTime: this._firstInstallTime,
freeDiskStorage: this._freeDiskStorage,
hardware: this._hardware,
host: this._host,
incremental: this._incremental,
installerPackageName: this._installerPackageName,
installReferrer: this._installReferrer,
lastUpdateTime: this._lastUpdateTime,
manufacturer: this._manufacturer,
model: this._model,
product: this._product,
readableVersion: this._readableVersion,
systemVersion: this._systemVersion,
tags: this._tags,
type: this._type,
userAgent: this._userAgent,
versionString: this._version,
isEmulator: this._isEmulator,
isTablet: this._isTablet,
supportedAbis: JSON.stringify(this._supportedAbis),
...payload,
};
});
this._beaverLogger.info(payload.eventType, payload);
}
}
export default UserActionLogger;