Skip to content

Commit 1ebcf4a

Browse files
kholood-eaa7medev
andauthored
feat(android): add session sync callback (#1281)
* feat(android): add SRSyncCallback * feat: implement and test syncCallback CP side * feat(example): use SRSyncCallback in example app * ci: fix tests * fix: export session data type * fix(example): use session data type * fix(android):remove data modifier * fix(android): add property modifiers * fix(android): update test case * fix: enhance test case * fix: update session data type * fix: add more session metadata to setSyncCallback * fix: update syncCallback test * feat: add launchType to session metadata for setSyncCallback * fix: import type * fix: assert evaluate sync returns correct value * fix: import type * fix: cleanup * chore: update js doc * fix: typo * fix: follow interface naming convention * fix: update type * fix: refactor syncCallback * fix: default syncing session to true * fix: convert network logs to readable array * chore: add discriptive comment * chore: use readable map for session metadata * fix: setSyncCallback should sync in case of exception * fix: move SessionMetadata to models * fix: update SessionMetadata type import * fix: report bug e2e test --------- Co-authored-by: Ahmed Elrefaey <[email protected]>
1 parent 1f64e19 commit 1ebcf4a

File tree

15 files changed

+333
-24
lines changed

15 files changed

+333
-24
lines changed

android/native.gradle

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
project.ext.instabug = [
2-
version: '13.3.0'
2+
version: '13.3.0.6212131-SNAPSHOT',
33
]
44

55
dependencies {

android/src/main/java/com/instabug/reactlibrary/ArgsRegistry.java

+17
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
import com.instabug.library.invocation.InstabugInvocationEvent;
1616
import com.instabug.library.invocation.util.InstabugFloatingButtonEdge;
1717
import com.instabug.library.invocation.util.InstabugVideoRecordingButtonPosition;
18+
import com.instabug.library.sessionreplay.model.SessionMetadata;
1819
import com.instabug.library.ui.onboarding.WelcomeMessage;
1920

2021
import java.util.ArrayList;
@@ -58,6 +59,7 @@ static Map<String, Object> getAll() {
5859
putAll(nonFatalExceptionLevel);
5960
putAll(locales);
6061
putAll(placeholders);
62+
putAll(launchType);
6163
}};
6264
}
6365

@@ -238,4 +240,19 @@ static Map<String, Object> getAll() {
238240
put("team", Key.CHATS_TEAM_STRING_NAME);
239241
put("insufficientContentMessage", Key.COMMENT_FIELD_INSUFFICIENT_CONTENT);
240242
}};
243+
244+
public static ArgsMap<String> launchType = new ArgsMap<String>() {{
245+
put("cold", SessionMetadata.LaunchType.COLD);
246+
put("hot",SessionMetadata.LaunchType.HOT );
247+
put("warm",SessionMetadata.LaunchType.WARM );
248+
}};
249+
250+
// Temporary workaround to be removed in future release
251+
// This is used for mapping native `LaunchType` values into React Native enum values.
252+
public static HashMap<String,String> launchTypeReversed = new HashMap<String,String>() {{
253+
put(SessionMetadata.LaunchType.COLD,"cold");
254+
put(SessionMetadata.LaunchType.HOT,"hot" );
255+
put(SessionMetadata.LaunchType.WARM,"warm" );
256+
}};
257+
241258
}

android/src/main/java/com/instabug/reactlibrary/Constants.java

+2
Original file line numberDiff line numberDiff line change
@@ -9,4 +9,6 @@ final class Constants {
99

1010
final static String IBG_ON_NEW_MESSAGE_HANDLER = "IBGonNewMessageHandler";
1111
final static String IBG_ON_NEW_REPLY_RECEIVED_CALLBACK = "IBGOnNewReplyReceivedCallback";
12+
final static String IBG_SESSION_REPLAY_ON_SYNC_CALLBACK_INVOCATION = "IBGSessionReplayOnSyncCallback";
13+
1214
}

android/src/main/java/com/instabug/reactlibrary/RNInstabugSessionReplayModule.java

+103-4
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,44 @@
11
package com.instabug.reactlibrary;
22

3+
4+
import androidx.annotation.NonNull;
35
import androidx.annotation.Nullable;
46

