diff --git a/README.md b/README.md index 68055707b..b6bb24b34 100644 --- a/README.md +++ b/README.md @@ -2224,3 +2224,34 @@ Alternatively, a different folder can be specified by setting the `MLS_ASSETS_FO For a form named `example_form` the generated assets will follow the following naming convention : `example_form.json` and `example_form.properties`. The properties file can then be copied over to the `resources` folder of your Android project under `src/main`. The placeholder-injected JsonForm will typically be copied over to the `assets` folder of your Android project (although not mandatory). + +## How to use Sentry for OpenSRP Client Performance Monitoring + +This section explains how to setup performance monitoring with Sentry on opensrp clients if you have added opensrp-native-forms library as a dependency. Instructions are given in this article on how to initialize Sentry in the opensrp client apps and how to check for the performance data or transactions on the Sentry dashboard. + +> ### Note +> +> * You need to have already setup a [project](https://docs.sentry.io/product/sentry-basics/integrate-frontend/create-new-project/) and dashboard with the necessary access permissions, and +> * are able to obtain the dsn of this project through `[Project] > Settings > Client Keys (DSN)`. This will be required later for configuration and logging. + +### Instructions + +1. Add and update opensrp-native-forms library to version above v3.0.4-SNAPSHOT + +2. Sync and rebuild project to ensure new dependencies are loaded. Sentry would come as dependency to opensrp-native-forms and would load up automatically. Run your application, and confirm no logs are sent to the sentry dashboard + +3. There are a couple of configurations that would need to be setup first for logs to. show up to the dashboard. These are: + * DSN (Data Source Name); you can find your DSN in your Sentry project settings by navigating to `[Project] > Settings > Client Keys (DSN)` in sentry.io. + + * Environment; unique name that would be used to tag logs + + In your app’s Application class, add the following code snippet in onCreate with the placeholder values replaced . Run your application once more + + ```Java + SentryAndroid.init(this, options -> { + options.setEnvironment("YOUR_ENVIRONMENT_NAME"); + options.setDsn("YOUR_DSN_NAME_LINK"); + }) + ``` + +4. Logs in performance monitoring should show up in the Sentry dashboard. Use the Environment tag set from the previous step to filter environment. diff --git a/android-json-form-wizard/build.gradle b/android-json-form-wizard/build.gradle index 60f0faed7..c1867697d 100644 --- a/android-json-form-wizard/build.gradle +++ b/android-json-form-wizard/build.gradle @@ -33,6 +33,13 @@ android { multiDexEnabled true testInstrumentationRunner "com.android.test.runner.MultiDexTestRunner" + javaCompileOptions { + annotationProcessorOptions { + includeCompileClasspath = true + } + } + + multiDexKeepProguard file('multidex-config.pro') } buildTypes { @@ -58,6 +65,14 @@ android { packagingOptions { exclude 'META-INF/DEPENDENCIES' } + configurations.all { + resolutionStrategy.force 'com.android.support:design:28.0.0' + } + + compileOptions { + sourceCompatibility = JavaVersion.VERSION_1_8 + targetCompatibility = JavaVersion.VERSION_1_8 + } } @@ -137,6 +152,8 @@ dependencies { implementation "org.greenrobot:eventbus:3.2.0" implementation 'androidx.multidex:multidex:2.0.1' + api 'io.sentry:sentry-android:5.0.1' + // PowerMock def powerMockVersion = '2.0.9' testImplementation "org.powermock:powermock-module-junit4:$powerMockVersion" @@ -210,3 +227,6 @@ task javadoc(type: Javadoc) { classpath += project.files(android.getBootClasspath().join(File.pathSeparator)) classpath += configurations.compile } + + + diff --git a/android-json-form-wizard/src/main/AndroidManifest.xml b/android-json-form-wizard/src/main/AndroidManifest.xml index 9bb98439f..abfe7dcb7 100644 --- a/android-json-form-wizard/src/main/AndroidManifest.xml +++ b/android-json-form-wizard/src/main/AndroidManifest.xml @@ -24,6 +24,7 @@ android:theme="@style/NativeFormsAppTheme" tools:replace="android:theme"> + + diff --git a/android-json-form-wizard/src/main/java/com/vijay/jsonwizard/customviews/DatePickerDialog.java b/android-json-form-wizard/src/main/java/com/vijay/jsonwizard/customviews/DatePickerDialog.java index b7b14f1d5..e00d2afdd 100644 --- a/android-json-form-wizard/src/main/java/com/vijay/jsonwizard/customviews/DatePickerDialog.java +++ b/android-json-form-wizard/src/main/java/com/vijay/jsonwizard/customviews/DatePickerDialog.java @@ -6,6 +6,8 @@ import android.content.DialogInterface; import android.os.Bundle; import androidx.annotation.Nullable; +import androidx.annotation.VisibleForTesting; + import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; @@ -27,6 +29,10 @@ public class DatePickerDialog extends DialogFragment { private DatePicker datePicker; + + private Button okButton; + + private Button cancelButton; private android.app.DatePickerDialog.OnDateSetListener onDateSetListener; private DialogInterface.OnShowListener onShowListener; private Date date; @@ -67,9 +73,6 @@ public void setOnShowListener(DialogInterface.OnShowListener onShowListener_) { public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { ViewGroup dialogView = (ViewGroup) inflater.inflate(R.layout.native_form_dialog_date_picker, container, false); - Button cancelButton; - Button okButton; - setOnShowListener(new DialogInterface.OnShowListener() { @Override public void onShow(DialogInterface dialog) { @@ -179,4 +182,14 @@ public DatePicker getDatePicker() { public void setNumericDatePicker(boolean numericDatePicker) { isNumericDatePicker = numericDatePicker; } + + @VisibleForTesting(otherwise = VisibleForTesting.PROTECTED) + public Button getOkButton(){ + return okButton; + } + + @VisibleForTesting(otherwise = VisibleForTesting.PROTECTED) + public Button getCancelButton(){ + return cancelButton; + } } diff --git a/android-json-form-wizard/src/main/java/com/vijay/jsonwizard/utils/FormUtils.java b/android-json-form-wizard/src/main/java/com/vijay/jsonwizard/utils/FormUtils.java index 6744f8d74..00cefe542 100644 --- a/android-json-form-wizard/src/main/java/com/vijay/jsonwizard/utils/FormUtils.java +++ b/android-json-form-wizard/src/main/java/com/vijay/jsonwizard/utils/FormUtils.java @@ -1139,28 +1139,30 @@ public JSONArray getSecondaryValues(JSONObject jsonObject, String type) { */ public JSONArray getFormFields(String stepName, Context context) { Activity activity = (Activity) context; - JsonApi jsonApi = (JsonApi) activity; JSONArray fields = new JSONArray(); - JSONObject mJSONObject = jsonApi.getmJSONObject(); - if (mJSONObject != null) { - JSONObject parentJson = jsonApi.getStep(stepName); - try { - if (parentJson.has(JsonFormConstants.SECTIONS) && parentJson - .get(JsonFormConstants.SECTIONS) instanceof JSONArray) { - JSONArray sections = parentJson.getJSONArray(JsonFormConstants.SECTIONS); - for (int i = 0; i < sections.length(); i++) { - JSONObject sectionJson = sections.getJSONObject(i); - if (sectionJson.has(JsonFormConstants.FIELDS)) { - fields = concatArray(fields, sectionJson.getJSONArray(JsonFormConstants.FIELDS)); + if (activity instanceof JsonApi) { + JsonApi jsonApi = (JsonApi) activity; + JSONObject mJSONObject = jsonApi.getmJSONObject(); + if (mJSONObject != null) { + JSONObject parentJson = jsonApi.getStep(stepName); + try { + if (parentJson.has(JsonFormConstants.SECTIONS) && parentJson + .get(JsonFormConstants.SECTIONS) instanceof JSONArray) { + JSONArray sections = parentJson.getJSONArray(JsonFormConstants.SECTIONS); + for (int i = 0; i < sections.length(); i++) { + JSONObject sectionJson = sections.getJSONObject(i); + if (sectionJson.has(JsonFormConstants.FIELDS)) { + fields = concatArray(fields, sectionJson.getJSONArray(JsonFormConstants.FIELDS)); + } } - } - } else if (parentJson.has(JsonFormConstants.FIELDS) && parentJson - .get(JsonFormConstants.FIELDS) instanceof JSONArray) { - fields = parentJson.getJSONArray(JsonFormConstants.FIELDS); + } else if (parentJson.has(JsonFormConstants.FIELDS) && parentJson + .get(JsonFormConstants.FIELDS) instanceof JSONArray) { + fields = parentJson.getJSONArray(JsonFormConstants.FIELDS); + } + } catch (JSONException e) { + Timber.e(e); } - } catch (JSONException e) { - Timber.e(e); } } return fields; diff --git a/android-json-form-wizard/src/main/java/com/vijay/jsonwizard/widgets/NativeRadioButtonFactory.java b/android-json-form-wizard/src/main/java/com/vijay/jsonwizard/widgets/NativeRadioButtonFactory.java index ad4d459b0..638fc2535 100644 --- a/android-json-form-wizard/src/main/java/com/vijay/jsonwizard/widgets/NativeRadioButtonFactory.java +++ b/android-json-form-wizard/src/main/java/com/vijay/jsonwizard/widgets/NativeRadioButtonFactory.java @@ -64,7 +64,7 @@ */ public class NativeRadioButtonFactory implements FormWidgetFactory { - private static final String TAG = NativeRadioButtonFactory.class.getCanonicalName(); + public static final String TAG = NativeRadioButtonFactory.class.getCanonicalName(); private final FormUtils formUtils = new FormUtils(); private final CustomTextViewClickListener customTextViewClickListener = new CustomTextViewClickListener(); private RadioButton radioButton; diff --git a/android-json-form-wizard/src/multidex-config.pro b/android-json-form-wizard/src/multidex-config.pro new file mode 100644 index 000000000..94584adff --- /dev/null +++ b/android-json-form-wizard/src/multidex-config.pro @@ -0,0 +1,2 @@ +-keep class io.sentry.android.core.SentryAndroidOptions +-keep class io.sentry.android.ndk.SentryNdk \ No newline at end of file diff --git a/android-json-form-wizard/src/test/java/com/vijay/jsonwizard/presenters/JsonWizardFormFragmentPresenterRoboelectricTest.java b/android-json-form-wizard/src/test/java/com/vijay/jsonwizard/presenters/JsonWizardFormFragmentPresenterRoboelectricTest.java index 0c5579ab2..6913ebdbd 100644 --- a/android-json-form-wizard/src/test/java/com/vijay/jsonwizard/presenters/JsonWizardFormFragmentPresenterRoboelectricTest.java +++ b/android-json-form-wizard/src/test/java/com/vijay/jsonwizard/presenters/JsonWizardFormFragmentPresenterRoboelectricTest.java @@ -1,12 +1,12 @@ package com.vijay.jsonwizard.presenters; import android.app.Activity; +import android.app.FragmentManager; import android.content.Context; import android.content.Intent; import android.content.pm.PackageManager; import android.net.Uri; import android.provider.MediaStore; -import androidx.appcompat.app.AppCompatActivity; import android.view.LayoutInflater; import android.view.View; import android.widget.LinearLayout; @@ -48,6 +48,7 @@ import static com.vijay.jsonwizard.presenters.JsonFormFragmentPresenter.RESULT_LOAD_IMG; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.spy; @@ -159,10 +160,10 @@ public void testOnClickShouldSOpenPictureTakingActivity() { } @Test - public void testOnClickShouldDisplayDatePickerDialog() { + public void testOnClickShouldDisplayDatePickerDialog() throws InterruptedException { LinearLayout view = (LinearLayout) LayoutInflater.from(context).inflate(R.layout.native_form_compound_button_parent, null); CustomTextView customTextView = new CustomTextView(context); - Activity activity = Robolectric.buildActivity(AppCompatActivity.class).create().get(); + Activity activity = Robolectric.buildActivity(Activity.class).create().get(); RadioButton radioButton = new RadioButton(context); view.setTag(R.id.specify_textview, customTextView); view.setTag(R.id.native_radio_button, radioButton); @@ -175,9 +176,17 @@ public void testOnClickShouldDisplayDatePickerDialog() { view.setTag(R.id.specify_widget, JsonFormConstants.DATE_PICKER); view.setTag(R.id.option_json_object, new JSONObject()); formFragmentPresenter.onClick(view); - DatePickerDialog dialogFragment = (DatePickerDialog) activity.getFragmentManager() - .findFragmentByTag(NativeRadioButtonFactory.class.getCanonicalName()); - assertNotNull(view); + Thread.sleep(3000); + DatePickerDialog dialogFragment = null; + FragmentManager fragmentManager = activity.getFragmentManager(); + if (fragmentManager != null){ + dialogFragment = (DatePickerDialog) fragmentManager.findFragmentByTag(NativeRadioButtonFactory.TAG); + } + assertNotNull(dialogFragment); + + View okButton = dialogFragment.getOkButton(); + okButton.performClick(); + assertTrue(radioButton.getText().length() > 0); } } diff --git a/gradle.properties b/gradle.properties index 5abb92c86..906886943 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,4 +1,4 @@ -VERSION_NAME=3.1.1-SNAPSHOT +VERSION_NAME=3.2.0-SNAPSHOT VERSION_CODE=1 GROUP=org.smartregister POM_SETTING_DESCRIPTION=OpenSRP Client Native Form Json Wizard diff --git a/sample/build.gradle b/sample/build.gradle index 81bfb98d6..5f5e2113b 100644 --- a/sample/build.gradle +++ b/sample/build.gradle @@ -29,6 +29,8 @@ android { versionCode 1 versionName "1.0" multiDexEnabled true + + buildConfigField("String", "SENTRY_DSN", "\"" + getProps("sentry.dsn") + "\"") } buildTypes { @@ -60,3 +62,14 @@ dependencies { implementation 'org.smartregister:opensrp-client-utils:0.0.6-SNAPSHOT' } + +// get property from local.properties +def getProps(String propName) { + def propsFile = rootProject.file('local.properties') + if (propsFile.exists()) { + def props = new Properties() + props.load(new FileInputStream(propsFile)) + return props[propName] + } + return ""; +} diff --git a/sample/src/main/AndroidManifest.xml b/sample/src/main/AndroidManifest.xml index 75c11a6ab..1eae6330d 100644 --- a/sample/src/main/AndroidManifest.xml +++ b/sample/src/main/AndroidManifest.xml @@ -11,6 +11,7 @@ android:icon="@mipmap/ic_launcher" android:label="@string/app_name" android:supportsRtl="true" + android:name=".MainApplication" android:theme="@style/NativeFormsAppTheme"> { + options.setEnvironment("opensrp-native-form-sample"); + options.setDsn(BuildConfig.SENTRY_DSN.trim()); + }); + } + + } +}