Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add rtcAnalytics slice and remaining rtcstats events #175

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
1 change: 1 addition & 0 deletions src/lib/core/redux/slices/deviceCredentials.ts
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@ export const doGetDeviceCredentials = createAppAsyncThunk(

export const selectDeviceCredentialsRaw = (state: RootState) => state.deviceCredentials;
export const selectHasFetchedDeviceCredentials = (state: RootState) => !!state.deviceCredentials.data?.credentials;
export const selectDeviceId = (state: RootState) => state.deviceCredentials.data?.credentials?.uuid;

/**
* Reactors
Expand Down
5 changes: 5 additions & 0 deletions src/lib/core/redux/slices/localParticipant.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { signalEvents } from "./signalConnection/actions";

export interface LocalParticipantState extends LocalParticipant {
isScreenSharing: boolean;
roleName: string;
}

const initialState: LocalParticipantState = {
Expand All @@ -21,6 +22,7 @@ const initialState: LocalParticipantState = {
isLocalParticipant: true,
stream: undefined,
isScreenSharing: false,
roleName: "",
};

export const doEnableAudio = createAppAsyncThunk(
Expand Down Expand Up @@ -100,9 +102,11 @@ export const localParticipantSlice = createSlice({
};
});
builder.addCase(signalEvents.roomJoined, (state, action) => {
const client = action.payload?.room?.clients.find((c) => c.id === action.payload?.selfId);
return {
...state,
id: action.payload.selfId,
roleName: client?.role.roleName || "",
};
});
},
Expand All @@ -112,6 +116,7 @@ export const { doSetLocalParticipant } = localParticipantSlice.actions;

export const selectLocalParticipantRaw = (state: RootState) => state.localParticipant;
export const selectSelfId = (state: RootState) => state.localParticipant.id;
export const selectLocalParticipantRole = (state: RootState) => state.localParticipant.roleName;
export const selectLocalParticipantIsScreenSharing = (state: RootState) => state.localParticipant.isScreenSharing;

startAppListening({
Expand Down
202 changes: 202 additions & 0 deletions src/lib/core/redux/slices/rtcAnalytics.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,202 @@
import { PayloadAction, createSlice } from "@reduxjs/toolkit";
import { RootState } from "../store";
import { createAppThunk } from "../thunk";
import { createReactor, startAppListening } from "../listenerMiddleware";
import { selectRtcConnectionRaw, selectRtcManagerInitialized, selectRtcStatus } from "./rtcConnection";
import { selectAppDisplayName, selectAppExternalId } from "./app";
import { selectOrganizationId } from "./organization";
import { selectLocalParticipantRole, selectSelfId } from "./localParticipant";
import { selectSignalStatus } from "./signalConnection";
import { selectDeviceId } from "./deviceCredentials";
import {
selectIsCameraEnabled,
selectIsMicrophoneEnabled,
selectLocalMediaStream,
selectScreenshareStream,
} from "./localMedia";

type RtcAnalyticsCustomEvent = {
actionType: string;
rtcEventName: string;
getValue: (state: RootState) => unknown;
getOutput: (value: unknown) => unknown;
};

export const rtcAnalyticsCustomEvents: { [key: string]: RtcAnalyticsCustomEvent } = {
jamesdools-whereby marked this conversation as resolved.
Show resolved Hide resolved
audioEnabled: {
actionType: "localParticipant/doEnableAudio/fulfilled",
rtcEventName: "audioEnabled",
getValue: (state: RootState) => selectIsMicrophoneEnabled(state),
getOutput: (value) => ({ enabled: value }),
},
videoEnabled: {
actionType: "localParticipant/doEnableVideo/fulfilled",
rtcEventName: "videoEnabled",
getValue: (state: RootState) => selectIsCameraEnabled(state),
getOutput: (value) => ({ enabled: value }),
},
localStream: {
actionType: "localMedia/reactSetDevice/fulfilled",
rtcEventName: "localStream",
getValue: (state: RootState) =>
selectLocalMediaStream(state)
?.getTracks()
.map((track) => ({ id: track.id, kind: track.kind, label: track.label })),
getOutput: (value) => ({ stream: value }),
},
localScreenshareStream: {
actionType: "localMedia/doStartScreenshare/fulfilled",
rtcEventName: "localScreenshareStream",
getValue: (state: RootState) =>
selectScreenshareStream(state)
?.getTracks()
.map((track) => ({ id: track.id, kind: track.kind, label: track.label })),
getOutput: (value) => ({ tracks: value }),
},
localScreenshareStreamStopped: {
actionType: "localMedia/stopScreenshare",
rtcEventName: "localScreenshareStream",
getValue: () => () => null,
getOutput: () => ({}),
},
displayName: {
actionType: "localParticipant/doSetDisplayName/fulfilled",
rtcEventName: "displayName",
getValue: (state: RootState) => selectAppDisplayName(state),
getOutput: (value) => ({ displayName: value }),
},
clientId: {
actionType: "",
rtcEventName: "clientId",
getValue: (state: RootState) => selectSelfId(state),
getOutput: (value) => ({ clientId: value }),
},
deviceId: {
actionType: "",
rtcEventName: "deviceId",
getValue: (state: RootState) => selectDeviceId(state),
getOutput: (value) => ({ deviceId: value }),
},
externalId: {
actionType: "",
rtcEventName: "externalId",
getValue: (state: RootState) => selectAppExternalId(state),
getOutput: (value) => ({ externalId: value }),
},
organizationId: {
actionType: "",
rtcEventName: "organizationId",
getValue: (state: RootState) => selectOrganizationId(state),
getOutput: (value) => ({ organizationId: value }),
},
signalConnectionStatus: {
actionType: "",
rtcEventName: "signalConnectionStatus",
getValue: (state: RootState) => selectSignalStatus(state),
getOutput: (value) => ({ status: value }),
},
rtcConnectionStatus: {
actionType: "",
rtcEventName: "rtcConnectionStatus",
getValue: (state: RootState) => selectRtcStatus(state),
getOutput: (value) => ({ status: value }),
},
userRole: {
actionType: "",
rtcEventName: "userRole",
getValue: (state: RootState) => selectLocalParticipantRole(state),
getOutput: (value) => ({ userRole: value }),
},
};

const makeComparable = (value: unknown) => {
if (typeof value === "object") return JSON.stringify(value);

return value;
};

/**
* Reducer
*/

export interface rtcAnalyticsState {
reportedValues: { [key: string]: unknown };
}

const initialState: rtcAnalyticsState = {
reportedValues: {},
};

export const rtcAnalyticsSlice = createSlice({
initialState,
name: "rtcAnalytics",
reducers: {
updateReportedValues(state, action: PayloadAction<{ rtcEventName: string; value: unknown }>) {
state.reportedValues[action.payload.rtcEventName] = action.payload.value;
},
},
});

export const doRtcAnalyticsCustomEventsInitialize = createAppThunk(() => (dispatch, getState) => {
const state = getState();
const rtcManager = selectRtcConnectionRaw(state).rtcManager;

if (!rtcManager) return;

Object.values(rtcAnalyticsCustomEvents).forEach(({ rtcEventName, getValue, getOutput }) => {
const value = getValue(state);
const output = { ...(getOutput(value) as Record<string, unknown>), _time: Date.now() };

const comparableValue = makeComparable(value);

if (state.rtcAnalytics.reportedValues?.[rtcEventName] !== comparableValue) {
rtcManager.sendStatsCustomEvent(rtcEventName, output);
dispatch(updateReportedValues({ rtcEventName, value }));
}
});
});

/**
* Action creators
*/
export const { updateReportedValues } = rtcAnalyticsSlice.actions;

startAppListening({
predicate: (_action) => {
jamesdools-whereby marked this conversation as resolved.
Show resolved Hide resolved
const rtcCustomEventActions = Object.values(rtcAnalyticsCustomEvents).map(({ actionType }) => actionType);

const isRtcEvent = rtcCustomEventActions.includes(_action.type);

return isRtcEvent;
},
effect: ({ type }, { getState, dispatch }) => {
const state: RootState = getState();

const rtcManager = selectRtcConnectionRaw(state).rtcManager;
if (!rtcManager) return;

const rtcCustomEvent = Object.values(rtcAnalyticsCustomEvents).find(({ actionType }) => actionType === type);
if (!rtcCustomEvent) return;

const { getValue, getOutput, rtcEventName } = rtcCustomEvent;

const value = getValue(state);
const comparableValue = makeComparable(value);
const output = { ...(getOutput(value) as Record<string, unknown>), _time: Date.now() };

if (state.rtcAnalytics.reportedValues?.[rtcEventName] !== comparableValue) {
rtcManager.sendStatsCustomEvent(rtcEventName, output);
dispatch(updateReportedValues({ rtcEventName, value }));
}
},
});

/**
* Reactors
*/

createReactor([selectRtcManagerInitialized], ({ dispatch }, selectRtcManagerInitialized) => {
if (selectRtcManagerInitialized) {
dispatch(doRtcAnalyticsCustomEventsInitialize());
jamesdools-whereby marked this conversation as resolved.
Show resolved Hide resolved
}
});
47 changes: 0 additions & 47 deletions src/lib/core/redux/slices/rtcConnection/actions.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,6 @@
import { createAction } from "@reduxjs/toolkit";
import { RtcManagerCreatedPayload, RtcStreamAddedPayload } from "@whereby/jslib-media/src/webrtc/RtcManagerDispatcher";

import { RootState } from "../../store";
import { selectIsCameraEnabled, selectIsMicrophoneEnabled, selectScreenshareStream } from "../localMedia";
import { selectAppDisplayName } from "../app";

function createRtcEventAction<T>(name: string) {
return createAction<T>(`rtcConnection/event/${name}`);
}
Expand All @@ -14,46 +10,3 @@ export const rtcEvents = {
rtcManagerDestroyed: createRtcEventAction<void>("rtcManagerDestroyed"),
streamAdded: createRtcEventAction<RtcStreamAddedPayload>("streamAdded"),
};

type RtcAnalyticsCustomEvent = {
actionType: string;
rtcEventName: string;
getValue: (state: RootState) => unknown;
getOutput: (value: unknown) => unknown;
};

export const rtcAnalyticsCustomEvents: { [key: string]: RtcAnalyticsCustomEvent } = {
audioEnabled: {
actionType: "localParticipant/doEnableAudio/fulfilled",
rtcEventName: "audioEnabled",
getValue: (state: RootState) => selectIsMicrophoneEnabled(state),
getOutput: (value) => ({ enabled: value }),
},
videoEnabled: {
actionType: "localParticipant/doEnableVideo/fulfilled",
rtcEventName: "videoEnabled",
getValue: (state: RootState) => selectIsCameraEnabled(state),
getOutput: (value) => ({ enabled: value }),
},
localScreenshareStream: {
actionType: "localMedia/doStartScreenshare/fulfilled",
rtcEventName: "localScreenshareStream",
getValue: (state: RootState) =>
selectScreenshareStream(state)
?.getTracks()
.map((track) => ({ id: track.id, kind: track.kind, label: track.label })),
getOutput: (value) => ({ tracks: value }),
},
localScreenshareStreamStopped: {
actionType: "localMedia/stopScreenshare",
rtcEventName: "localScreenshareStream",
getValue: () => () => null,
getOutput: () => ({}),
},
displayName: {
actionType: "localParticipant/doSetDisplayName/fulfilled",
rtcEventName: "displayName",
getValue: (state: RootState) => selectAppDisplayName(state),
getOutput: (value) => ({ displayName: value }),
},
};
31 changes: 1 addition & 30 deletions src/lib/core/redux/slices/rtcConnection/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ import {
doStartScreenshare,
stopScreenshare,
} from "../localMedia";
import { rtcAnalyticsCustomEvents, rtcEvents } from "./actions";
import { rtcEvents } from "./actions";
import { StreamStatusUpdate } from "./types";
import { signalEvents } from "../signalConnection/actions";

Expand Down Expand Up @@ -307,35 +307,6 @@ export const selectIsAcceptingStreams = (state: RootState) => state.rtcConnectio
/**
* Reactors
*/
startAppListening({
predicate: (_action) => {
const rtcCustomEventActions = Object.values(rtcAnalyticsCustomEvents).map(({ actionType }) => actionType);

const isRtcEvent = rtcCustomEventActions.includes(_action.type);

return isRtcEvent;
},
effect: ({ type }, { getState }) => {
const state: RootState = getState();
const rtcManager = selectRtcConnectionRaw(state).rtcManager;

const rtcCustomEvent = Object.values(rtcAnalyticsCustomEvents).find(({ actionType }) => actionType === type);

if (!rtcCustomEvent) {
throw new Error("No rtc custom event");
}

const { getValue, getOutput, rtcEventName } = rtcCustomEvent;
const value = getValue(state);
const output = { ...(getOutput(value) as Record<string, unknown>), _time: Date.now() };

if (!rtcManager) {
throw new Error("No rtc manager");
}

rtcManager.sendStatsCustomEvent(rtcEventName, output);
},
});

startAppListening({
actionCreator: doSetDevice.fulfilled,
Expand Down
2 changes: 2 additions & 0 deletions src/lib/core/redux/store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import { organizationSlice } from "./slices/organization";
import { remoteParticipantsSlice } from "./slices/remoteParticipants";
import { roomConnectionSlice } from "./slices/roomConnection";
import { signalConnectionSlice } from "./slices/signalConnection";
import { rtcAnalyticsSlice } from "./slices/rtcAnalytics";
import { rtcConnectionSlice } from "./slices/rtcConnection";
import { streamingSlice } from "./slices/streaming";
import { waitingParticipantsSlice } from "./slices/waitingParticipants";
Expand All @@ -28,6 +29,7 @@ export const rootReducer = combineReducers({
organization: organizationSlice.reducer,
remoteParticipants: remoteParticipantsSlice.reducer,
roomConnection: roomConnectionSlice.reducer,
rtcAnalytics: rtcAnalyticsSlice.reducer,
rtcConnection: rtcConnectionSlice.reducer,
signalConnection: signalConnectionSlice.reducer,
streaming: streamingSlice.reducer,
Expand Down
31 changes: 31 additions & 0 deletions src/lib/core/redux/tests/store/rtcAnalytics.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { createStore } from "../store.setup";
import { doRtcAnalyticsCustomEventsInitialize, rtcAnalyticsState } from "../../slices/rtcAnalytics";
import { diff } from "deep-object-diff";

describe("actions", () => {
it("doRtcAnalyticsCustomEventsInitialize", async () => {
const store = createStore({ withRtcManager: true });

const before = store.getState().rtcAnalytics;

store.dispatch(doRtcAnalyticsCustomEventsInitialize());

const after = store.getState().rtcAnalytics;

const updatedState = diff(before, after) as rtcAnalyticsState;

expect(Object.keys(updatedState?.reportedValues)).toEqual(
expect.arrayContaining([
"audioEnabled",
"videoEnabled",
"localScreenshareStream",
"displayName",
"clientId",
"externalId",
"signalConnectionStatus",
"rtcConnectionStatus",
"userRole",
])
);
});
});
Loading