7+
import com.facebook.react.bridge.Arguments;
58
import com.facebook.react.bridge.Promise;
69
import com.facebook.react.bridge.ReactApplicationContext;
7-
import com.facebook.react.bridge.ReactContextBaseJavaModule;
810
import com.facebook.react.bridge.ReactMethod;
9-
import com.instabug.chat.Replies;
11+
import com.facebook.react.bridge.ReadableArray;
12+
import com.facebook.react.bridge.WritableArray;
13+
import com.facebook.react.bridge.WritableMap;
1014
import com.instabug.library.OnSessionReplayLinkReady;
15+
import com.instabug.library.SessionSyncListener;
1116
import com.instabug.library.sessionreplay.SessionReplay;
17+
import com.instabug.library.sessionreplay.model.SessionMetadata;
18+
import com.instabug.reactlibrary.utils.EventEmitterModule;
1219
import com.instabug.reactlibrary.utils.MainThreadHandler;
20+
import java.util.ArrayList;
21+
import java.util.List;
22+
import java.util.concurrent.CountDownLatch;
1323

1424
import javax.annotation.Nonnull;
1525

16-
public class RNInstabugSessionReplayModule extends ReactContextBaseJavaModule {
26+
public class RNInstabugSessionReplayModule extends EventEmitterModule {
1727

1828
public RNInstabugSessionReplayModule(ReactApplicationContext reactApplicationContext) {
1929
super(reactApplicationContext);
2030
}
2131

32+
@ReactMethod
33+
public void addListener(String event) {
34+
super.addListener(event);
35+
}
36+
37+
@ReactMethod
38+
public void removeListeners(Integer count) {
39+
super.removeListeners(count);
40+
}
41+
2242
@Nonnull
2343
@Override
2444
public String getName() {
@@ -79,7 +99,7 @@ public void run() {
7999
e.printStackTrace();
80100
}
81101
}
82-
});
102+
});
83103
}
84104

85105
@ReactMethod
@@ -97,6 +117,85 @@ public void onSessionReplayLinkReady(@Nullable String link) {
97117
}
98118
});
99119

120+
}
121+
122+
public ReadableMap getSessionMetadataMap(SessionMetadata sessionMetadata){
123+
WritableMap params = Arguments.createMap();
124+
params.putString("appVersion",sessionMetadata.getAppVersion());
125+
params.putString("OS",sessionMetadata.getOs());
126+
params.putString("device",sessionMetadata.getDevice());
127+
params.putDouble("sessionDurationInSeconds",(double)sessionMetadata.getSessionDurationInSeconds());
128+
params.putBoolean("hasLinkToAppReview",sessionMetadata.getLinkedToReview());
129+
params.putString("launchType",ArgsRegistry.launchTypeReversed.get(sessionMetadata.getLaunchType()) );
130+
params.putDouble("launchDuration", sessionMetadata.getLaunchDuration());
131+
params.putArray("networkLogs",getNetworkLogsArray(sessionMetadata.getNetworkLogs()));
132+
133+
// TODO:Add rest of sessionMetadata
134+
// params.putDouble("bugsCount", ??);
135+
// params.putDouble("fatalCrashCount",??);
136+
// params.putDouble("oomCrashCount",??);
137+
return params;
138+
}
139+
140+
public ReadableArray getNetworkLogsArray(List<SessionMetadata.NetworkLog> networkLogList ){
141+
WritableArray networkLogs = Arguments.createArray();
142+
143+
for (SessionMetadata.NetworkLog log : networkLogList) {
144+
WritableMap networkLog = Arguments.createMap();
145+
networkLog.putString("url", log.getUrl());
146+
networkLog.putDouble("duration", log.getDuration());
147+
networkLog.putInt("statusCode", log.getStatusCode());
148+
149+
networkLogs.pushMap(networkLog);
150+
}
151+
152+
return networkLogs;
153+
}
154+
155+
private boolean shouldSync = true;
156+
private CountDownLatch latch;
157+
@ReactMethod
158+
public void setSyncCallback() {
159+
MainThreadHandler.runOnMainThread(new Runnable() {
160+
@Override
161+
public void run() {
162+
try {
163+
SessionReplay.setSyncCallback(new SessionSyncListener() {
164+
@Override
165+
public boolean onSessionReadyToSync(@NonNull SessionMetadata sessionMetadata) {
166+
167+
sendEvent(Constants.IBG_SESSION_REPLAY_ON_SYNC_CALLBACK_INVOCATION,getSessionMetadataMap(sessionMetadata));
100168

169+
latch = new CountDownLatch(1);
170+
171+
try {
172+
latch.await();
173+
} catch (InterruptedException e) {
174+
e.printStackTrace();
175+
return true;
176+
}
177+
178+
return shouldSync;
179+
}
180+
});
181+
}
182+
catch(Exception e){
183+
e.printStackTrace();
184+
}
185+
186+
}
187+
});
101188
}
189+
190+
@ReactMethod
191+
public void evaluateSync(boolean result) {
192+
shouldSync = result;
193+
194+
if (latch != null) {
195+
latch.countDown();
196+
}
197+
}
198+
199+
200+
102201
}

