Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
77b8993
feat(android): add SRSyncCallback
kholood-ea Aug 28, 2024
e6b816f
feat: implement and test syncCallback CP side
kholood-ea Aug 28, 2024
bad72d6
feat(example): use SRSyncCallback in example app
kholood-ea Aug 28, 2024
2b01c9d
ci: fix tests
kholood-ea Aug 28, 2024
61d6e52
fix: export session data type
kholood-ea Sep 2, 2024
849648a
fix(example): use session data type
kholood-ea Sep 2, 2024
02c621c
fix(android):remove data modifier
kholood-ea Sep 2, 2024
0d9c7f1
fix(android): add property modifiers
kholood-ea Sep 2, 2024
261aa76
fix(android): update test case
kholood-ea Sep 2, 2024
5056f58
fix: enhance test case
kholood-ea Sep 5, 2024
2ff447f
fix: update session data type
kholood-ea Sep 9, 2024
131a795
fix: add more session metadata to setSyncCallback
kholood-ea Sep 10, 2024
2b53766
fix: update syncCallback test
kholood-ea Sep 10, 2024
41e6b52
feat: add launchType to session metadata for setSyncCallback
kholood-ea Sep 11, 2024
c6b9173
fix: import type
kholood-ea Sep 12, 2024
3b1c37b
fix: assert evaluate sync returns correct value
kholood-ea Sep 16, 2024
446e9e4
fix: import type
kholood-ea Sep 16, 2024
8187f60
fix: cleanup
kholood-ea Sep 16, 2024
d0e972c
chore: update js doc
kholood-ea Sep 16, 2024
c700600
fix: typo
kholood-ea Sep 16, 2024
38bf28f
fix: follow interface naming convention
kholood-ea Sep 16, 2024
8cef372
fix: update type
kholood-ea Sep 16, 2024
40556ec
fix: refactor syncCallback
kholood-ea Sep 16, 2024
bc4f699
fix: default syncing session to true
kholood-ea Sep 16, 2024
6eec19f
fix: convert network logs to readable array
kholood-ea Sep 16, 2024
b3c0a31
chore: add discriptive comment
kholood-ea Sep 16, 2024
e50fbef
chore: use readable map for session metadata
a7medev Sep 16, 2024
d08d84c
fix: setSyncCallback should sync in case of exception
kholood-ea Sep 18, 2024
6ee32a8
fix: move SessionMetadata to models
kholood-ea Sep 18, 2024
6bf9f32
fix: update SessionMetadata type import
kholood-ea Sep 18, 2024
df89665
fix: report bug e2e test
kholood-ea Sep 18, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -9,4 +9,6 @@ final class Constants {

final static String IBG_ON_NEW_MESSAGE_HANDLER = "IBGonNewMessageHandler";
final static String IBG_ON_NEW_REPLY_RECEIVED_CALLBACK = "IBGOnNewReplyReceivedCallback";
final static String IBG_SESSION_REPLAY_ON_SYNC_CALLBACK_INVOCATION = "IBGSessionReplayOnSyncCallback";

}
Original file line number Diff line number Diff line change
@@ -1,24 +1,40 @@
package com.instabug.reactlibrary;


import androidx.annotation.NonNull;
import androidx.annotation.Nullable;

import com.facebook.react.bridge.Arguments;
import com.facebook.react.bridge.Promise;
import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.bridge.ReactContextBaseJavaModule;
import com.facebook.react.bridge.ReactMethod;
import com.instabug.chat.Replies;
import com.facebook.react.bridge.WritableMap;
import com.instabug.library.OnSessionReplayLinkReady;
import com.instabug.library.SessionSyncListener;
import com.instabug.library.sessionreplay.SessionReplay;
import com.instabug.library.sessionreplay.model.SessionMetadata;
import com.instabug.reactlibrary.utils.EventEmitterModule;
import com.instabug.reactlibrary.utils.MainThreadHandler;
import java.util.concurrent.CountDownLatch;

import javax.annotation.Nonnull;

