diff --git a/backtrace-library/src/androidTest/java/backtraceio/library/BacktraceClientAttributeTests.java b/backtrace-library/src/androidTest/java/backtraceio/library/BacktraceClientAttributeTests.java new file mode 100644 index 00000000..26ef5c10 --- /dev/null +++ b/backtrace-library/src/androidTest/java/backtraceio/library/BacktraceClientAttributeTests.java @@ -0,0 +1,130 @@ +package backtraceio.library; + +import static junit.framework.TestCase.fail; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; + +import android.content.Context; +import androidx.test.platform.app.InstrumentationRegistry; +import androidx.test.ext.junit.runners.AndroidJUnit4; + +import net.jodah.concurrentunit.Waiter; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; + +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.TimeUnit; + +import backtraceio.library.events.OnServerResponseEventListener; +import backtraceio.library.events.RequestHandler; +import backtraceio.library.models.BacktraceData; +import backtraceio.library.models.BacktraceResult; +import backtraceio.library.models.types.BacktraceResultStatus; + +@RunWith(AndroidJUnit4.class) +public class BacktraceClientAttributeTests { + private Context context; + private BacktraceCredentials credentials; + private BacktraceDatabase database; + + @Before + public void setUp() { + context = InstrumentationRegistry.getInstrumentation().getContext(); + credentials = new BacktraceCredentials("https://example-endpoint.com/", ""); + database = new BacktraceDatabase(context, context.getFilesDir().getAbsolutePath()); + } + + @Test + public void shouldAddASingleAttribute() { + final String attributeKey = "test-attribute"; + final String attributeValue = "test-value"; + BacktraceClient backtraceClient = new BacktraceClient(context, credentials, database); + backtraceClient.addAttribute(attributeKey, attributeValue); + + Map attributes = backtraceClient.getAttributes(); + + Object value = attributes.get(attributeKey); + assertNotNull(value); + assertEquals(value, attributeValue); + } + + @Test + public void shouldAddMultipleAttributesAtOnce() { + final String attributeKey = "test-attribute"; + final String attributeValue = "test-value"; + final Integer numberOfAttributesToVerify = 3; + Map attributes = new HashMap<>(); + for (int attributeIteration = 0; attributeIteration < numberOfAttributesToVerify; attributeIteration++) { + attributes.put(String.format("%s %d", attributeKey, attributeIteration), attributeValue); + } + BacktraceClient backtraceClient = new BacktraceClient(context, credentials, database); + backtraceClient.addAttribute(attributes); + + Map clientAttributes = backtraceClient.getAttributes(); + + for (int attributeIteration = 0; attributeIteration < numberOfAttributesToVerify; attributeIteration++) { + Object value = attributes.get(String.format("%s %d", attributeKey, attributeIteration)); + assertNotNull(value); + assertEquals(value, attributeValue); + } + } + + @Test + public void shouldReplaceExistingAttribute() { + final String attributeKey = "test-attribute"; + final String oldAttributeValue = "old-test-value"; + final String newAttributeValue = "test-value-new"; + + BacktraceClient backtraceClient = new BacktraceClient(context, credentials, database); + backtraceClient.addAttribute(attributeKey, oldAttributeValue); + + backtraceClient.addAttribute(attributeKey, newAttributeValue); + + Map attributes = backtraceClient.getAttributes(); + + Object value = attributes.get(attributeKey); + assertNotNull(value); + assertEquals(value, newAttributeValue); + } + + @Test + public void attributesShouldBeAvailableInReport() { + final String errorMessage = "error message"; + final String attributeKey = "test-attribute"; + final String attributeValue = "test-value"; + BacktraceClient backtraceClient = new BacktraceClient(context, credentials, database); + backtraceClient.addAttribute(attributeKey, attributeValue); + RequestHandler rh = new RequestHandler() { + @Override + public BacktraceResult onRequest(BacktraceData data) { + Object value =data.attributes.get(attributeKey); + assertNotNull(value); + assertEquals(value, attributeValue); + return new BacktraceResult(data.report, data.report.exception.getMessage(), + BacktraceResultStatus.Ok); + } + }; + backtraceClient.setOnRequestHandler(rh); + final Waiter waiter = new Waiter(); + + // WHEN + backtraceClient.send(new Exception(errorMessage), new + OnServerResponseEventListener() { + @Override + public void onEvent(BacktraceResult backtraceResult) { + waiter.resume(); + } + } + ); + // WAIT FOR THE RESULT FROM ANOTHER THREAD + try { + waiter.await(5, TimeUnit.SECONDS); + } catch (Exception ex) { + fail(ex.getMessage()); + } + } + +} diff --git a/backtrace-library/src/androidTest/java/backtraceio/library/database/BacktraceDatabaseTest.java b/backtrace-library/src/androidTest/java/backtraceio/library/database/BacktraceDatabaseTest.java index 27645fe7..51ebb6ab 100644 --- a/backtrace-library/src/androidTest/java/backtraceio/library/database/BacktraceDatabaseTest.java +++ b/backtrace-library/src/androidTest/java/backtraceio/library/database/BacktraceDatabaseTest.java @@ -3,6 +3,8 @@ import static org.junit.Assert.assertEquals; import android.content.Context; +import android.os.FileUtils; + import androidx.test.platform.app.InstrumentationRegistry; import androidx.test.ext.junit.runners.AndroidJUnit4; @@ -14,7 +16,9 @@ import org.junit.runner.RunWith; import java.io.File; +import java.io.FileFilter; import java.io.IOException; +import java.nio.file.FileSystems; import java.util.ArrayList; import java.util.HashMap; import java.util.List; @@ -36,6 +40,8 @@ public class BacktraceDatabaseTest { private Context context; private String dbPath; private BacktraceDatabase database; + private BacktraceClient client; + private BacktraceCredentials credentials; private final String testMessage = "Example test string"; @Before @@ -43,6 +49,8 @@ public void setUp() { this.context = InstrumentationRegistry.getInstrumentation().getContext(); this.dbPath = this.context.getFilesDir().getAbsolutePath(); this.database = new BacktraceDatabase(this.context, dbPath); + this.credentials = new BacktraceCredentials("https://test.sp.backtrace.io", "1231231231231"); + this.client = new BacktraceClient(this.context, this.credentials); this.database.start(); this.database.clear(); } @@ -50,8 +58,37 @@ public void setUp() { @After public void after() { this.database.clear(); + this.database.disableNativeIntegration(); + } + + @Test + public void shouldNotCrashWhenNativeIntegrationIsNotEnabled() { + assertEquals(false, this.database.addNativeAttribute("key", "value")); + } + + @Test + public void shouldAddAnAttributeToNativeIntegration() { + this.database.setupNativeIntegration(this.client, this.credentials); + assertEquals(true, this.database.addNativeAttribute("key", "value")); + } + + @Test + public void shouldNotAddAnAttributeToNativeIntegrationWithComplexAttributeValue() { + this.database.setupNativeIntegration(this.client, this.credentials); + assertEquals(false, this.database.addNativeAttribute("key", new HashMap<>())); } + @Test + public void shouldPreventAddingAnAttributeWithNullableKey() { + this.database.setupNativeIntegration(this.client, this.credentials); + assertEquals(false, this.database.addNativeAttribute(null, "value")); + } + + @Test + public void shouldPreventAddingAnAttributeWithNullableValue() { + this.database.setupNativeIntegration(this.client, this.credentials); + assertEquals(false, this.database.addNativeAttribute("key", null)); + } @Test public void isDatabaseEmpty() { @@ -124,8 +161,13 @@ public void clearDatabase() { assertEquals(2, database.count()); database.clear(); - int filesNumber = new File(this.dbPath).listFiles().length; - assertEquals(0, filesNumber); + File[] files = new File(this.dbPath).listFiles(new FileFilter() { + @Override + public boolean accept(File pathname) { + return pathname.isFile(); + } + }); + assertEquals(0, files.length); assertEquals(0, database.getDatabaseSize()); assertEquals(0, database.count()); } diff --git a/backtrace-library/src/main/java/backtraceio/library/BacktraceDatabase.java b/backtrace-library/src/main/java/backtraceio/library/BacktraceDatabase.java index 1559181d..23504a90 100644 --- a/backtrace-library/src/main/java/backtraceio/library/BacktraceDatabase.java +++ b/backtrace-library/src/main/java/backtraceio/library/BacktraceDatabase.java @@ -12,6 +12,7 @@ import backtraceio.library.base.BacktraceBase; import backtraceio.library.breadcrumbs.BacktraceBreadcrumbs; import backtraceio.library.common.FileHelper; +import backtraceio.library.common.TypeHelper; import backtraceio.library.enums.UnwindingMode; import backtraceio.library.enums.database.RetryBehavior; import backtraceio.library.events.OnServerResponseEventListener; @@ -50,6 +51,7 @@ public class BacktraceDatabase implements Database { private boolean _enable = false; private Breadcrumbs breadcrumbs; + private boolean _enabledNativeIntegration = false; /** * Add attributes to native reports * @@ -167,7 +169,7 @@ public Boolean setupNativeIntegration(BacktraceBase client, BacktraceCredentials public Boolean setupNativeIntegration(BacktraceBase client, BacktraceCredentials credentials, boolean enableClientSideUnwinding, UnwindingMode unwindingMode) { // avoid initialization when database doesn't exist - if (getSettings() == null) { + if (_enable == false || getSettings() == null) { return false; } String minidumpSubmissionUrl = credentials.getMinidumpSubmissionUrl().toString(); @@ -202,7 +204,7 @@ public Boolean setupNativeIntegration(BacktraceBase client, BacktraceCredentials File crashHandlerDir = new File(databasePath); crashHandlerDir.mkdir(); - Boolean initialized = initialize( + _enabledNativeIntegration = initialize( minidumpSubmissionUrl, databasePath, handlerPath, @@ -213,12 +215,12 @@ public Boolean setupNativeIntegration(BacktraceBase client, BacktraceCredentials unwindingMode ); - if (initialized && this.breadcrumbs.isEnabled()) { + if (_enabledNativeIntegration && this.breadcrumbs.isEnabled()) { this.breadcrumbs.setOnSuccessfulBreadcrumbAddEventListener(breadcrumbId -> { this.addAttribute("breadcrumbs.lastId", Long.toString((breadcrumbId))); }); } - return initialized; + return _enabledNativeIntegration; } /** @@ -227,6 +229,7 @@ public Boolean setupNativeIntegration(BacktraceBase client, BacktraceCredentials @Override public void disableNativeIntegration() { disable(); + this._enabledNativeIntegration = false; } @Override @@ -234,6 +237,22 @@ public Breadcrumbs getBreadcrumbs() { return this.breadcrumbs; } + public Boolean addNativeAttribute(String key, Object value) { + if (!_enabledNativeIntegration) { + return false; + } + + if (key == null || value == null) { + return false; + } + Class type = value.getClass(); + if (!TypeHelper.isPrimitiveOrPrimitiveWrapperOrString(type)) { + return false; + } + addAttribute(key, value.toString()); + return true; + } + public void start() { if (databaseSettings == null) { return; diff --git a/backtrace-library/src/main/java/backtraceio/library/base/BacktraceBase.java b/backtrace-library/src/main/java/backtraceio/library/base/BacktraceBase.java index b3b88ef8..a3970f3d 100644 --- a/backtrace-library/src/main/java/backtraceio/library/base/BacktraceBase.java +++ b/backtrace-library/src/main/java/backtraceio/library/base/BacktraceBase.java @@ -296,6 +296,41 @@ public Map getAttributes() { return attributes; } + /** + * Adds a new attribute to Backtrace Client. If the native integration is enabled, adds the attribute + * to the native report attributes if: + * - the value exists (is not a null) + * - is not an object (the attribute value is primitive type like String, or Int) + * + * @param key attribute name + * @param value attribute value. + */ + public void addAttribute(String key, Object value) { + attributes.put(key, value); + if (database == null) { + return; + } + + database.addNativeAttribute(key, value); + } + + /** + * Adds new attributes to Backtrace Client. If the native integration is enabled, adds attributes + * to the native report attributes if: + * - the value exists (is not a null) + * - is not an object (the attribute value is primitive type like String, or Int) + * + * @param attributes Map of attributes + */ + public void addAttribute(Map attributes) { + if (attributes == null) { + return; + } + for (Map.Entry entry : attributes.entrySet()) { + addAttribute(entry.getKey(), entry.getValue()); + } + } + /** * Set event executed before sending data to Backtrace API * diff --git a/backtrace-library/src/main/java/backtraceio/library/interfaces/Client.java b/backtrace-library/src/main/java/backtraceio/library/interfaces/Client.java index 6785495e..3b639bf1 100644 --- a/backtrace-library/src/main/java/backtraceio/library/interfaces/Client.java +++ b/backtrace-library/src/main/java/backtraceio/library/interfaces/Client.java @@ -1,5 +1,7 @@ package backtraceio.library.interfaces; +import java.util.Map; + import backtraceio.library.models.json.BacktraceReport; /** @@ -17,4 +19,21 @@ public interface Client { * Capture unhandled native exceptions (Backtrace database integration is required to enable this feature). */ void enableNativeIntegration(); + + /** + * Adds new attributes to the client. + * If the native integration is available and attributes are primitive type, + * they will be added to the native reports. + * @param attributes client Attributes + */ + void addAttribute(Map attributes); + + /** + * Adds new attribute to the client. + * If the native integration is available and attributes are primitive type, + * they will be added to the native reports. + * @param key attribute key + * @param value attribute value + */ + void addAttribute(String key, Object value); } diff --git a/backtrace-library/src/main/java/backtraceio/library/interfaces/Database.java b/backtrace-library/src/main/java/backtraceio/library/interfaces/Database.java index 3ee5f67a..751567d3 100644 --- a/backtrace-library/src/main/java/backtraceio/library/interfaces/Database.java +++ b/backtrace-library/src/main/java/backtraceio/library/interfaces/Database.java @@ -120,4 +120,13 @@ Boolean setupNativeIntegration(BacktraceBase client, BacktraceCredentials creden * @return the breadcrumbs implementation for this Database, if any */ Breadcrumbs getBreadcrumbs(); + + /** + * If the native integration is enabled and a value is a primitive type, + * adds a new attribute to the native integration. + * @param key attribute key + * @param value attribute value + * @return true, if attribute was added to the native report. Otherwise false. + */ + Boolean addNativeAttribute(String key, Object value); }