android/src/test/java/com/instabug/reactlibrary/RNInstabugSessionReplayModuleTest.java

+52-13
Original file line numberDiff line numberDiff line change
@@ -1,40 +1,44 @@
11
package com.instabug.reactlibrary;
22

3+
import static junit.framework.TestCase.assertEquals;
4+
import static junit.framework.TestCase.assertTrue;
5+
6+
import static org.mockito.ArgumentMatchers.eq;
37
import static org.mockito.Matchers.any;
48
import static org.mockito.Mockito.doAnswer;
59
import static org.mockito.Mockito.mock;
10+
import static org.mockito.Mockito.mockConstruction;
611
import static org.mockito.Mockito.mockStatic;
7-
import static org.mockito.Mockito.timeout;
8-
import static org.mockito.Mockito.times;
12+
import static org.mockito.Mockito.spy;
913
import static org.mockito.Mockito.verify;
1014
import static org.mockito.Mockito.when;
1115

12-
import android.os.Handler;
1316
import android.os.Looper;
1417

1518
import com.facebook.react.bridge.Arguments;
16-
import com.facebook.react.bridge.JavaOnlyArray;
19+
import com.facebook.react.bridge.JavaOnlyMap;
1720
import com.facebook.react.bridge.Promise;
18-
import com.facebook.react.bridge.ReadableArray;
19-
import com.facebook.react.bridge.WritableArray;
20-
import com.instabug.chat.Replies;
21-
import com.instabug.featuresrequest.ActionType;
22-
import com.instabug.featuresrequest.FeatureRequests;
23-
import com.instabug.library.Feature;
21+
import com.facebook.react.bridge.ReactApplicationContext;
22+
import com.facebook.react.bridge.WritableMap;
2423
import com.instabug.library.OnSessionReplayLinkReady;
24+
import com.instabug.library.SessionSyncListener;
2525
import com.instabug.library.sessionreplay.SessionReplay;
26+
import com.instabug.library.sessionreplay.model.SessionMetadata;
2627
import com.instabug.reactlibrary.utils.MainThreadHandler;
2728

2829
import org.junit.After;
2930
import org.junit.Before;
3031
import org.junit.Test;
32+
import org.mockito.MockedConstruction;
3133
import org.mockito.MockedStatic;
3234
import org.mockito.Mockito;
3335
import org.mockito.invocation.InvocationOnMock;
3436
import org.mockito.stubbing.Answer;
3537

38+
import java.util.concurrent.CountDownLatch;
3639
import java.util.concurrent.Executors;
3740
import java.util.concurrent.ScheduledExecutorService;
41+
import java.util.concurrent.atomic.AtomicBoolean;
3842

3943

