/* * Copyright (c) AppDynamics, Inc., and its affiliates * 2023 * All Rights Reserved * THIS IS UNPUBLISHED PROPRIETARY CODE OF APPDYNAMICS, INC. * The copyright notice above does not evidence any actual or intended publication of such source code */ package com.appdynamics.eum.reactnative import android.util.Log import com.appdynamics.eumagent.runtime.* import com.appdynamics.repacked.gson.JsonObject import com.appdynamics.repacked.gson.JsonParser import com.facebook.react.bridge.* import org.json.JSONObject import java.net.URL import java.util.Date import java.util.UUID import java.util.Calendar import kotlin.collections.HashMap import kotlin.collections.HashSet class ReactNativeAppdynamicsModuleImpl internal constructor(val context: ReactApplicationContext) { companion object { const val NAME = "ReactNativeAppdynamics" } private val trackers: MutableMap = HashMap() private val sessionFrames: MutableMap = HashMap() private val customRequestTrackers: MutableMap = HashMap() private var reactCrashRequestCallback: Callback? = null fun setCrashReportCallback(cb: Callback) { reactCrashRequestCallback = cb } private class CrashCallbackObject(val reactCrashReportCallback: Callback) : CrashReportCallback { override fun onCrashesReported(summaries: Collection) { val reactArray = Arguments.createArray() for (summary in summaries) { val map = Arguments.createMap() map.putString("crashId", summary.crashId) map.putString("exceptionClass", summary.exceptionClass) map.putString("exceptionMessage", summary.exceptionMessage) reactArray.pushMap(map) } reactCrashReportCallback.invoke(reactArray) } } fun getConstants(): Map { val constants: MutableMap = HashMap() constants["BREADCRUMB_VISIBILITY_CRASHES_AND_SESSIONS"] = BreadcrumbVisibility.CRASHES_AND_SESSIONS constants["BREADCRUMB_VISIBILITY_CRASHES_ONLY"] = BreadcrumbVisibility.CRASHES_ONLY constants["ERROR_SEVERITY_LEVEL_CRITICAL"] = ErrorSeverityLevel.CRITICAL constants["ERROR_SEVERITY_LEVEL_INFO"] = ErrorSeverityLevel.INFO constants["ERROR_SEVERITY_LEVEL_WARNING"] = ErrorSeverityLevel.WARNING constants["LOGGING_LEVEL_INFO"] = Instrumentation.LOGGING_LEVEL_INFO constants["LOGGING_LEVEL_NONE"] = Instrumentation.LOGGING_LEVEL_NONE constants["LOGGING_LEVEL_VERBOSE"] = Instrumentation.LOGGING_LEVEL_VERBOSE constants["MAX_USER_DATA_STRING_LENGTH"] = Instrumentation.MAX_USER_DATA_STRING_LENGTH return constants } fun start( properties: ReadableMap, hybridAgentType: String?, hybridAgentVersion: String?, promise: Promise ) { try { if (!properties.hasKey("appKey") || properties.getType("appKey") != ReadableType.String) { Log.e( javaClass.name, "Could not start AppDynamics instrumentation: Invalid or missing appKey" ) return } val builder: AgentConfiguration.Builder = AgentConfiguration.builder().withAppKey(properties.getString("appKey")).withContext(context) if (properties.hasKey("applicationName") && properties.getType("applicationName") == ReadableType.String) { builder.withApplicationName(properties.getString("applicationName")) } if (properties.hasKey("autoInstrument") && properties.getType("autoInstrument") == ReadableType.Boolean) { builder.withAutoInstrument(properties.getBoolean("autoInstrument")) } if (properties.hasKey("collectorURL") && properties.getType("collectorURL") == ReadableType.String) { builder.withCollectorURL(properties.getString("collectorURL")) } if (properties.hasKey("screenshotURL") && properties.getType("screenshotURL") == ReadableType.String) { builder.withScreenshotURL(properties.getString("screenshotURL")) } if (properties.hasKey("screenshotsEnabled") && properties.getType("screenshotsEnabled") == ReadableType.Boolean) { builder.withScreenshotsEnabled(properties.getBoolean("screenshotsEnabled")) } if (properties.hasKey("excludedURLPatterns") && properties.getType("excludedURLPatterns") == ReadableType.Array) { builder.withExcludedUrlPatterns(deserializeStringSet(properties.getArray("excludedURLPatterns"))) } if (properties.hasKey("loggingLevel") && properties.getType("loggingLevel") == ReadableType.Number) { builder.withLoggingLevel(properties.getInt("loggingLevel")) } if (properties.hasKey("compileTimeInstrumentationCheck") && properties.getType("compileTimeInstrumentationCheck") == ReadableType.Boolean) { builder.withCompileTimeInstrumentationCheck(properties.getBoolean("compileTimeInstrumentationCheck")) } if (properties.hasKey("jsAgentInjectionEnabled") && properties.getType("jsAgentInjectionEnabled") == ReadableType.Boolean) { builder.withJSAgentInjectionEnabled(properties.getBoolean("jsAgentInjectionEnabled")) } if (properties.hasKey("jsAgentAjaxEnabled") && properties.getType("jsAgentAjaxEnabled") == ReadableType.Boolean) { builder.withJSAgentAjaxEnabled(properties.getBoolean("jsAgentAjaxEnabled")) } if (properties.hasKey("crashReportingEnabled") && properties.getType("crashReportingEnabled") == ReadableType.Boolean) { builder.withCrashReportingEnabled(properties.getBoolean("crashReportingEnabled")) } if (reactCrashRequestCallback != null) { builder.withCrashCallback(CrashCallbackObject(reactCrashRequestCallback!!)) } if (properties.hasKey("sessionReplayURL") && properties.getType("sessionReplayURL") == ReadableType.String) { builder.withBlobServiceURL(properties.getString("sessionReplayURL")) builder.withSessionReplayEnabled(true) } // Disabled because it's not working properly. Users should fallback to manual tracking. builder.withInteractionCaptureMode(InteractionCaptureMode.None) Instrumentation.startFromHybrid(builder.build(), hybridAgentType, hybridAgentVersion) promise.resolve(null) } catch (e: RuntimeException) { promise.reject(e) } } fun changeAppKey(appKey: String, promise: Promise) { try { Instrumentation.changeAppKey(appKey) promise.resolve(null) } catch (e: Throwable) { promise.reject("Failed to change app key", e.message) } } fun reportMetric(name: String, value: Double, promise: Promise) { try { Instrumentation.reportMetric(name, value.toLong()) promise.resolve(null) } catch (e: Throwable) { promise.reject("Failed to report metric", e.message) } } fun reportError(hybridExceptionData: String, severityLevel: Double, promise: Promise) { try { Instrumentation.reportRawError(hybridExceptionData, severityLevel.toInt()) promise.resolve(null) } catch (e: Throwable) { promise.reject("Failed to report error", e.message) } } fun createCrashReport(hybridExceptionData: String, promise: Promise) { try { Instrumentation.createRawCrashReport(hybridExceptionData) promise.resolve(null) } catch (e: Throwable) { promise.reject("Failed to send crash report", e.message) } } fun startTimer(name: String, promise: Promise) { try { Instrumentation.startTimer(name) promise.resolve(null) } catch (e: Throwable) { promise.reject("Failed to start timer", e.message) } } fun stopTimer(name: String, promise: Promise) { try { Instrumentation.stopTimer(name) promise.resolve(null) } catch (e: Throwable) { promise.reject("Failed to stop timer", e.message) } } fun shutdownAgent(promise: Promise) { try { Instrumentation.shutdownAgent() promise.resolve(null) } catch (e: Throwable) { promise.reject("Failed to shutdown agent", e.message) } } fun restartAgent(promise: Promise) { try { Instrumentation.restartAgent() promise.resolve(null) } catch (e: Throwable) { promise.reject("Failed to restart agent", e.message) } } fun trackScreenStart(trackedScreen: ReadableMap, promise: Promise) { try { val pageName = trackedScreen.getString("screenName") val startDate = trackedScreen.getDouble("startDate") val uuid = UUID.randomUUID() Instrumentation.trackPageStart(pageName, uuid, startDate.toLong()) promise.resolve(uuid.toString()) } catch (e: Throwable) { promise.reject("Failed to create track page start", e) } } fun trackScreenEnd(trackedScreen: ReadableMap, promise: Promise) { try { val pageName = trackedScreen.getString("screenName") val uuidString = trackedScreen.getString("uuidString") val uuid = UUID.fromString(uuidString) val startDate = trackedScreen.getDouble("startDate") val endDate = trackedScreen.getDouble("endDate") Instrumentation.trackPageEnd(pageName, uuid, startDate.toLong(), endDate.toLong()) promise.resolve(null) } catch (e: Throwable) { promise.reject("Failed to create track page start", e) } } fun beginCall( className: String?, methodName: String?, args: ReadableArray, isStaticMethod: Boolean, promise: Promise ) { try { val callId = UUID.randomUUID().toString() val parsedArgs = deserializeArray(args) val callTracker: CallTracker = Instrumentation.beginCall(isStaticMethod, className, methodName, *parsedArgs) trackers[callId] = callTracker promise.resolve(callId) } catch (e: Throwable) { promise.reject("Failed to start function tracking", e) } } fun endCallWithSuccess(callId: String?, data: ReadableMap, promise: Promise) { try { val callTracker: CallTracker? = trackers[callId] if (callTracker != null) { trackers.remove(callId) callTracker.reportCallEndedWithReturnValue(deserializeMap(data)["result"]) } promise.resolve(null) } catch (e: Throwable) { promise.reject("Failed to end function tracking", e) } } fun endCallWithError(callId: String?, error: ReadableMap, promise: Promise) { try { val callTracker: CallTracker? = trackers[callId] if (callTracker != null) { trackers.remove(callId) callTracker.reportCallEndedWithException( Exception( """ ${error.getString("message")} ${error.getString("stack")} """.trimIndent() ) ) } promise.resolve(null) } catch (e: Throwable) { promise.reject("Failed to end function tracking", e) } } fun leaveBreadcrumb(breadcrumb: String, mode: Double, promise: Promise) { try { Instrumentation.leaveBreadcrumb(breadcrumb, mode.toInt()) promise.resolve(null) } catch (e: Throwable) { promise.reject("Failed to leave breadcrumb", e) } } fun unblockScreenshots(promise: Promise) { try { Instrumentation.unblockScreenshots() promise.resolve(null) } catch (e: Throwable) { promise.reject("Failed to unblock screenshots.", e) } } fun blockScreenshots(promise: Promise) { try { Instrumentation.blockScreenshots() promise.resolve(null) } catch (e: Throwable) { promise.reject("Failed to block screenshots.", e) } } fun screenshotsBlocked(promise: Promise) { try { promise.resolve(Instrumentation.screenshotsBlocked()) } catch (e: Throwable) { promise.reject("Failed to check if screenshots are blocked.", e) } } fun takeScreenshot(promise: Promise) { try { Instrumentation.takeScreenshot() promise.resolve(null) } catch (e: Throwable) { promise.reject("Failed to take screenshot.", e) } } fun setUserData(key: String, value: String, promise: Promise) { try { Instrumentation.setUserData(key, value) promise.resolve(null) } catch (e: Throwable) { promise.reject("Failed to set user data.", e) } } fun removeUserData(key: String, promise: Promise) { try { Instrumentation.setUserData(key, null) promise.resolve(null) } catch (e: Throwable) { promise.reject("Failed to remove user data.", e) } } fun setUserDataNumber(key: String, value: Double, promise: Promise) { try { Instrumentation.setUserDataDouble(key, value) promise.resolve(null) } catch (e: Throwable) { promise.reject("Failed to set user data number.", e) } } fun removeUserDataNumber(key: String, promise: Promise) { try { Instrumentation.setUserDataDouble(key, null) promise.resolve(null) } catch (e: Throwable) { promise.reject("Failed to remove user data number.", e) } } fun setUserDataBoolean(key: String, value: Boolean, promise: Promise) { try { Instrumentation.setUserDataBoolean(key, value) promise.resolve(null) } catch (e: Throwable) { promise.reject("Failed to set user data boolean.", e) } } fun removeUserDataBoolean(key: String, promise: Promise) { try { Instrumentation.setUserDataBoolean(key, null) promise.resolve(null) } catch (e: Throwable) { promise.reject("Failed to remove user data boolean.", e) } } fun setUserDataDate(key: String, valueStr: String, promise: Promise) { try { Instrumentation.setUserDataDate(key, Date(parseLong(valueStr)!!)) promise.resolve(null) } catch (e: Throwable) { promise.reject("Failed to set user data date.", e) } } fun removeUserDataDate(key: String, promise: Promise) { try { Instrumentation.setUserDataDate(key, null) promise.resolve(null) } catch (e: Throwable) { promise.reject("Failed remove user data date.", e) } } fun startNextSession(promise: Promise) { try { Instrumentation.startNextSession() promise.resolve(null) } catch (e: Throwable) { promise.reject("Failed to start next session", e) } } fun startSessionFrame(sessionFrameName: String, promise: Promise) { try { val frameId = UUID.randomUUID().toString() val sessionFrame = Instrumentation.startSessionFrame(sessionFrameName) sessionFrames[frameId] = sessionFrame promise.resolve(frameId) } catch (e: Throwable) { promise.reject("Failed to create session frame", e) } } fun updateSessionFrameName(id: String, name: String, promise: Promise) { try { sessionFrames[id]?.updateName(name) promise.resolve(null) } catch (e: Throwable) { promise.reject("Failed to update session frame", e) } } fun endSessionFrame(id: String, promise: Promise) { try { sessionFrames[id]?.end() sessionFrames.remove(id) promise.resolve(null) } catch (e: Throwable) { promise.reject("Failed to end session frame", e) } } fun trackUIEvent(eventInfo: ReadableMap, promise: Promise) { try { val screenName = eventInfo.getString("screenName") val eventName = eventInfo.getString("eventName") val className = eventInfo.getString("className") val label = eventInfo.getString("label") val accessibilityLabel = eventInfo.getString("accessibilityLabel") val tag = if (!eventInfo.hasKey("tag")) 0 else eventInfo.getInt("tag") val index = eventInfo.getString("index") val uiResponder = eventInfo.getString("uiResponder") val calendar = Calendar.getInstance() val startTime = calendar.timeInMillis Instrumentation.trackUIEvent( screenName, eventName, className, startTime, label, accessibilityLabel, tag, index, uiResponder ) promise.resolve(null) } catch (e: Throwable) { promise.reject("Failed to track UI event", e) } } fun getServerCorrelationHeaders(promise: Promise) { try { val headers = ServerCorrelationHeaders.generate() promise.resolve(convertToReadableMap(headers)) } catch (e: Throwable) { promise.reject("Failed to get server correlation headers", e) } } fun getRequestTrackerWithUrl(urlString: String, promise: Promise) { try { val trackerId = UUID.randomUUID().toString() val tracker = Instrumentation.beginHttpRequest(URL(urlString)) customRequestTrackers[trackerId] = tracker promise.resolve(trackerId) } catch (e: Throwable) { promise.reject("Failed to create request tracker", e) } } fun setRequestTrackerErrorInfo( trackerId: String, errorDict: ReadableMap, promise: Promise ) { try { val errorMessage = errorDict.getString("message") ?: "RN error without a message." customRequestTrackers[trackerId]?.withError(errorMessage) promise.resolve(null) } catch (e: Throwable) { promise.reject("Failed to set request tracker error info", e) } } fun setRequestTrackerStatusCode( trackerId: String, statusCode: Double, promise: Promise ) { try { customRequestTrackers[trackerId]?.withResponseCode(statusCode.toInt()) promise.resolve(null) } catch (e: Throwable) { promise.reject("Failed to set request tracker status code", e) } } fun setRequestTrackerResponseHeaders( trackerId: String, responseHeaders: ReadableMap, promise: Promise ) { try { val map: MutableMap> = java.util.HashMap() for ((key, value) in responseHeaders.toHashMap()) { map[key] = if (value is String) listOf(value) else listOf("undefined") } customRequestTrackers[trackerId]?.withResponseHeaderFields(map) promise.resolve(null) } catch (e: Throwable) { promise.reject("Failed to set request tracker response headers", e) } } fun setRequestTrackerRequestHeaders( trackerId: String, requestHeaders: ReadableMap, promise: Promise ) { try { val map: MutableMap> = java.util.HashMap() for ((key, value) in requestHeaders.toHashMap()) { map[key] = if (value is String) listOf(value) else listOf("undefined") } customRequestTrackers[trackerId]?.withRequestHeaderFields(map) promise.resolve(null) } catch (e: Throwable) { promise.reject("Failed to set request tracker request headers", e) } } fun requestTrackerReport(trackerId: String, promise: Promise) { try { customRequestTrackers[trackerId]?.reportDone() promise.resolve(null) } catch (e: Throwable) { promise.reject("Failed to report request tracker", e) } } fun setRequestTrackerUserData( trackerId: String, key: String, value: String, promise: Promise ) { try { customRequestTrackers[trackerId]?.withUserData(key, value) promise.resolve(null) } catch (e: Throwable) { promise.reject("Failed to set request tracker data", e) } } fun setRequestTrackerUserDataNumber( trackerId: String, key: String, value: Double, promise: Promise ) { try { customRequestTrackers[trackerId]?.withUserDataDouble(key, value) promise.resolve(null) } catch (e: Throwable) { promise.reject("Failed to set request tracker data", e) } } fun setRequestTrackerUserDataBoolean( trackerId: String, key: String, value: Boolean, promise: Promise ) { try { customRequestTrackers[trackerId]?.withUserDataBoolean(key, value) promise.resolve(null) } catch (e: Throwable) { promise.reject("Failed to set request tracker data", e) } } fun setRequestTrackerUserDataDate( trackerId: String, key: String, value: Double, promise: Promise ) { try { customRequestTrackers[trackerId]?.withUserDataDate(key, Date(value.toLong())) promise.resolve(null) } catch (e: Throwable) { promise.reject("Failed to set request tracker data", e) } } /* * We do all of this because React doesn't allow passing Long values over the bridge */ private fun parseLong(valueStr: String?): Long? { return valueStr?.toLong(10) } private fun deserializeStringSet(readableArray: ReadableArray?): Set { val ret: MutableSet = HashSet(readableArray!!.size()) for (i in 0 until readableArray.size()) { when (readableArray.getType(i)) { ReadableType.String -> ret.add(readableArray.getString(i)) else -> ret.add(null) } } return ret } private fun deserializeArray(readableArray: ReadableArray): Array { val ret = arrayOfNulls(readableArray.size()) for (i in 0 until readableArray.size()) { when (readableArray.getType(i)) { ReadableType.Null -> ret[i] = null ReadableType.Boolean -> ret[i] = readableArray.getBoolean(i) ReadableType.Number -> ret[i] = readableArray.getDouble(i) ReadableType.String -> ret[i] = readableArray.getString(i) // Note: skipping nested structures for now because it could create an infinite recursion // ReadableType.Map -> ret[i] = readableArray.getMap(i) // ReadableType.Array -> ret[i] = readableArray.getArray(i) else -> println("deserializeArray else statement triggered.") } } return ret } private fun deserializeMap(readableMap: ReadableMap): Map { val ret: MutableMap = HashMap() val iterator = readableMap.keySetIterator() while (iterator.hasNextKey()) { val key = iterator.nextKey() when (readableMap.getType(key)) { ReadableType.Null -> ret[key] = null ReadableType.Boolean -> ret[key] = readableMap.getBoolean(key) ReadableType.Number -> ret[key] = readableMap.getDouble(key) ReadableType.String -> ret[key] = readableMap.getString(key) // Note: skipping nested structures for now because it could create an infinite recursion // ReadableType.Map -> ret[key] = readableMap.getMap(key) // ReadableType.Array -> ret[key] = readableMap.getArray(key) else -> println("deserializeMap else statement triggered.") } } return ret } private fun convertToReadableMap(map: Map): ReadableMap { val readableMap = Arguments.createMap() map.forEach { (key, value) -> when (value) { is String -> readableMap.putString(key, value) is Int -> readableMap.putInt(key, value) is Double -> readableMap.putDouble(key, value) is Boolean -> readableMap.putBoolean(key, value) is Map<*, *> -> readableMap.putMap(key, convertToReadableMap(value as Map)) is List<*> -> readableMap.putArray(key, convertToReadableArray(value as List)) } } return readableMap } private fun convertToReadableArray(list: List): ReadableArray { val readableArray = Arguments.createArray() list.forEach { item -> when (item) { is String -> readableArray.pushString(item) is Int -> readableArray.pushInt(item) is Double -> readableArray.pushDouble(item) is Boolean -> readableArray.pushBoolean(item) is Map<*, *> -> readableArray.pushMap(convertToReadableMap(item as Map)) is List<*> -> readableArray.pushArray(convertToReadableArray(item as List)) } } return readableArray } }