Skip to content

Commit

Permalink
Stack tracing & Crash reports work in progress - testing
Browse files Browse the repository at this point in the history
  • Loading branch information
RNEvok committed Jun 8, 2024
1 parent c3808b3 commit 8bc3c6b
Show file tree
Hide file tree
Showing 16 changed files with 383 additions and 41 deletions.
101 changes: 71 additions & 30 deletions Connector.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,24 +4,28 @@ import { EventHandleError, ScenarioHandleError } from './Errors';
import { SOCKET_EVENTS_LISTEN, SOCKET_EVENTS_EMIT } from './api/api';
import { SPECIAL_INSTRUCTIONS_TABLE, SPECIAL_INSTRUCTIONS } from './constants/events';
import { io, Socket } from "socket.io-client";
import { codebudConsoleLog, codebudConsoleWarn, jsonStringifyKeepMeta, stringifyIfNotString } from './helpers/helperFunctions';
import { codebudConsoleLog, codebudConsoleWarn, jsonStringifyKeepMeta, stringifyIfNotString, errorToJSON } from './helpers/helperFunctions';
import { getProcessEnv } from './helpers/environment';
import { getBrowserInfo } from './helpers/browserInfo';
import { getEnvironmentPlatform } from './helpers/platform';
import { getOS } from './helpers/os';
import { remoteSettingsService } from './services/remoteSettingsService';
import { asyncStoragePlugin } from './asyncStorage/asyncStorage';
import { localStoragePlugin } from './localStorage/localStorage';
import { getStackTrace } from './StackTracing';
import moment from 'moment';