4044
public class RNInstabugSessionReplayModuleTest {
@@ -44,8 +48,8 @@ public class RNInstabugSessionReplayModuleTest {
4448

4549
// Mock Objects
4650
private MockedStatic<Looper> mockLooper;
47-
private MockedStatic <MainThreadHandler> mockMainThreadHandler;
48-
private MockedStatic <SessionReplay> mockSessionReplay;
51+
private MockedStatic<MainThreadHandler> mockMainThreadHandler;
52+
private MockedStatic<SessionReplay> mockSessionReplay;
4953

5054
@Before
5155
public void mockMainThreadHandler() throws Exception {
@@ -107,7 +111,7 @@ public void testSetInstabugLogsEnabled() {
107111
@Test
108112
public void testGetSessionReplayLink() {
109113
Promise promise = mock(Promise.class);
110-
String link="instabug link";
114+
String link = "instabug link";
111115

112116
mockSessionReplay.when(() -> SessionReplay.getSessionReplayLink(any())).thenAnswer(
113117
invocation -> {
@@ -136,5 +140,40 @@ public void testSetUserStepsEnabled() {
136140
mockSessionReplay.verifyNoMoreInteractions();
137141
}
138142

143+
@Test
144+
public void testSetSyncCallback() throws Exception {
145+
MockedStatic<Arguments> mockArguments = mockStatic(Arguments.class);
146+
MockedConstruction<CountDownLatch> mockCountDownLatch = mockConstruction(CountDownLatch.class);
147+
RNInstabugSessionReplayModule SRModule = spy(new RNInstabugSessionReplayModule(mock(ReactApplicationContext.class)));
148+
149+
final boolean shouldSync = true;
150+
final AtomicBoolean actual = new AtomicBoolean();
151+
152+
mockArguments.when(Arguments::createMap).thenReturn(new JavaOnlyMap());
153+
154+
mockSessionReplay.when(() -> SessionReplay.setSyncCallback(any(SessionSyncListener.class)))
155+
.thenAnswer((invocation) -> {
156+
SessionSyncListener listener = (SessionSyncListener) invocation.getArguments()[0];
157+
SessionMetadata metadata = mock(SessionMetadata.class);
158+
actual.set(listener.onSessionReadyToSync(metadata));
159+
return null;
160+
});
161+
162+
doAnswer((invocation) -> {
163+
SRModule.evaluateSync(shouldSync);
164+
return null;
165+
}).when(SRModule).sendEvent(eq(Constants.IBG_SESSION_REPLAY_ON_SYNC_CALLBACK_INVOCATION), any());
166+
167+
WritableMap params = Arguments.createMap();
168+
169+
SRModule.setSyncCallback();
170+
171+
assertEquals(shouldSync, actual.get());
172+
verify(SRModule).sendEvent(Constants.IBG_SESSION_REPLAY_ON_SYNC_CALLBACK_INVOCATION, params);
173+
mockSessionReplay.verify(() -> SessionReplay.setSyncCallback(any(SessionSyncListener.class)));
174+
175+
mockArguments.close();
176+
mockCountDownLatch.close();
177+
}
139178

140179
}

examples/default/e2e/reportBug.e2e.ts

+3-1
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,9 @@ it('reports a bug', async () => {
1414
await waitFor(floatingButton).toBeVisible().withTimeout(30000);
1515
await floatingButton.tap();
1616

17-
await getElement('reportBugMenuItem').tap();
17+
const reportBugMenuItemButton = getElement('reportBugMenuItem');
18+
await waitFor(reportBugMenuItemButton).toBeVisible().withTimeout(30000);
19+
await reportBugMenuItemButton.tap();
1820

1921
await getElement('emailField').typeText(mockData.email);
2022
await getElement('commentField').typeText(mockData.bugComment);

examples/default/src/App.tsx

+19
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,10 @@ import Instabug, {
88
InvocationEvent,
99
LogLevel,
1010
ReproStepsMode,
11+
SessionReplay,
12+
LaunchType,
1113
} from 'instabug-reactnative';
14+
import type { SessionMetadata } from 'instabug-reactnative';
1215
import { NativeBaseProvider } from 'native-base';
1316

1417
import { RootTabNavigator } from './navigation/RootTab';
@@ -20,8 +23,24 @@ import { QueryClient, QueryClientProvider } from 'react-query';
2023
const queryClient = new QueryClient();
2124

2225
export const App: React.FC = () => {
26+
const shouldSyncSession = (data: SessionMetadata) => {
27+
if (data.launchType === LaunchType.cold) {
28+
return true;
29+
}
30+
if (data.sessionDurationInSeconds > 20) {
31+
return true;
32+
}
33+
if (data.OS === 'OS Level 34') {
34+
return true;
35+
}
36+
return false;
37+
};
38+
2339
const navigationRef = useNavigationContainerRef();
40+
2441
useEffect(() => {
42+
SessionReplay.setSyncCallback((data) => shouldSyncSession(data));
43+
2544
Instabug.init({
2645
token: 'deb1910a7342814af4e4c9210c786f35',
2746
invocationEvents: [InvocationEvent.floatingButton],

src/index.ts

+2-1
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import * as Replies from './modules/Replies';
1414
import type { Survey } from './modules/Surveys';
1515
import * as Surveys from './modules/Surveys';
1616
import * as SessionReplay from './modules/SessionReplay';
17+
import type { SessionMetadata } from './models/SessionMetadata';
1718

1819
export * from './utils/Enums';
1920
export {
@@ -28,6 +29,6 @@ export {
2829
Replies,
2930
Surveys,
3031
};
31-
export type { InstabugConfig, Survey, NetworkData, NetworkDataObfuscationHandler };
32+
export type { InstabugConfig, Survey, NetworkData, NetworkDataObfuscationHandler, SessionMetadata };
3233

3334
export default Instabug;

0 commit comments

Comments
 (0)