public class RNInstabugSessionReplayModule extends ReactContextBaseJavaModule {
public class RNInstabugSessionReplayModule extends EventEmitterModule {

public RNInstabugSessionReplayModule(ReactApplicationContext reactApplicationContext) {
super(reactApplicationContext);
}

@ReactMethod
public void addListener(String event) {
super.addListener(event);
}

@ReactMethod
public void removeListeners(Integer count) {
super.removeListeners(count);
}

@Nonnull
@Override
public String getName() {
Expand Down Expand Up @@ -99,4 +115,55 @@ public void onSessionReplayLinkReady(@Nullable String link) {


}

volatile boolean shouldSync = false;
CountDownLatch latch;
@ReactMethod
public void setSyncCallback() {
MainThreadHandler.runOnMainThread(new Runnable() {
@Override
public void run() {
try {
SessionReplay.setSyncCallback(new SessionSyncListener() {
@Override
public boolean onSessionReadyToSync(@NonNull SessionMetadata sessionMetadata) {
WritableMap params = Arguments.createMap();
params.putString("appVersion",sessionMetadata.getAppVersion());
params.putString("OS",sessionMetadata.getOs());
params.putString("device",sessionMetadata.getDevice());
params.putDouble("sessionDurationInSeconds",(double)sessionMetadata.getSessionDurationInSeconds());

sendEvent(Constants.IBG_SESSION_REPLAY_ON_SYNC_CALLBACK_INVOCATION,params);

latch = new CountDownLatch(1);

try {
latch.await();
} catch (InterruptedException e) {
e.printStackTrace();
}

return shouldSync;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's default to returning true if an exception is thrown in latch.await() or remove the try-catch and throw the exception to the native SDK to handle it if possible.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@kholood-ea We need to resolve this one before we merge the PR to avoid unintentional return values from the sync callback.

}
});
}
catch(Exception e){
e.printStackTrace();
}

}
});
}

@ReactMethod
public void evaluateSync(boolean result) {
shouldSync = result;

if (latch != null) {
latch.countDown();
}
}



}
Original file line number Diff line number Diff line change
@@ -1,28 +1,26 @@
package com.instabug.reactlibrary;

import static junit.framework.TestCase.assertEquals;
import static junit.framework.TestCase.assertTrue;

import static org.mockito.Matchers.any;
import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.mockStatic;
import static org.mockito.Mockito.timeout;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;

import android.os.Handler;
import android.os.Looper;

import com.facebook.react.bridge.Arguments;
import com.facebook.react.bridge.JavaOnlyArray;
import com.facebook.react.bridge.JavaOnlyMap;
import com.facebook.react.bridge.Promise;
import com.facebook.react.bridge.ReadableArray;
import com.facebook.react.bridge.WritableArray;
import com.instabug.chat.Replies;
import com.instabug.featuresrequest.ActionType;
import com.instabug.featuresrequest.FeatureRequests;
import com.instabug.library.Feature;
import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.bridge.WritableMap;
import com.instabug.library.OnSessionReplayLinkReady;
import com.instabug.library.SessionSyncListener;
import com.instabug.library.sessionreplay.SessionReplay;
import com.instabug.library.sessionreplay.model.SessionMetadata;
import com.instabug.reactlibrary.utils.MainThreadHandler;

import org.junit.After;
Expand All @@ -33,6 +31,7 @@
import org.mockito.invocation.InvocationOnMock;
import org.mockito.stubbing.Answer;

import java.util.concurrent.CountDownLatch;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;

Expand All @@ -44,8 +43,8 @@ public class RNInstabugSessionReplayModuleTest {

// Mock Objects
private MockedStatic<Looper> mockLooper;
private MockedStatic <MainThreadHandler> mockMainThreadHandler;
private MockedStatic <SessionReplay> mockSessionReplay;
private MockedStatic<MainThreadHandler> mockMainThreadHandler;
private MockedStatic<SessionReplay> mockSessionReplay;

@Before
public void mockMainThreadHandler() throws Exception {
Expand Down Expand Up @@ -107,7 +106,7 @@ public void testSetInstabugLogsEnabled() {
@Test
public void testGetSessionReplayLink() {
Promise promise = mock(Promise.class);
String link="instabug link";
String link = "instabug link";

mockSessionReplay.when(() -> SessionReplay.getSessionReplayLink(any())).thenAnswer(
invocation -> {
Expand Down Expand Up @@ -136,5 +135,51 @@ public void testSetUserStepsEnabled() {
mockSessionReplay.verifyNoMoreInteractions();
}

@Test

public void testSetSyncCallback() throws Exception {
MockedStatic mockArgument = mockStatic(Arguments.class);
RNInstabugSessionReplayModule SRModule = spy(new RNInstabugSessionReplayModule(mock(ReactApplicationContext.class)));

CountDownLatch latch =new CountDownLatch(1);
SRModule.latch=latch;

when(Arguments.createMap()).thenReturn(new JavaOnlyMap());

mockSessionReplay.when(() -> SessionReplay.setSyncCallback(any(SessionSyncListener.class)))
.thenAnswer(new Answer<Void>() {
@Override
public Void answer(InvocationOnMock invocation) {
((SessionSyncListener) invocation.getArguments()[0]).onSessionReadyToSync(new SessionMetadata("device","android","1.0",20));
return null;
}
});

Thread thread= new Thread (() ->{
try {
Thread.sleep(500);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
SRModule.evaluateSync(true);
});
thread.start();

SRModule.setSyncCallback();

WritableMap params = Arguments.createMap();
params.putString("appVersion","1.0");
params.putString("OS","android");
params.putString("device","device");
params.putDouble("sessionDurationInSeconds",20);

assertEquals(SRModule.shouldSync,true);
assertTrue("Latch should be zero after evaluateSync is called", SRModule.latch.getCount() == 0);

verify(SRModule).sendEvent(Constants.IBG_SESSION_REPLAY_ON_SYNC_CALLBACK_INVOCATION, params);
mockSessionReplay.verify(() -> SessionReplay.setSyncCallback(any(SessionSyncListener.class)));

mockArgument.close();
}

}
19 changes: 19 additions & 0 deletions examples/default/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import Instabug, {
InvocationEvent,
LogLevel,
ReproStepsMode,
SessionReplay,
} from 'instabug-reactnative';
import { NativeBaseProvider } from 'native-base';

Expand All @@ -20,8 +21,26 @@ import { QueryClient, QueryClientProvider } from 'react-query';
const queryClient = new QueryClient();

export const App: React.FC = () => {
const shouldSyncSession = (data: {
appVersion: string;
OS: string;
device: string;
sessionDurationInSeconds: number;
}) => {
if (data.sessionDurationInSeconds > 20) {
return true;
}
if (data.OS === 'OS Level 34') {
return true;
}
return false;
};

const navigationRef = useNavigationContainerRef();

useEffect(() => {
SessionReplay.setSyncCallback((data) => shouldSyncSession(data));

Instabug.init({
token: 'deb1910a7342814af4e4c9210c786f35',
invocationEvents: [InvocationEvent.floatingButton],
Expand Down
37 changes: 36 additions & 1 deletion src/modules/SessionReplay.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { NativeSessionReplay } from '../native/NativeSessionReplay';
import { NativeSessionReplay, NativeEvents, emitter } from '../native/NativeSessionReplay';

/**
* Enables or disables Session Replay for your Instabug integration.
Expand Down Expand Up @@ -75,3 +75,38 @@ export const setUserStepsEnabled = (isEnabled: boolean) => {
export const getSessionReplayLink = async (): Promise<string> => {
return NativeSessionReplay.getSessionReplayLink();
};

/**
* Set a callback for weather this session should sync
*
* @param handler

* @example
* ```ts
* SessionReplay.setSyncCallback((payload)=>{
* if(payload.device == "Xiaomi M2007J3SY" &&
* payload.os == "OS Level 33" &&
* payload.appVersion == "3.1.4 (4)" ||
* payload.sessionDurationInSeconds > 20 )
* {return true}
* });
* ```
*/
export const setSyncCallback = async (
handler: (payload: {
appVersion: string;
OS: string;
device: string;
sessionDurationInSeconds: number;
}) => boolean,
): Promise<void> => {
emitter.addListener(NativeEvents.SESSION_REPLAY_ON_SYNC_CALLBACK_INVOCATION, (payload) => {
if (typeof handler(payload) !== 'boolean') {
console.warn('setSyncCallback expects boolean value');
}

NativeSessionReplay.evaluateSync(Boolean(handler(payload)));
});

return NativeSessionReplay.setSyncCallback();
};
9 changes: 8 additions & 1 deletion src/native/NativeSessionReplay.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type { NativeModule } from 'react-native';
import { NativeEventEmitter, type NativeModule } from 'react-native';

import { NativeModules } from './NativePackage';

Expand All @@ -8,6 +8,13 @@ export interface SessionReplayNativeModule extends NativeModule {
setInstabugLogsEnabled(isEnabled: boolean): void;
setUserStepsEnabled(isEnabled: boolean): void;
getSessionReplayLink(): Promise<string>;
setSyncCallback(): Promise<void>;
evaluateSync(shouldSync: boolean): void;
}

export const NativeSessionReplay = NativeModules.IBGSessionReplay;
export enum NativeEvents {
SESSION_REPLAY_ON_SYNC_CALLBACK_INVOCATION = 'IBGSessionReplayOnSyncCallback',
}

export const emitter = new NativeEventEmitter(NativeSessionReplay);
2 changes: 2 additions & 0 deletions test/mocks/mockSessionReplay.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ const mockSessionReplay: SessionReplayNativeModule = {
setInstabugLogsEnabled: jest.fn(),
setUserStepsEnabled: jest.fn(),
getSessionReplayLink: jest.fn().mockReturnValue('link'),
setSyncCallback: jest.fn(),
evaluateSync: jest.fn(),
};

export default mockSessionReplay;
13 changes: 12 additions & 1 deletion test/modules/SessionReplay.spec.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import * as SessionReplay from '../../src/modules/SessionReplay';
import { NativeSessionReplay } from '../../src/native/NativeSessionReplay';
import { NativeSessionReplay, emitter, NativeEvents } from '../../src/native/NativeSessionReplay';

describe('Session Replay Module', () => {
it('should call the native method setEnabled', () => {
Expand Down Expand Up @@ -36,4 +36,15 @@ describe('Session Replay Module', () => {
expect(NativeSessionReplay.getSessionReplayLink).toBeCalledTimes(1);
expect(NativeSessionReplay.getSessionReplayLink).toReturnWith('link');
});

it('should call the native method setSyncCallback', () => {
const callback = jest.fn().mockReturnValue(true);

SessionReplay.setSyncCallback(callback);
emitter.emit(NativeEvents.SESSION_REPLAY_ON_SYNC_CALLBACK_INVOCATION);

expect(NativeSessionReplay.setSyncCallback).toBeCalledTimes(1);
expect(emitter.listenerCount(NativeEvents.SESSION_REPLAY_ON_SYNC_CALLBACK_INVOCATION)).toBe(1);
expect(NativeSessionReplay.evaluateSync).toBeCalledTimes(1);
});
});