class Connector {
private _eventListenersTable: T.EventListenersTable = {};
private _currentInterceptedReduxActionId = 0;
private _currentInterceptedStorageActionId = 0;
private _currentCapturedEventId = 0;
private _currentCrashReportId = 0;
private _currentInterceptedTanStackQueryEventId = 0;
private _connectorInitiated: boolean = false;
private _apiKey: string = "";
private _projectInfo: T.ProjectInfo | null = null;
private _enableStackTracing: boolean = false;
private _instructionsTable: T.InstructionsTable = {};
private _onEventUsersCustomCallback: T.OnEventUsersCustomCallback | undefined;
private _networkInterceptor: T.NetworkInterceptorInstance | null = null;
Expand Down Expand Up @@ -91,22 +95,12 @@ class Connector {
};

private _fillInstructionsTable(instructions: T.Instruction[]) {
const table: T.InstructionsTable = {};

instructions.forEach((ins: T.Instruction) => {
table[ins.id] = ins;
});

this._instructionsTable = table;
this._instructionsTable = {};
instructions.forEach((ins) => this._instructionsTable[ins.id] = ins);
}

private _getInstructionsPublicFields(instructions: T.Instruction[]) {
const instructionsPublic = instructions.map((el: T.Instruction) => {
const publicData = (({ handler, ...o }) => o)(el); // remove "handler" field
return publicData;
});

return instructionsPublic;
return instructions.map((el) => (({ handler, ...o }) => o)(el)); // removing "handler" field
}

private serveAllExternalListenersWithNewEvent(event: T.RemoteEvent) {
Expand Down Expand Up @@ -276,12 +270,12 @@ class Connector {
this._fillInstructionsTable(instructions);
this._onEventUsersCustomCallback = usersCustomCallback;

if (config?.EncryptionPlugin) {
if (config?.EncryptionPlugin)
this._encryption = new config.EncryptionPlugin();
}
if (config?.projectInfo) {
if (config?.projectInfo)
this._projectInfo = config.projectInfo;
}
if (config?.enableStackTracing)
this._enableStackTracing = true;

this._connectionInfoPacket = {
apiKey,
Expand All @@ -301,12 +295,10 @@ class Connector {
reconnectionDelay: 3e3,
});

if (config?.Interceptor) {
if (config?.Interceptor)
this._setupNetworkMonitor(config);
}
if (config?.ReactNativePlugin) {
if (config?.ReactNativePlugin)
this._setupRN(config);
}

this._socket.on(SOCKET_EVENTS_LISTEN.CONNECT, () => {
codebudConsoleLog('Socket connected:', this._socket?.connected);
Expand Down Expand Up @@ -377,11 +369,12 @@ class Connector {
}
}

public handleDispatchedReduxAction(action: T.InterceptedReduxAction, batchingTimeMs: number) {
public async handleDispatchedReduxAction(action: T.InterceptedReduxAction, batchingTimeMs: number) {
if (this._socket?.connected) {
const timestamp = moment().valueOf();
const actionId = this._currentInterceptedReduxActionId++;
const reduxActionData = {actionId: `RA_${actionId}`, action, timestamp};
const _stackTraceData = this._enableStackTracing ? (await getStackTrace(new Error(''))) : undefined;
const reduxActionData = {actionId: `RA_${actionId}`, action, timestamp, _stackTraceData};
jsonStringifyKeepMeta(reduxActionData).ok && this._currentReduxActionsBatch.push(reduxActionData);

if (this._sendReduxActionsBatchingTimer)
Expand Down Expand Up @@ -419,11 +412,12 @@ class Connector {

// AsyncStorage / localStorage
// used in asyncStoragePlugin & localStoragePlugin, (binded context)
private _handleInterceptedStorageAction(action: string, data?: any) {
private async _handleInterceptedStorageAction(action: string, data?: any) {
if (this._socket?.connected) {
const timestamp = moment().valueOf();
const storageActionId = this._currentInterceptedStorageActionId++;
const storageActionData = {storageActionId: `SA_${storageActionId}`, action, data, timestamp};
const _stackTraceData = this._enableStackTracing ? (await getStackTrace(new Error(''))) : undefined;
const storageActionData = {storageActionId: `SA_${storageActionId}`, action, data, timestamp, _stackTraceData};
jsonStringifyKeepMeta(storageActionData).ok && this._currentStorageActionsBatch.push(storageActionData);

if (this._sendStorageActionsBatchingTimer)
Expand Down Expand Up @@ -455,16 +449,61 @@ class Connector {
this._untrackLocalStorage = controlFunctions.untrackLocalStorage;
}

public captureEvent(title: string, data: any) {
public async captureEvent(title: string, data: any) {
if (this._socket?.connected) {
const timestamp = moment().valueOf();
const capturedEventId = this._currentCapturedEventId++;

const encryptedData = this._encryptData({timestamp, capturedEventId: `UCE_${capturedEventId}`, title, data});
const _stackTraceData = this._enableStackTracing ? (await getStackTrace(new Error(''))) : undefined;

const encryptedData = this._encryptData({timestamp, capturedEventId: `UCE_${capturedEventId}`, title, data, _stackTraceData});
encryptedData.ok && this._socket?.emit(SOCKET_EVENTS_EMIT.CAPTURE_EVENT, encryptedData.result);
}
}

public async captureCrashReport(type: string, data: any) {
codebudConsoleWarn(`${CONFIG.PRODUCT_NAME} has captured important exception:`, type, data);

if (this._socket?.connected) {
const timestamp = moment().valueOf();
const crashReportId = this._currentCrashReportId++;

let _stackTraceData;
if ((data instanceof Error) || data?.stack)
_stackTraceData = await getStackTrace(data.stack);

const encryptedData = this._encryptData({timestamp, crashReportId: `ACR_${crashReportId}`, type, data, _stackTraceData});
encryptedData.ok && this._socket?.emit(SOCKET_EVENTS_EMIT.CAPTURE_CRASH_REPORT, encryptedData.result);
}
}

public enableApplicationCrashInterception() {
const environmentPlatform = getEnvironmentPlatform();

switch (environmentPlatform) {
case "nodejs": {
process
.on('unhandledRejection', (reason) => this.captureCrashReport('unhandledRejection', reason))
.on('uncaughtException', (err) => this.captureCrashReport('uncaughtException', errorToJSON(err)).then(() => process.exit(1)));
break;
}
case "web": {
window.onunhandledrejection = (event) => {
this.captureCrashReport('window.onunhandledrejection', {reason: event.reason});
event.preventDefault(); // Prevent the default handling (such as outputting the error to the console)
}
window.onerror = (message, source, line, col, error) => {
this.captureCrashReport('window.onerror', {message, source, line, col, error: errorToJSON(error)});
return true; // Return true to prevent the default browser error handling
};
break;
}
default:
codebudConsoleWarn(`enableApplicationCrashInterception method does not do anything for this platform: ${environmentPlatform}. Consider reading ${CONFIG.PRODUCT_NAME} docs.`);
break;
}
}

private _proceedNewTanStackQueriesData(queriesData: T.TanStackGetQueriesDataReturnType, batchingTimeMs: number) {
const previousTanStackQueriesDataCopyStr = JSON.stringify(this._currentTanStackQueriesDataCopy);
this._currentTanStackQueriesDataCopy = queriesData;
Expand Down Expand Up @@ -498,11 +537,12 @@ class Connector {
}
}

private _proceedInterceptedTanStackQueryEvent(event: T.TanStackQueryCacheEvent, batchingTimeMs: number) {
private async _proceedInterceptedTanStackQueryEvent(event: T.TanStackQueryCacheEvent, batchingTimeMs: number) {
if (this._socket?.connected) {
const timestamp = moment().valueOf();
const tanStackQueryEventId = this._currentInterceptedTanStackQueryEventId++;
const tanStackQueryEventData: T.InterceptedTanStackQueryEventPreparedData = {tanStackQueryEventId: `TQE_${tanStackQueryEventId}`, event, timestamp};
const _stackTraceData = this._enableStackTracing ? (await getStackTrace(new Error(''))) : undefined;
const tanStackQueryEventData: T.InterceptedTanStackQueryEventPreparedData = {tanStackQueryEventId: `TQE_${tanStackQueryEventId}`, event, timestamp, _stackTraceData};
jsonStringifyKeepMeta(tanStackQueryEventData).ok && this._currentTanStackQueryEventsBatch.push(tanStackQueryEventData);

if (this._sendTanStackQueryEventsBatchingTimer)
Expand Down Expand Up @@ -554,6 +594,7 @@ class Connector {
this._socket = undefined;
this._apiKey = "";
this._projectInfo = null;
this._enableStackTracing = false;
this._instructionsTable = {};
this._onEventUsersCustomCallback = undefined;
this._connectionInfoPacket = undefined;
Expand Down
26 changes: 26 additions & 0 deletions StackTracing/constants.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
export const CALLEE_EXCLUDE = [
'Object.dispatch',
'dispatch',
'HTMLUnknownElement.callCallback',
'Object.invokeGuardedCallbackDev',
'invokeGuardedCallback',
'invokeGuardedCallbackAndCatchFirstError',
'executeDispatch',
'processDispatchQueueItemsInOrder',
'processDispatchQueue',
'dispatchEventsForPlugins',
'batchedEventUpdates$1',
'batchedEventUpdates',
'scheduler.development.js',
'trace',
'logger.ts',
'unstable_runWithPriority',
'Object.captureEvent'
];

export const FILE_NAME_EXCLUDE = [
'logger.ts',
'react-dom.development.js',
'serializableStateInvariantMiddleware.ts',
'Connector.ts'
];
39 changes: 39 additions & 0 deletions StackTracing/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import { getEnvironmentPlatform } from "../helpers/platform";
import { StackTraceData } from "../types";
import { CALLEE_EXCLUDE, FILE_NAME_EXCLUDE } from "./constants";
import { filterStack, prepareStack } from "./stackTraceyHelpers";
import { parseRawStack, filterStack as filterSimpleStack, prepareStack as prepareSimpleStack } from "./simpleTracing";
import StackTracey from 'stacktracey';

export const getStackTrace = async (errorOrStack: Error | string | undefined): Promise<StackTraceData> => {
const environmentPlatform = getEnvironmentPlatform();
const stackStr = (errorOrStack instanceof Error) ? errorOrStack.stack : errorOrStack;

switch (environmentPlatform) {
case "nodejs": {
const stack = new StackTracey(stackStr);
const stackWithSource = stack.withSources();

const filteredStack = filterStack(stackWithSource, CALLEE_EXCLUDE, FILE_NAME_EXCLUDE);
const preparedStack = prepareStack(filteredStack);

return { stack: preparedStack };
}
case "web": {
const stack = new StackTracey(stackStr);
const stackWithSource = await stack.withSourcesAsync();

const filteredStack = filterStack(stackWithSource, CALLEE_EXCLUDE, FILE_NAME_EXCLUDE);
const preparedStack = prepareStack(filteredStack);

return { stack: preparedStack };
}
case "react-native": {
const stack = parseRawStack(stackStr);
const filteredStack = filterSimpleStack(stack, CALLEE_EXCLUDE, FILE_NAME_EXCLUDE);
const preparedStack = prepareSimpleStack(filteredStack);

return { stack: preparedStack };
}
}
}
80 changes: 80 additions & 0 deletions StackTracing/simpleTracing.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
import { StackTraceCallData } from "../types";

export const nixSlashes = (x: string) => x.replace (/\\/g, '/');

type SimpleParsedStack = {
beforeParse: string;
callee: string;
native: boolean;
file: string;
line: number | undefined;
column: number | undefined;
}[];

// Based on stacktracey rawParse method
export const parseRawStack = (str: string = ""): SimpleParsedStack => {
const lines = str.split('\n');

const entries = lines.map(line => {
line = line.trim();

let callee, fileLineColumn = [], native, planA, planB;

if ((planA = line.match (/at (.+) \(eval at .+ \((.+)\), .+\)/)) || // eval calls
(planA = line.match (/at (.+) \((.+)\)/)) ||
((line.slice (0, 3) !== 'at ') && (planA = line.match (/(.*)@(.*)/)))) {

callee = planA[1];
native = (planA[2] === 'native');
fileLineColumn = (planA[2].match (/(.*):(\d+):(\d+)/) || planA[2].match (/(.*):(\d+)/) || []).slice(1);

} else if ((planB = line.match (/^(at\s+)*(.+):(\d+):(\d+)/) )) {
fileLineColumn = (planB).slice (2)
} else {
return undefined
}

if (callee && !fileLineColumn[0]) {
const type = callee.split ('.')[0];
if (type === 'Array') {
native = true;
}
}

return {
beforeParse: line,
callee: callee || '',
native: native || false,
file: nixSlashes(fileLineColumn[0] || ''),
line: parseInt(fileLineColumn[1] || '', 10) || undefined,
column: parseInt(fileLineColumn[2] || '', 10) || undefined
}
});

return entries.filter(x => (x !== undefined)) as SimpleParsedStack;
};

export const filterStack = (stack: SimpleParsedStack, calleeExclude: string[], fileNameExclude: string[]) => {
return stack.filter((a) => {
let fileExcluded = false;

for (const fileName of fileNameExclude) {
if (a.file.includes(fileName)) {
fileExcluded = true;
break;
}
}

return !calleeExclude.includes(a.callee) && !fileExcluded && !a.file.includes('node_modules');
});
}

export const prepareStack = (stack: SimpleParsedStack): StackTraceCallData[] => {
return stack.map((a) => ({
beforeParse: a.beforeParse,
callee: a.callee,
native: a.native,
file: a.file,
line: a.line
}));
}
20 changes: 20 additions & 0 deletions StackTracing/stackTraceyHelpers.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import StackTracey from "stacktracey";
import { StackTraceCallData } from "../types";

export const filterStack = (stackWithSource: StackTracey, calleeExclude: string[], fileNameExclude: string[]): StackTracey => {
return stackWithSource.filter((a) => !calleeExclude.includes(a.callee) && !fileNameExclude.includes(a.fileName) && !a.file.includes('node_modules'));
};

export const prepareStack = (stackWithSource: StackTracey): StackTraceCallData[] => {
return stackWithSource.items.map((a) => ({
sourceLine: a.sourceLine,
beforeParse: a.beforeParse,
callee: a.callee,
calleeShort: a.calleeShort,
native: a.native,
fileRelative: a.fileRelative,
fileShort: a.fileShort,
fileName: a.fileName,
line: a.line
}));
}
1 change: 1 addition & 0 deletions api/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ const SOCKET_EVENTS_EMIT = {
SAVE_MOBILE_APP_STATE: "saveMobileAppState",
SAVE_INTERCEPTED_STORAGE_ACTIONS_BATCH: "saveInterceptedStorageActionsBatch",
CAPTURE_EVENT: "captureEvent",
CAPTURE_CRASH_REPORT : "captureCrashReport",
SAVE_TANSTACK_QUERIES_DATA_COPY: "saveTanStackQueriesDataCopy",
SAVE_TANSTACK_QUERY_EVENTS_BATCH: "saveTanStackQueryEventsBatch",
SAVE_CONTEXT_VALUE_COPY: "saveContextValueCopy",
Expand Down
Loading

0 comments on commit 8bc3c6b

Please sign in to comment.